# Section divider

> A divider between content areas: a hairline rule, a rule split by a visible label such as 'or' or 'Week 2', or a wave or slant that carries one section tone into the next without a seam.

- ID: `cmp_section_divider_01`
- Slug: `section-divider-01`
- Version: `1.0.0` (current)
- Status: published
- Published: 2026-10-01T07:29:36Z
- Updated: 2026-10-01
- Available versions: `1.0.0`
- Kind: control
- Primary category: `layout`
- Detail page: https://pagesugar.com/components/section-divider-01
- Preview: https://pagesugar.com/preview/section-divider-01

## Variants

| Variant | Label | Default | Artifact digest |
| --- | --- | --- | --- |
| `neutral` | Neutral | yes | `sha256-00a30fdb0cccb5b2914125ba63d475e0d37486a1214192f9e44c66da125470ed` |

## Runtime and compatibility

- Runtime: svelte
- Svelte: 5
- SvelteKit required: no (portable Svelte component)
- Tailwind CSS: 4
- SSR: supported
- Requires client-side JavaScript: no
- Integration level: presentational
- Appearance modes: light
- Suggested directory: `src/lib/components/section-divider-01`

## Dependencies

No third-party runtime packages.

## Services

No external services required.

## Usage

Presentational only. It draws a rule, a labelled rule or a tone-to-tone shape and nothing else: it paints no section backgrounds, so the sections on either side of a wave or slant keep their own, and their colours have to match the divider's surface tokens. It renders no heading; a label is separator text, not a title.

Required props: none

```svelte
<script lang="ts">
	import SectionDivider from '$lib/components/section-divider-01/SectionDivider.svelte';
</script>

<section class="px-4 py-16">
	<h2 class="text-3xl font-semibold tracking-tight">See every launch on one line</h2>
</section>
<SectionDivider variant="wave" fromTone="default" toTone="muted" />
<section class="bg-zinc-100 px-4 py-16">
	<h2 class="text-3xl font-semibold tracking-tight">Starter is free for up to three people</h2>
	<SectionDivider variant="label" label="or" spacing="sm" />
</section>
```

Limitations:

- A wave or slant only looks seamless when the sections beside it use the same colours as its surface tokens; set --section-divider-surface, -muted-surface and -inverse-surface to your section backgrounds.
- Shapes overlap each neighbour by one pixel to hide sub-pixel cracks, and keep their edge clear of that pixel. Put them directly between two full-width sections; inside padding or a gap they show as a band of their own.
- A translucent surface token blends with the page and with the one-pixel stroke that hides the seam between two solid tones; use opaque colours or transparent.
- There is one wave and one slant, both rising toward the end of the line. To run one the other way, wrap it in an element with a horizontal flip.
- A label is read as the separator's name, not as a heading. For a real chapter title that should appear in a heading list, use a heading.
- The label variant without label text draws a plain rule.
- Light appearance only. Retone the tokens for a dark page; no dark mode is declared.

## Usage guide

### Section divider

Four ways to separate content, all from one component:

| Variant | Renders                                         | Exposed as                  |
| ------- | ----------------------------------------------- | --------------------------- |
| `rule`  | a one-pixel hairline (`hr`)                     | separator, or hidden        |
| `label` | a hairline split by visible text                | separator named by the text |
| `wave`  | one long, uneven swell from one tone to another | always hidden               |
| `slant` | a straight edge from one tone to another        | always hidden               |

#### Rules and labels

Use a rule or label where the content changes topic: two releases in a changelog, the chapters of
a course, "or" between two ways to sign in. Between things that space already separates, you
probably want no divider at all; where you want one for rhythm only, pass `decorative`.

```svelte
<SectionDivider />
<SectionDivider variant="label" label="or" spacing="sm" />
<SectionDivider variant="label" label="Week 2 · Shaping and proofing" />
```

A long label wraps, up to about 75 characters a line, with its lines filling its box. Each rule keeps at least 32 px, so the label never
squeezes them away on a phone.

#### Shapes between sections

Put a wave or slant directly between two full-width sections. It paints no section of its own:
the upper region is the `fromTone` surface and the lower region the `toTone` surface, and it
tucks one pixel under each neighbour so no sub-pixel crack opens between them.

```svelte
<section class="py-24">…</section>
<SectionDivider variant="wave" fromTone="default" toTone="muted" />
<section class="bg-zinc-100 py-24">…</section>
```

