Section wrapper
A page section with four tones, four spacing presets and four container widths. Same-tone neighbours merge into one band, anchor jumps clear a sticky header, and a heading id makes it a named region.
cmp_section_wrapper_01 Preview
Fit to the available width. The frame follows the height of its content; previews taller than the maximum auto-height scroll inside it.
Give this component to your coding agent Copy a prompt that fetches this exact version and palette through the PageSugar MCP server.
Using the PageSugar MCP server, fetch component cmp_section_wrapper_01 version 1.0.0 with variant "neutral", first inspect its requirements and license status and confirm this project uses Svelte 5 and Tailwind CSS 4. Retrieve every manifest file, including binary assets and any manifest-only response files, preserving relative paths. Then integrate the source and follow its usage notes. Run project checks, review the browser result and report anything unverified. Do not substitute another version or invent missing files.Not connected yet? Set up the MCP server
Code
- Palette
- Neutral
- Version
- 1.0.0
- Digest
Full digest
sha256-dbe848d7690641a787819a6b7fdecc18628e9c31915d31932eb80027cdaaa9e6
<script lang="ts" module>
export type SectionSpacing = 'none' | 'sm' | 'md' | 'lg';
export type SectionTone = 'default' | 'muted' | 'inverse' | 'accent';
export type SectionWidth = 'prose' | 'default' | 'wide' | 'full';
</script>
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
/** Anchor id, so `#id` links land on this section below the sticky header. */
id?: string;
/** Vertical padding preset. */
spacing?: SectionSpacing;
/** Background and text tone. */
tone?: SectionTone;
/** Inner container width. */
width?: SectionWidth;
/** Id of the heading inside that names this section. */
labelledby?: string;
/** The section's content. */
children: Snippet;
}
let {
id,
spacing = 'md',
tone = 'default',
width = 'default',
labelledby,
children
}: Props = $props();
/*
* Section padding (DESIGN §3.3). md is the standard marketing section; sm is for a band that
* leans on its neighbours (a logo strip, a short CTA) and still clears 80 px on desktop.
*/
const padding: Record<SectionSpacing, string> = {
none: 'py-0',
sm: 'py-12 sm:py-16 lg:py-20',
md: 'py-16 sm:py-24 lg:py-32',
lg: 'py-20 sm:py-32 lg:py-40'
};
/*
* Gutters sit on the section and the measure on the container inside it, the same shape as the
* catalogue's own sections, so default content is 1152 px and every section on one width
* starts on the same edge. Prose is a narrower centred column for long reading. Full hands the
* whole width to its content, which brings its own gutters: a catalogue section that already
* pads and constrains itself, or media that runs edge to edge.
*/
const gutters = 'px-4 sm:px-6 lg:px-8';
const measure: Record<SectionWidth, string> = {
prose: 'mx-auto max-w-2xl',
default: 'mx-auto max-w-6xl',
wide: 'mx-auto max-w-7xl',
full: ''
};
</script>
<section
{id}
class={['section-wrapper', padding[spacing], width !== 'full' && gutters]}
data-tone={tone}
aria-labelledby={labelledby || undefined}
>
<div class={['w-full', measure[width]]}>
{@render children?.()}
</div>
</section>
<style>
/*
* Public tokens: set --section-wrapper-* on this section or any ancestor to retone it. Each
* tone resolves them into four --section-tone-* values that content inside can read, so a
* heading or a hairline follows the band it sits on.
*/
.section-wrapper {
--_accent: var(--section-wrapper-accent, #18181b);
--_on-accent: var(--section-wrapper-on-accent, #ffffff);
--_ink: var(--section-wrapper-ink, #18181b);
--_muted: var(--section-wrapper-muted, #52525b);
--_hairline: var(--section-wrapper-hairline, rgb(0 0 0 / 0.08));
--_surface: var(--section-wrapper-surface, transparent);
--_muted-surface: var(--section-wrapper-muted-surface, #f4f4f5);
--_inverse-surface: var(--section-wrapper-inverse-surface, #09090b);
--_on-inverse: var(--section-wrapper-on-inverse, #fafafa);
--_inverse-muted: var(--section-wrapper-inverse-muted, #a1a1aa);
--_inverse-scheme: var(--section-wrapper-inverse-scheme, dark);
--_accent-scheme: var(--section-wrapper-accent-scheme, dark);
--_header-offset: var(--section-wrapper-header-offset, 0px);
--section-tone-surface: var(--_surface);
--section-tone-ink: var(--_ink);
--section-tone-muted: var(--_muted);
--section-tone-hairline: var(--_hairline);
background-color: var(--section-tone-surface);
color: var(--section-tone-ink);
/* An anchor jump stops below the site's sticky header instead of under it. */
scroll-margin-block-start: var(--_header-offset);
}
.section-wrapper[data-tone='muted'] {
--section-tone-surface: var(--_muted-surface);
}
.section-wrapper[data-tone='inverse'] {
--section-tone-surface: var(--_inverse-surface);
--section-tone-ink: var(--_on-inverse);
--section-tone-muted: var(--_inverse-muted);
--section-tone-hairline: color-mix(in oklab, var(--_on-inverse) 12%, transparent);
color-scheme: var(--_inverse-scheme);
}
.section-wrapper[data-tone='accent'] {
--section-tone-surface: var(--_accent);
--section-tone-ink: var(--_on-accent);
--section-tone-muted: color-mix(in oklab, var(--_on-accent) 84%, var(--_accent));
--section-tone-hairline: color-mix(in oklab, var(--_on-accent) 20%, transparent);
color-scheme: var(--_accent-scheme);
}
/*
* Two neighbours on the same tone read as one band, so the second drops its top padding and
* the gap between them is the first one's bottom padding, not both. Its anchor stop keeps a
* little air above the heading that now sits at its top edge.
*/
:global(.section-wrapper[data-tone='default']) + .section-wrapper[data-tone='default'],
:global(.section-wrapper[data-tone='muted']) + .section-wrapper[data-tone='muted'],
:global(.section-wrapper[data-tone='inverse']) + .section-wrapper[data-tone='inverse'],
:global(.section-wrapper[data-tone='accent']) + .section-wrapper[data-tone='accent'] {
padding-block-start: 0;
scroll-margin-block-start: calc(var(--_header-offset) + 2rem);
}
</style>
Usage#
On this pagePresentational layout only: it paints a tone, pads the section and constrains its content. It renders no heading, eyebrow or copy of its own; you pass those as children and hand their heading id to labelledby. It sets inherited defaults (text colour, and color-scheme on the dark tones) but never overrides colours your content or other components set explicitly.
- Suggested location
src/lib/components/section-wrapper-01- Required props
children
Limitations
- Renders no heading. Pass one as a child and give its id to labelledby, or the section stays an unnamed generic container.
- Components placed inside keep their own colour tokens, surfaces included: on the inverse or accent tone, point every colour they paint at the tone (for example surface, ink, muted and hairline to var(
--section-tone-surface), var(--section-tone-ink), var(--section-tone-muted) and var(--section-tone-hairline)).usage.mdhas a worked example. - The same-tone collapse uses the adjacent-sibling selector, so it only applies when two sections are direct siblings; a wrapper element or a hidden element between them keeps both paddings.
- The header offset defaults to 0. Set
--section-wrapper-header-offsetto your sticky header's height, and do not also set html { scroll-padding-top }, or the two offsets add up. A section that follows a same-tone neighbour stops 2rem further down, since it has no top padding. - The offset is scroll-margin on the section, so it covers jumps to the section itself, not focus moving to a link or field inside it; use html { scroll-padding-top } instead if focused controls can land under the header.
- In the neutral palette the accent tone is near-black, close to the inverse tone; the accent tone becomes a brand band once
--section-wrapper-accentis set. - Light appearance by default. The tone tokens retone it for a dark page, but no dark mode is declared or selected automatically.
Example
<script lang="ts">
import Section from '$lib/components/section-wrapper-01/Section.svelte';
</script>
<Section id="plans" tone="inverse" labelledby="plans-title">
<h2 id="plans-title" class="text-3xl font-semibold tracking-tight text-[var(--section-tone-ink)]">
Starter is free for up to three people
</h2>
<p class="mt-4 max-w-xl text-lg text-[var(--section-tone-muted)]">
Team is $12 per seat per month and Business is $24.
</p>
</Section>Building a page from sections#
Copy Section.svelte into src/lib/components/section-wrapper-01/. Each band of the page is one Section; put your heading, copy and components inside it, and hand the heading's id to labelledby for the sections a reader would navigate to.
<script lang="ts">
import Section from '$lib/components/section-wrapper-01/Section.svelte';
</script>
<Section id="timelines" labelledby="timelines-title">
<h2 id="timelines-title">Every date on the roadmap, and what it waits on</h2>
<p>Move a date and everything after it moves too.</p>
</Section>
<Section id="plans" tone="inverse" spacing="sm" labelledby="plans-title">
<h2 id="plans-title">Starter is free for up to three people</h2>
</Section>The section renders no heading of its own. It sets inherited defaults (text colour on every tone, color-scheme on the inverse and accent tones), but it never overrides a colour your markup or another component sets explicitly.
Presets#
| Prop | Values |
|---|---|
spacing |
none 0 · sm 48 / 64 / 80 px · md (default) 64 / 96 / 128 px · lg 80 / 128 / 160 px, at phone, sm and lg widths |
tone |
default (the page) · muted (one step off it) · inverse (a dark band) · accent (a band in your accent colour) |
width |
prose max-w-2xl · default max-w-6xl · wide max-w-7xl · full the whole width, no gutters |
The gutters (16, 24 and 32 px) sit on the section and the width caps the content inside them, so default gives 1152 px of content, the same shape as the catalogue's own sections. Sections on the same width share one content edge down the page; a different width centres on its own, so a prose section is a narrower centred column for long reading. On a phone they all start on the same 16 px edge. full hands the whole width to its content with no gutters, for content that brings its own: a catalogue section, or media that runs edge to edge.
Two sections on the same tone that are direct siblings read as one band: the second drops its top padding, so the gap between their content is the first section's bottom padding rather than both. Give neighbours different tones, or wrap one in its own element, to keep both paddings.
Content that follows the tone#
Each tone publishes four values for the content inside it:
| Variable | Default tone | Inverse tone |
|---|---|---|
--section-tone-ink |
#18181b |
#fafafa |
--section-tone-muted |
#52525b |
#a1a1aa |
--section-tone-hairline |
8% black | 12% of on-inverse |
--section-tone-surface |
transparent |
#09090b |
Use them in your own markup (text-[var(--section-tone-muted)], border-[var(--section-tone-hairline)]) and a heading or a divider follows the band. A catalogue section such as pricing-grid-01 already pads and constrains itself with the same padding and gutters as spacing="md" and width="default", so wrap it with spacing="none" and width="full"; it then lands on the same edge as the sections around it. It keeps its own tokens, including its surfaces, so on a dark band point every colour it paints at the tone, not only its text, or its white cards keep the dark band's pale ink:
<Section tone="inverse" spacing="none" width="full">
<div
style="
--pricing-grid-surface: var(--section-tone-surface);
--pricing-grid-ink: var(--section-tone-ink);
--pricing-grid-muted: var(--section-tone-muted);
--pricing-grid-hairline: var(--section-tone-hairline);
--pricing-grid-control-border: var(--section-tone-hairline);
--pricing-grid-accent: var(--section-tone-ink);
--pricing-grid-on-accent: var(--section-tone-surface);
"
>
<PricingGrid title="Plans and pricing" {plans} />
</div>
</Section>The inverse and accent tones also set color and color-scheme, so inherited text and native controls are drawn for a dark fill. Links you add in those bands should carry an underline or another mark beyond colour.
Sticky headers#
Set the header's height once:
:root {
--section-wrapper-header-offset: 4rem;
}A jump to a section's id then stops 4rem down, so the band's top edge sits under the header. A section that follows a same-tone neighbour has no top padding, so it stops 2rem further down (the offset plus 2rem) to keep its heading off the header. Near the end of a short page the browser may not be able to scroll that far.
If you already set html { scroll-padding-top } for your header, leave this token at 0: the two add up. The collapsed section's extra 2rem applies either way.
The offset belongs to the section, so it only covers jumps to the section itself. A link or field inside it that receives focus scrolls by its own margin; if keyboard focus can land under your header, set html { scroll-padding-top } instead and leave the token at 0.
Retoning#
| Variable | Default | Draws |
|---|---|---|
--section-wrapper-ink |
#18181b |
Text on the default and muted tones |
--section-wrapper-muted |
#52525b |
Secondary text on the default and muted tones |
--section-wrapper-hairline |
rgb(0 0 0 / 0.08) |
Hairline value on the default and muted tones |
--section-wrapper-surface |
transparent |
Default tone background |
--section-wrapper-muted-surface |
#f4f4f5 |
Muted tone background |
--section-wrapper-inverse-surface |
#09090b |
Inverse tone background |
--section-wrapper-on-inverse |
#fafafa |
Text on the inverse tone |
--section-wrapper-inverse-muted |
#a1a1aa |
Secondary text on the inverse tone |
--section-wrapper-accent |
#18181b |
Accent tone background |
--section-wrapper-on-accent |
#ffffff |
Text on the accent tone |
--section-wrapper-inverse-scheme |
dark |
color-scheme on the inverse tone |
--section-wrapper-accent-scheme |
dark |
color-scheme on the accent tone |
--section-wrapper-header-offset |
0px |
Anchor stop below a sticky header |
In the neutral palette the accent is near-black, so the accent band sits close to the inverse one; set your brand colour and it becomes a brand band. A dark page swaps the ramps:
<div
class="bg-zinc-950"
style="
color-scheme: dark;
--section-wrapper-surface: #09090b;
--section-wrapper-muted-surface: #18181b;
--section-wrapper-ink: #fafafa;
--section-wrapper-muted: #a1a1aa;
--section-wrapper-hairline: rgb(255 255 255 / 0.1);
--section-wrapper-inverse-surface: #fafafa;
--section-wrapper-on-inverse: #09090b;
--section-wrapper-inverse-muted: #52525b;
--section-wrapper-inverse-scheme: light;
"
>
<!-- sections -->
</div>Re-check contrast after any change: 4.5:1 for text against each tone's surface.
Props and content inputs#
On this page| Name | Type | Required | Default | Description |
|---|---|---|---|---|
children | Snippet | Yes | None | The section's content, rendered inside the inner container. |
id | string | No | None | Anchor id for #links. The section stops below the sticky header set by --section-wrapper-header-offset. |
spacing | 'none' | 'sm' | 'md' | 'lg' | No | 'md' | Vertical padding preset: none 0; sm 48/64/80 px; md 64/96/128 px; lg 80/128/160 px at phone, sm and lg breakpoints. |
tone | 'default' | 'muted' | 'inverse' | 'accent' | No | 'default' | Background and text tone: the page, one step off the page, a dark band, or a band in the accent colour. |
width | 'prose' | 'default' | 'wide' | 'full' | No | 'default' | Inner container: max-w-2xl, max-w-6xl or max-w-7xl of content inside shared 16/24/32 px gutters, or full: the whole width with no gutters, for content that brings its own (a catalogue section, edge-to-edge media). |
labelledby | string | No | None | Id of the heading inside that names the section. When set, the section is exposed as a named region; when omitted, no aria-labelledby is rendered. |
Customization#
On this pagePick a tone, spacing and width through props. Retone every tone through --section-wrapper-* variables, and read the four --section-tone-* values inside a section so your content follows the band it sits on.
- Content colours: inside a section, use var(
--section-tone-ink) for headings and body text, var(--section-tone-muted) for secondary text, var(--section-tone-hairline) for dividers and var(--section-tone-surface) for anything that must match the band. They change with the tone. - Accent: set
--section-wrapper-accentand--section-wrapper-on-accenttogether; the accent tone fills the band with the first and sets text in the second. Keep them at 4.5:1. The accent band's muted text and hairline are mixed from the pair. - Page tones:
--section-wrapper-ink,--section-wrapper-mutedand--section-wrapper-hairlineare the text and rule colours on the default and muted tones;--section-wrapper-surfaceis the default tone's background (transparent, so the page shows through) and--section-wrapper-muted-surfacethe muted band's. - Inverse:
--section-wrapper-inverse-surface,--section-wrapper-on-inverseand--section-wrapper-inverse-mutedset the dark band. Its hairline is mixed from on-inverse. - Native controls:
--section-wrapper-inverse-schemeand--section-wrapper-accent-schemeset color-scheme on those bands (dark by default). Set the accent one to light if your accent is pale, so checkboxes and selects are drawn for a light fill. - Sticky header: set
--section-wrapper-header-offseton :root to your header's height (4rem for a 64 px bar). Jumps to a section id then stop below it; a collapsed same-tone section stops 2rem further down. - Dark page retone: on the page wrapper set color-scheme: dark, then surface
#09090b, muted-surface#18181b, ink#fafafa, muted#a1a1aa, hairline rgb(255 255 255 / 0.1), inverse-surface#fafafa, on-inverse#09090b, inverse-muted#52525b, inverse-scheme light (all--section-wrapper-*).usage.mdhas the snippet. - Spacing and widths: the padding and measure maps at the top of the script hold the Tailwind classes for each preset; edit them there to change the scale for every section at once.
- Collapse: two direct-sibling sections on the same tone merge. To keep a gap between them, give them different tones or put the second in its own wrapper.
- Catalogue sections:
pricing-grid-01,faq-accordion-01and similar already pad and constrain themselves with the same classes as spacing="md" and width="default". To put one on a tone, wrap it in a Section with spacing="none" and width="full"; it keeps its own padding and gutters and starts on the same edge as the default-width sections around it.
Public CSS variables
| Variable | Token |
|---|---|
--section-wrapper-accent | accent |
--section-wrapper-on-accent | onAccent |
--section-wrapper-ink | ink |
--section-wrapper-muted | muted |
--section-wrapper-hairline | hairline |
--section-wrapper-surface | surface |
--section-wrapper-muted-surface | mutedSurface |
--section-wrapper-inverse-surface | inverseSurface |
--section-wrapper-on-inverse | onInverse |
--section-wrapper-inverse-muted | inverseMuted |
--section-wrapper-inverse-scheme | inverseScheme |
--section-wrapper-accent-scheme | accentScheme |
--section-wrapper-header-offset | headerOffset |
Accessibility#
On this page- The root is a section element. With labelledby it carries
aria-labelledbyand is exposed as a named region; without it no aria attribute is rendered, so it stays a generic container rather than an unnamed landmark. - Point labelledby at the section's own visible heading. Name only the sections a reader would navigate to; a landmark for every band makes the landmark list noisy.
- Contrast (WCAG relative luminance): ink
#18181bon white 17.7:1 and on the muted band#f4f4f516.1:1; muted#52525b7.7:1 on white and 7.0:1 on the muted band; on the inverse band#fafafameasures 19.1:1 and#a1a1aa7.8:1; white on the neutral accent 17.7:1 and on the blue accent 6.7:1. - Every tone sets the inherited text colour, and the inverse and accent tones also set color-scheme, so unstyled text, links that use
currentColorand native controls are drawn for the band. Explicit colours on content inside are left alone. - With an id, scroll-margin-block-start takes the header offset token, so a jump to the section stops below a sticky header of that height. It does not cover focus moving to a control inside the section; html { scroll-padding-top } does.
- Layout uses logical properties throughout and mirrors under dir="rtl".
Known limitations
- Links inside a band are the consumer's markup: in the inverse and accent tones, make inline links distinguishable by more than colour (an underline) and give focus rings a colour that clears 3:1 against the band, such as var(
--section-tone-ink). - Contrast figures cover the shipped palettes only; re-check any retoned token.
Release details#
On this page- Integration
- Presentational
- Works without client-side JavaScript
- Server-side rendering supported
- Dependencies
- No additional runtime packages beyond Svelte and Tailwind CSS
- License
MIT. Default license approval is pending; see the license status before adopting the source.
- Version history
- 1.0.0 (Published) Current release · 30 September 2026
Only the current release is available. Keep downloaded source and its receipt if you need to use it again later.