The shape only looks seamless when its surfaces match your sections. The defaults are
`transparent` (the page), `#f4f4f5` (`bg-zinc-100`) and `#09090b` (`bg-zinc-950`). Set them once,
on a page wrapper:

```css
.page {
	--section-divider-surface: #fffdf8;
	--section-divider-muted-surface: #f5efe6;
	--section-divider-inverse-surface: #1c1917;
}
```

With `section-wrapper-01`, point the divider at the wrapper's tokens so one retone moves both:

```css
.page {
	--section-divider-muted-surface: var(--section-wrapper-muted-surface, #f4f4f5);
	--section-divider-inverse-surface: var(--section-wrapper-inverse-surface, #09090b);
}
```

`spacing` sets the shape's depth rather than a margin: the sections either side already have
their padding. The depth steps up with the viewport so the angle stays low on a phone.

#### Right to left

Shapes mirror under `dir="rtl"`, so the lower tone still rises toward the end of the line.

## Props

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `variant` | 'rule' \| 'label' \| 'wave' \| 'slant' | no | `'rule'` | A hairline rule, a rule split by a label, or a wave or slant shape that joins two section tones. |
| `label` | `string` | no |  | Visible text for the label variant, also used as the separator's accessible name. Wraps at about 75 characters a line. Without it the label variant draws a plain rule. |
| `decorative` | `boolean` | no | `false` | Hide the rule or label from assistive tech, for a break that separates nothing in meaning. Wave and slant are always hidden. |
| `fromTone` | 'default' \| 'muted' \| 'inverse' | no | `'default'` | Wave and slant: the tone of the section above, painted from the matching surface token. |
| `toTone` | 'default' \| 'muted' \| 'inverse' | no | `'muted'` | Wave and slant: the tone of the section below. |
| `spacing` | 'sm' \| 'md' \| 'lg' | no | `'md'` | Rule and label: margin above and below (sm 32 px; md 48/64 px; lg 64/96 px). Wave and slant: the shape's depth (sm 24/32/40 px; md 32/48/64 px; lg 48/64/96 px at phone, sm and lg breakpoints). |

## Customization

Pick a variant, tones and spacing through props. Retone the label, rule and the three shape surfaces through five --section-divider-\* variables.

- Section colours: set --section-divider-surface, --section-divider-muted-surface and --section-divider-inverse-surface to the backgrounds of your default, muted and dark sections. surface is transparent by default so a wave from the page tone shows whatever the page is.
- With section-wrapper-01: on a shared ancestor set --section-divider-muted-surface: var(--section-wrapper-muted-surface) and --section-divider-inverse-surface: var(--section-wrapper-inverse-surface), so retoning the sections retones the shapes.
- Rule and label on a dark band: set --section-divider-hairline to rgb(255 255 255 / 0.12) and --section-divider-muted to #a1a1aa on that band.
- Cream or tinted page: set --section-divider-muted-surface to the tinted band (for example #f5efe6) and --section-divider-hairline to rgb(68 40 0 / 0.12), so the rule takes the page's warmth.
- Shape geometry: the paths object at the top of the script holds the wave and slant in a 1440 by 64 box. Edit the curve there; keep the lower region one unit higher than the upper one so solid tones never leave a seam.
- Spacing: the margin and depth maps hold the Tailwind classes for each preset.

| Token | Public CSS variable |
| --- | --- |
| `muted` | `--section-divider-muted` |
| `hairline` | `--section-divider-hairline` |
| `surface` | `--section-divider-surface` |
| `mutedSurface` | `--section-divider-muted-surface` |
| `inverseSurface` | `--section-divider-inverse-surface` |

## Accessibility

- The rule is an hr, exposed as a separator. With decorative it carries aria-hidden.
- The label variant is a div with role separator and aria-label set to the label; the same text stays visible. The separator role makes its children presentational, so its accessible name is the label alone; how often a screen reader speaks it depends on the reader.
- Wave and slant are always aria-hidden, with focusable false on the SVG: a change of background tone carries no meaning.
- Use a separator only where the content actually changes topic. Between items that space already separates, pass decorative.
- Label contrast: #52525b on white 7.7:1 and on #f4f4f5 7.0:1. The hairline is decorative and needs no contrast ratio.
- Layout uses logical properties; shapes mirror under dir="rtl".

Known limitations:

- Screen reader support for a named separator varies: some announce the label with the separator, some announce only the separator. If the label carries information a reader must not miss, put it in the content as well.
- Contrast figures cover the default tokens only; re-check retoned ones.

## License

- Declared source: MIT
- Default license approval is pending. See https://pagesugar.com/docs/license.

## Source

- Palette: Neutral (`neutral`)
- Entry: `SectionDivider.svelte`
- Suggested directory: `src/lib/components/section-divider-01`
- Files: 1
- Artifact digest: `sha256-00a30fdb0cccb5b2914125ba63d475e0d37486a1214192f9e44c66da125470ed`

Paths below are relative to the suggested directory. Copy the files as they are;
they import nothing from this site.

#### `SectionDivider.svelte`

Role: entry · 6063 bytes · SHA-256 `cdbab1ad84a7e9b03f6e002a847257056f2df1662eb4c1d449695dc7b96546ed`

```svelte
<script lang="ts" module>
	export type SectionDividerVariant = 'rule' | 'label' | 'wave' | 'slant';
	export type SectionDividerTone = 'default' | 'muted' | 'inverse';
	export type SectionDividerSpacing = 'sm' | 'md' | 'lg';
</script>

<script lang="ts">
	interface Props {
		/** A hairline, a hairline split by a label, or a shape that carries one tone into the next. */
		variant?: SectionDividerVariant;
		/** Visible text for the label variant. Without it, the label variant draws a plain rule. */
		label?: string;
		/** Hide the divider from assistive tech; for a break that separates nothing in meaning. */
		decorative?: boolean;
		/** Wave and slant: the tone of the section above. */
		fromTone?: SectionDividerTone;
		/** Wave and slant: the tone of the section below. */
		toTone?: SectionDividerTone;
		/** Rule and label: the space above and below. Wave and slant: the depth of the shape. */
		spacing?: SectionDividerSpacing;
	}

	let {
		variant = 'rule',
		label,
		decorative = false,
		fromTone = 'default',
		toTone = 'muted',
		spacing = 'md'
	}: Props = $props();

	const shape = $derived(variant === 'wave' || variant === 'slant');
	const labelled = $derived(variant === 'label' && !!label?.trim());
	/*
	 * A label with no spaces (an address, a file name) would wrap at a slash and leave a gap
	 * beside one rule; breaking anywhere lets its lines fill the box so both rules stay close.
	 */
	const unbroken = $derived(labelled && !/\s/.test(label!.trim()));

	/*
	 * Rule and label sit in the flow of a page, so spacing is margin: 32 px for a break inside a
	 * column, 48 to 64 px between groups, 64 to 96 px between topics (DESIGN §3.3 proximity).
	 */
	const margin: Record<SectionDividerSpacing, string> = {
		sm: 'my-8',
		md: 'my-12 sm:my-16',
		lg: 'my-16 sm:my-24'
	};

	/*
	 * A shape sits between two sections, which already bring their own padding, so spacing is
	 * the depth of the curve instead. It steps up with the width, so the angle stays low on a
	 * phone instead of a 360 px wedge.
	 */
	const depth: Record<SectionDividerSpacing, string> = {
		sm: 'h-6 sm:h-8 lg:h-10',
		md: 'h-8 sm:h-12 lg:h-16',
		lg: 'h-12 sm:h-16 lg:h-24'
	};

	const fill: Record<SectionDividerTone, string> = {
		default: 'fill-(--_surface)',
		muted: 'fill-(--_muted-surface)',
		inverse: 'fill-(--_inverse-surface)'
	};

	/*
	 * The upper region also strokes its own edge in its own tone, one screen pixel wide whatever
	 * the stretch, so where two solid tones meet it covers the pale anti-aliased seam. A
	 * transparent upper tone strokes nothing.
	 */
	const edge: Record<SectionDividerTone, string> = {
		default: 'stroke-(--_surface)',
		muted: 'stroke-(--_muted-surface)',
		inverse: 'stroke-(--_inverse-surface)'
	};

	/*
	 * Drawn in a 1440 × 64 box and stretched to the element, so the curve spans any width. The
	 * two regions share one edge exactly, each painted from its own token, so either can be
	 * transparent and let the page through. The edge stays at least 4 units clear of the top and
	 * bottom (1.5 px at the shallowest depth), so the one-pixel overlap with each neighbour only
	 * ever paints that neighbour's own tone. The wave is one long, uneven swell rather than a
	 * repeating sine: the lower tone rises toward the end of the line, and the slant is that rise
	 * as a straight edge.
	 */
	const paths = {
		wave: {
			from: 'M0 0H1440V8C1300 0 1280 0 960 32C640 64 320 64 0 48Z',
			to: 'M0 48C320 64 640 64 960 32C1280 0 1300 0 1440 8V64H0Z'
		},
		slant: {
			from: 'M0 0H1440V4L0 60Z',
			to: 'M0 60L1440 4V64H0Z'
		}
	};
	const d = $derived(paths[variant === 'slant' ? 'slant' : 'wave']);
</script>

{#if shape}
	<!--
		-my-px tucks the shape one pixel under each neighbour, so a fractional section height never
		opens a hairline crack. The SVG paints after both sections' backgrounds, and its edge rows
		are their own tones, so the overlap is invisible.
	-->
	<div class={['section-divider -my-px w-full', depth[spacing]]} aria-hidden="true">
		<svg
			class="block size-full rtl:-scale-x-100"
			viewBox="0 0 1440 64"
			preserveAspectRatio="none"
			focusable="false"
		>
			<path class={fill[toTone]} d={d.to} />
			<path
				class={[fill[fromTone], edge[fromTone]]}
				d={d.from}
				stroke-width="1"
				vector-effect="non-scaling-stroke"
			/>
		</svg>
	</div>
{:else if labelled}
	<!--
		The separator role makes its children presentational, so aria-label carries the text for
		assistive tech while the same words stay visible. The rules keep at least 32 px each, and
		the label wraps rather than squeezing them away. Pretty, not balanced, wrapping: a balanced
		label keeps its box at full width while its lines shrink, leaving a gap beside each rule.
	-->
	<div
		class={['section-divider flex items-center gap-4', margin[spacing]]}
		role={decorative ? undefined : 'separator'}
		aria-label={decorative ? undefined : label}
		aria-hidden={decorative ? 'true' : undefined}
	>
		<span class="h-px min-w-8 flex-1 bg-(--_hairline)"></span>
		<span
			class={[
				'max-w-lg min-w-0 text-center text-sm leading-5 font-medium text-pretty text-(--_muted)',
				unbroken ? 'break-all' : 'break-words'
			]}
		>
			{label}
		</span>
		<span class="h-px min-w-8 flex-1 bg-(--_hairline)"></span>
	</div>
{:else}
	<hr
		class={['section-divider border-0 border-t border-(--_hairline)', margin[spacing]]}
		aria-hidden={decorative ? 'true' : undefined}
	/>
{/if}

<style>
	/*
	 * Public tokens: set --section-divider-* on the divider or any ancestor. The three surfaces
	 * are the tones a shape joins; point them at your section colours (for section-wrapper-01,
	 * --section-divider-muted-surface: var(--section-wrapper-muted-surface) and so on).
	 */
	.section-divider {
		--_muted: var(--section-divider-muted, #52525b);
		--_hairline: var(--section-divider-hairline, rgb(0 0 0 / 0.1));
		--_surface: var(--section-divider-surface, transparent);
		--_muted-surface: var(--section-divider-muted-surface, #f4f4f5);
		--_inverse-surface: var(--section-divider-inverse-surface, #09090b);
	}
</style>
```

## Artifacts

### Neutral (`neutral`) (default)

- Artifact digest: `sha256-00a30fdb0cccb5b2914125ba63d475e0d37486a1214192f9e44c66da125470ed`
- Entry: `SectionDivider.svelte`
- Receipt: https://pagesugar.com/artifacts/cmp_section_divider_01/1.0.0/neutral/sha256-00a30fdb0cccb5b2914125ba63d475e0d37486a1214192f9e44c66da125470ed/manifest.json
- Bundle: https://pagesugar.com/artifacts/cmp_section_divider_01/1.0.0/neutral/sha256-00a30fdb0cccb5b2914125ba63d475e0d37486a1214192f9e44c66da125470ed/bundle.zip (5365 bytes, sha256 `4002766afb219186c9d0205f782f6a7bef49c1b7e661dfba6bdd651d1f346b5e`)

Files:

- `SectionDivider.svelte` (entry, 6063 bytes): https://pagesugar.com/artifacts/cmp_section_divider_01/1.0.0/neutral/sha256-00a30fdb0cccb5b2914125ba63d475e0d37486a1214192f9e44c66da125470ed/source/SectionDivider.svelte
