Skip to examples
Bento / Kitchen sink
Bento / compositions

Site pages

The public side: one landing page arranged three ways, and a coming-soon page. The words are placeholders and the same everywhere; what changes is the composition and the navigation. Each page is its own document, so it is shown in a frame.

Split hero, top bar

/ · SiteShell nav=bar · Hero split · FeatureGrid · CtaBand

The root page. A sticky bar with the site's links and both doors; the hero puts the words beside a picture of the product, drawn with its own components and hidden from assistive technology, since the words already say it.

Source src/lib/components/shells/site-shell/doc.ts · src/lib/components/shells/site-shell/SiteShell.svelte · src/lib/components/shells/site-shell/site-shell.module.css · src/lib/components/site/doc.ts · src/lib/components/site/hero/Hero.svelte · src/lib/components/site/feature-grid/FeatureGrid.svelte · src/lib/components/site/cta-band/CtaBand.svelte · src/lib/components/site/site-footer/SiteFooter.svelte · src/lib/components/site/backdrop/backdrop.module.css

src/lib/components/shells/site-shell/doc.ts

/**
 * SiteShell — the frame a public page sits in.
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § CONTRACT — the oracle. Names no library, contains no code.              │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * # Shape
 *
 *     links        REQUIRED, { href, label }[] — the site's sections
 *     current?     the href of the link where the reader is
 *     nav?         "bar" (default) | "floating" | "sidebar"
 *     actions?     the doors: sign in, sign up, the theme toggle
 *     brand?, renderLink?, footer?, children
 *
 * # Behaviour
 *
 * R1  Follows every shell rule: one main, a skip link first, navigation
 *     passed in and never lost at narrow widths.
 * R2  bar: across the top, sticky, the page scrolling under it. floating: a
 *     pill over the page's first section, which starts beneath it. sidebar:
 *     a sticky column with the doors at its foot.
 * R3  Narrow, every style becomes a bar with a Menu button; the links and
 *     the doors move into a drawer (from the left for sidebar), which
 *     closes when a link is followed.
 * R4  The current link is marked with aria-current, not colour alone.
 * R5  The footer, when given, is the page's contentinfo landmark.
 */
export {};

src/lib/components/shells/site-shell/SiteShell.svelte

<script lang="ts" module>
	export type SiteLink = { href: string; label: string };
</script>

<script lang="ts">
	import type { Snippet } from 'svelte';
	import { Mark } from '$lib/components/chrome/mark';
	import { Button } from '$lib/components/forms/button';
	import { NavLink } from '$lib/components/navigation/nav-link';
	import { Drawer, DrawerContent, DrawerTrigger } from '$lib/components/overlays/drawer';
	import { Menu } from '$lib/components/utility/icon';
	import { cn } from '$lib/utils/cn';
	import styles from './site-shell.module.css';

	/* The frame a public page sits in: a header with the site's links and its
	   doors (sign in, sign up), one main, and a footer. Owns the arrangement
	   only; the sections belong to the page. */
	let {
		links,
		current,
		nav = 'bar',
		actions,
		brand,
		footer,
		children,
		class: className = ''
	}: {
		/** The site's sections: a handful of links, as data. */
		links: readonly SiteLink[];
		/** Which link is where the reader is, by href. */
		current?: string;
		/** How the navigation sits: a bar across the top, a floating pill, or a
		 *  column down the side. All three move into a menu when narrow. */
		nav?: 'bar' | 'floating' | 'sidebar';
		/** The header's end: sign in, sign up, the theme toggle. */
		actions?: Snippet;
		/** Replaces the wordmark; wrap it in a link home. */
		brand?: Snippet;
		/** The page's footer, in the contentinfo landmark. */
		footer?: Snippet;
		children: Snippet;
		class?: string;
	} = $props();

	const id = $props.id();
	let open = $state(false);
</script>

{#snippet list(inMenu: boolean)}
	<ul class={styles.list} data-in={inMenu ? 'menu' : 'bar'}>
		{#each links as link (link.href)}
			<li>
				<NavLink
					href={link.href}
					active={link.href === current}
					onclick={inMenu ? () => (open = false) : undefined}>{link.label}</NavLink
				>
			</li>
		{/each}
	</ul>
{/snippet}

<div class={cn(styles.root, className)} data-nav={nav}>
	<a href="#{id}-main" class={styles.skip}>Skip to content</a>
	<header class={styles.header}>
		<div class={styles.bar}>
			<div class={styles.brand}>
				{#if brand}{@render brand()}{:else}<Mark />{/if}
			</div>
			<nav aria-label="Main" class={styles.links}>{@render list(false)}</nav>
			{#if actions}<div class={styles.actions}>{@render actions()}</div>{/if}
			<Drawer bind:open>
				<DrawerTrigger>
					{#snippet child({ props })}
						<Button {...props} variant="quiet" size="icon" aria-label="Menu" class={styles.menu}>
							<Menu aria-hidden="true" />
						</Button>
					{/snippet}
				</DrawerTrigger>
				<DrawerContent title="Menu" side={nav === 'sidebar' ? 'left' : 'right'} size="sm">
					<nav aria-label="Main">{@render list(true)}</nav>
					{#snippet footer()}
						{#if actions}<div class={styles.drawerActions}>{@render actions()}</div>{/if}
					{/snippet}
				</DrawerContent>
			</Drawer>
		</div>
	</header>
	<div class={styles.page}>
		<main id="{id}-main" tabindex="-1" class={styles.main}>{@render children()}</main>
		{#if footer}<footer class={styles.footer}>{@render footer()}</footer>{/if}
	</div>
</div>

src/lib/components/shells/site-shell/site-shell.module.css

@layer composition {
	.root {
		--site-header-h: calc(var(--control-md) + var(--space-5) * 2);
		--site-side-w: 15rem;
		display: grid;
		min-height: 100dvh;
		grid-template-rows: auto minmax(0, 1fr);
		background: var(--surface-ground);
		color: var(--ink);
		font-family: var(--font-sans);
		font-size: var(--text-body, var(--text-13));
	}
	.skip {
		position: absolute;
		z-index: var(--z-toast);
		top: var(--space-3);
		left: var(--space-3);
		padding: var(--space-3) var(--space-5);
		border-radius: var(--radius-2);
		background: var(--fill);
		color: var(--fill-ink);
		transform: translateY(-200%);
	}
	.skip:focus {
		transform: none;
	}

	/* ── The header: brand | links | actions, the menu button when narrow ── */
	.header {
		position: sticky;
		z-index: var(--z-sticky);
		top: 0;
	}
	.bar {
		display: flex;
		/* Lines up with a Container of width "content" below it. */
		width: min(100%, calc(var(--content-max) + 2 * var(--gutter)));
		min-height: var(--site-header-h);
		align-items: center;
		gap: var(--space-7);
		margin-inline: auto;
		padding: var(--space-5) var(--gutter);
	}
	.brand {
		display: flex;
		flex: none;
		align-items: center;
	}
	.brand a {
		color: inherit;
		text-decoration: none;
	}
	.links {
		min-width: 0;
		flex: 1;
	}
	.list {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: var(--space-2);
		margin: 0;
		padding: 0;
		list-style: none;
	}
	.list[data-in='menu'] {
		flex-direction: column;
		align-items: stretch;
	}
	.actions {
		display: flex;
		flex: none;
		align-items: center;
		gap: var(--space-3);
	}
	.menu {
		display: none;
		margin-left: auto;
	}
	.drawerActions {
		display: flex;
		flex-wrap: wrap;
		gap: var(--space-3);
	}
	.page {
		display: grid;
		min-width: 0;
		grid-template-rows: minmax(0, 1fr) auto;
	}
	.main {
		min-width: 0;
	}
	.main:focus {
		outline: none;
	}
	.footer {
		border-top: 1px solid var(--line);
	}

	/* Bar: across the top, over the page as it scrolls under. */
	.root[data-nav='bar'] .header {
		border-bottom: 1px solid var(--line);
		background: color-mix(in oklab, var(--surface-ground) 82%, transparent);
		backdrop-filter: blur(12px);
	}

	/* Floating: a pill that rides above the page. */
	.root[data-nav='floating'] .header {
		padding: var(--space-5) var(--gutter) 0;
		pointer-events: none;
	}
	.root[data-nav='floating'] .bar {
		width: min(100%, 60rem);
		min-height: auto;
		padding: var(--space-3) var(--space-3) var(--space-3) var(--space-6);
		border: 1px solid var(--line);
		border-radius: var(--radius-pill);
		background: color-mix(in oklab, var(--surface-panel) 86%, transparent);
		box-shadow: var(--shadow);
		backdrop-filter: blur(12px);
		pointer-events: auto;
	}
	.root[data-nav='floating'] .list {
		justify-content: center;
	}
	/* The pill overlaps the page's first section, which starts beneath it. */
	.root[data-nav='floating'] {
		--site-pill-h: calc(var(--control-md) + var(--space-3) * 2 + 2px);
	}
	.root[data-nav='floating'] .page {
		margin-top: calc(-1 * (var(--site-pill-h) + var(--space-5)));
	}

	/* Sidebar: a column down the side, with the doors at its foot. */
	@media (min-width: 52.001rem) {
		.root[data-nav='sidebar'] {
			grid-template-columns: var(--site-side-w) minmax(0, 1fr);
			grid-template-rows: none;
		}
		.root[data-nav='sidebar'] .header {
			height: 100dvh;
			border-right: 1px solid var(--line);
			background: var(--surface-rail, var(--surface-panel));
		}
		.root[data-nav='sidebar'] .bar {
			width: 100%;
			height: 100%;
			flex-direction: column;
			align-items: stretch;
			gap: var(--space-8);
			padding: var(--space-7) var(--space-5);
		}
		.root[data-nav='sidebar'] .brand {
			padding-inline: var(--space-4);
		}
		.root[data-nav='sidebar'] .list {
			flex-direction: column;
			align-items: stretch;
		}
		.root[data-nav='sidebar'] .actions {
			flex-direction: column;
			align-items: stretch;
		}
	}

	/* Narrow: every style becomes a bar with a menu button; the links and
     doors move into the menu. */
	@media (max-width: 52rem) {
		.root[data-nav='sidebar'] .header {
			border-bottom: 1px solid var(--line);
			background: var(--surface-ground);
		}
		.links,
		.actions {
			display: none;
		}
		.menu {
			display: inline-flex;
		}
	}
}

src/lib/components/site/doc.ts

/**
 * site — the sections a public page is built from.
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § CONTRACT — the oracle. Names no library, contains no code.              │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * # Members
 *
 *     Hero          title (the page's h1), eyebrow?, description?, actions?,
 *                   media?, layout "split" | "centered", backdrop?
 *     FeatureGrid   title (h2), eyebrow?, intro?, features { title,
 *                   description, icon? }[], columns 2 | 3, id?
 *     CtaBand       title (h2), description?, actions — on the gradient
 *     SiteFooter    brand, tagline?, columns { title, links }[], legal?
 *     backdrop      the preset's gradient mesh, as a class
 *
 * # Rules for every member
 *
 * R1  Each section is labelled by its own heading, and headings follow the
 *     outline: one h1 (the Hero's), h2 per section, h3 inside.
 * R2  Sections line up with the SiteShell's header: content width, the
 *     same gutter.
 * R3  The gradient is the preset's own (--gradient-1…3), mixed over the
 *     ground at --gradient-strength, which each mode sets so text on it
 *     keeps its contrast. It drifts only when asked and never under
 *     reduced motion.
 * R4  Columns collapse to one as the page narrows; nothing scrolls
 *     sideways.
 * R5  Sections own arrangement only; the words are the page's.
 */
export {};

src/lib/components/site/hero/Hero.svelte

<script lang="ts">
	import type { Snippet } from 'svelte';
	import { Container } from '$lib/components/layout/container';
	import { Heading } from '$lib/components/typography/heading';
	import { Text } from '$lib/components/typography/text';
	import { cn } from '$lib/utils/cn';
	import { backdrop } from '../backdrop';
	import styles from './hero.module.css';

	/* A page's opening section: the headline, what it is, and what to do next. */
	let {
		title,
		eyebrow,
		description,
		actions,
		media,
		layout = 'split',
		backdrop: washed = false,
		children,
		class: className = ''
	}: {
		/** The page's headline, rendered as its h1. */
		title: string;
		/** A short line above the title: a category, an announcement. */
		eyebrow?: Snippet;
		description?: string;
		/** The doors: sign up, sign in, a secondary link. */
		actions?: Snippet;
		/** Beside the text (split) or under it (centered): a product picture, a
		 *  form. Decorative pictures should be aria-hidden by the caller. */
		media?: Snippet;
		/** Text beside the media, or centred above it. Stacks when narrow. */
		layout?: 'split' | 'centered';
		/** Wash the section in the preset's gradient. */
		backdrop?: boolean;
		/** Under the actions: fine print, a sign-up form. */
		children?: Snippet;
		class?: string;
	} = $props();

	const id = $props.id();
</script>

<section
	aria-labelledby="{id}-title"
	data-layout={layout}
	data-strength="soft"
	class={cn(styles.root, washed && backdrop.mesh, className)}
>
	<Container width="content" class={styles.inner}>
		<div class={styles.copy}>
			{#if eyebrow}<div class={styles.eyebrow}>{@render eyebrow()}</div>{/if}
			<Heading level={1} size="display" id="{id}-title">{title}</Heading>
			{#if description}
				<Text size="lg" tone="muted" class={styles.description}>{description}</Text>
			{/if}
			{#if actions}<div class={styles.actions}>{@render actions()}</div>{/if}
			{@render children?.()}
		</div>
		{#if media}<div class={styles.media}>{@render media()}</div>{/if}
	</Container>
</section>

src/lib/components/site/feature-grid/FeatureGrid.svelte

<script lang="ts" module>
	import type { LayoutGrid } from '$lib/components/utility/icon';

	export type Feature = {
		title: string;
		description: string;
		icon?: typeof LayoutGrid;
	};
</script>

<script lang="ts">
	import type { Snippet } from 'svelte';
	import { Container } from '$lib/components/layout/container';
	import { Heading } from '$lib/components/typography/heading';
	import { Text } from '$lib/components/typography/text';
	import { cn } from '$lib/utils/cn';
	import styles from './feature-grid.module.css';

	/* What the product does, as a titled grid of short points. */
	let {
		title,
		eyebrow,
		intro,
		features,
		columns = 3,
		id,
		class: className = ''
	}: {
		/** The section's h2. */
		title: string;
		eyebrow?: Snippet;
		intro?: string;
		features: readonly Feature[];
		/** Most columns at full width; fewer as it narrows. */
		columns?: 2 | 3;
		/** For in-page links from the navigation. */
		id?: string;
		class?: string;
	} = $props();

	const heading = $props.id();
</script>

<section {id} aria-labelledby={heading} class={cn(styles.root, className)}>
	<Container width="content" class={styles.inner}>
		<div class={styles.head}>
			{#if eyebrow}<div class={styles.eyebrow}>{@render eyebrow()}</div>{/if}
			<Heading level={2} size="lg" id={heading}>{title}</Heading>
			{#if intro}<Text tone="muted">{intro}</Text>{/if}
		</div>
		<ul class={styles.grid} data-columns={columns}>
			{#each features as feature (feature.title)}
				<li class={styles.item}>
					{#if feature.icon}
						<span class={styles.icon}><feature.icon aria-hidden="true" /></span>
					{/if}
					<Heading level={3} size="sm">{feature.title}</Heading>
					<Text tone="muted">{feature.description}</Text>
				</li>
			{/each}
		</ul>
	</Container>
</section>

src/lib/components/site/cta-band/CtaBand.svelte

<script lang="ts">
	import type { Snippet } from 'svelte';
	import { Container } from '$lib/components/layout/container';
	import { Heading } from '$lib/components/typography/heading';
	import { Text } from '$lib/components/typography/text';
	import { cn } from '$lib/utils/cn';
	import { backdrop } from '../backdrop';
	import styles from './cta-band.module.css';

	/* The closing ask, on the preset's gradient: one line, one or two doors. */
	let {
		title,
		description,
		actions,
		class: className = ''
	}: {
		/** The section's h2. */
		title: string;
		description?: string;
		actions: Snippet;
		class?: string;
	} = $props();

	const id = $props.id();
</script>

<section aria-labelledby={id} class={cn(styles.root, className)}>
	<Container width="content">
		<div class={cn(styles.panel, backdrop.mesh)}>
			<div class={styles.copy}>
				<Heading level={2} size="lg" {id}>{title}</Heading>
				{#if description}<Text tone="muted">{description}</Text>{/if}
			</div>
			<div class={styles.actions}>{@render actions()}</div>
		</div>
	</Container>
</section>

src/lib/components/site/site-footer/SiteFooter.svelte

<script lang="ts" module>
	export type FooterLink = { href: string; label: string };
	export type FooterColumn = { title: string; links: readonly FooterLink[] };
</script>

<script lang="ts">
	import type { Snippet } from 'svelte';
	import { Container } from '$lib/components/layout/container';
	import { Text } from '$lib/components/typography/text';
	import { cn } from '$lib/utils/cn';
	import styles from './site-footer.module.css';

	/* The site's index: the brand, columns of links, and the fine print. Goes
	   in SiteShell's footer, which is the contentinfo landmark. */
	let {
		brand,
		tagline,
		columns,
		legal,
		class: className = ''
	}: {
		brand: Snippet;
		/** A line under the brand: what the product is. */
		tagline?: string;
		columns: readonly FooterColumn[];
		/** The bottom row: copyright, legal links. */
		legal?: Snippet;
		class?: string;
	} = $props();
</script>

<Container width="content" class={cn(styles.root, className)}>
	<div class={styles.top}>
		<div class={styles.brand}>
			{@render brand()}
			{#if tagline}<Text size="sm" tone="muted">{tagline}</Text>{/if}
		</div>
		<div class={styles.columns}>
			{#each columns as column (column.title)}
				<nav aria-label={column.title}>
					<p class={styles.title}>{column.title}</p>
					<ul class={styles.list}>
						{#each column.links as link (link.href + link.label)}
							<li>
								<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -- the caller resolves href -->
								<a href={link.href}>{link.label}</a>
							</li>
						{/each}
					</ul>
				</nav>
			{/each}
		</div>
	</div>
	{#if legal}<div class={styles.legal}>{@render legal()}</div>{/if}
</Container>

src/lib/components/site/backdrop/backdrop.module.css

@layer composition {
	/* The preset's gradient as a soft mesh behind content: three radial washes
     of --gradient-1…3 over the ground. Each is mixed at --gradient-strength,
     which each mode sets so text on top keeps its contrast. */
	.mesh {
		position: relative;
		isolation: isolate;
	}
	.mesh::before {
		position: absolute;
		z-index: -1;
		inset: 0;
		border-radius: inherit;
		background:
			radial-gradient(
				55% 65% at 12% 18%,
				color-mix(in oklab, var(--gradient-1) var(--gradient-strength), transparent),
				transparent 70%
			),
			radial-gradient(
				50% 60% at 88% 12%,
				color-mix(in oklab, var(--gradient-2) var(--gradient-strength), transparent),
				transparent 70%
			),
			radial-gradient(
				70% 70% at 55% 100%,
				color-mix(in oklab, var(--gradient-3) var(--gradient-strength), transparent),
				transparent 72%
			);
		background-size: 140% 140%;
		content: '';
		pointer-events: none;
	}
	/* Quieter: behind a hero that has text and a picture to carry. */
	.mesh[data-strength='soft']::before {
		opacity: 0.55;
	}
	/* Drifting: the washes wander slowly. Never under reduced motion. */
	@media (prefers-reduced-motion: no-preference) {
		.mesh[data-drift]::before {
			animation: drift 28s ease-in-out infinite alternate;
		}
	}
	@keyframes drift {
		from {
			background-position:
				0% 0%,
				100% 0%,
				50% 100%;
		}
		to {
			background-position:
				30% 40%,
				60% 30%,
				20% 60%;
		}
	}
}

Centred hero, floating nav

SiteShell nav=floating · email form · pricing · questions

The pill floats over the hero, which starts beneath it. The email form works without script: it opens sign-up with the address in the query.

Source src/lib/components/shells/site-shell/doc.ts · src/lib/components/shells/site-shell/SiteShell.svelte · src/lib/components/shells/site-shell/site-shell.module.css

src/lib/components/shells/site-shell/doc.ts

/**
 * SiteShell — the frame a public page sits in.
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § CONTRACT — the oracle. Names no library, contains no code.              │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * # Shape
 *
 *     links        REQUIRED, { href, label }[] — the site's sections
 *     current?     the href of the link where the reader is
 *     nav?         "bar" (default) | "floating" | "sidebar"
 *     actions?     the doors: sign in, sign up, the theme toggle
 *     brand?, renderLink?, footer?, children
 *
 * # Behaviour
 *
 * R1  Follows every shell rule: one main, a skip link first, navigation
 *     passed in and never lost at narrow widths.
 * R2  bar: across the top, sticky, the page scrolling under it. floating: a
 *     pill over the page's first section, which starts beneath it. sidebar:
 *     a sticky column with the doors at its foot.
 * R3  Narrow, every style becomes a bar with a Menu button; the links and
 *     the doors move into a drawer (from the left for sidebar), which
 *     closes when a link is followed.
 * R4  The current link is marked with aria-current, not colour alone.
 * R5  The footer, when given, is the page's contentinfo landmark.
 */
export {};

src/lib/components/shells/site-shell/SiteShell.svelte

<script lang="ts" module>
	export type SiteLink = { href: string; label: string };
</script>

<script lang="ts">
	import type { Snippet } from 'svelte';
	import { Mark } from '$lib/components/chrome/mark';
	import { Button } from '$lib/components/forms/button';
	import { NavLink } from '$lib/components/navigation/nav-link';
	import { Drawer, DrawerContent, DrawerTrigger } from '$lib/components/overlays/drawer';
	import { Menu } from '$lib/components/utility/icon';
	import { cn } from '$lib/utils/cn';
	import styles from './site-shell.module.css';

	/* The frame a public page sits in: a header with the site's links and its
	   doors (sign in, sign up), one main, and a footer. Owns the arrangement
	   only; the sections belong to the page. */
	let {
		links,
		current,
		nav = 'bar',
		actions,
		brand,
		footer,
		children,
		class: className = ''
	}: {
		/** The site's sections: a handful of links, as data. */
		links: readonly SiteLink[];
		/** Which link is where the reader is, by href. */
		current?: string;
		/** How the navigation sits: a bar across the top, a floating pill, or a
		 *  column down the side. All three move into a menu when narrow. */
		nav?: 'bar' | 'floating' | 'sidebar';
		/** The header's end: sign in, sign up, the theme toggle. */
		actions?: Snippet;
		/** Replaces the wordmark; wrap it in a link home. */
		brand?: Snippet;
		/** The page's footer, in the contentinfo landmark. */
		footer?: Snippet;
		children: Snippet;
		class?: string;
	} = $props();

	const id = $props.id();
	let open = $state(false);
</script>

{#snippet list(inMenu: boolean)}
	<ul class={styles.list} data-in={inMenu ? 'menu' : 'bar'}>
		{#each links as link (link.href)}
			<li>
				<NavLink
					href={link.href}
					active={link.href === current}
					onclick={inMenu ? () => (open = false) : undefined}>{link.label}</NavLink
				>
			</li>
		{/each}
	</ul>
{/snippet}

<div class={cn(styles.root, className)} data-nav={nav}>
	<a href="#{id}-main" class={styles.skip}>Skip to content</a>
	<header class={styles.header}>
		<div class={styles.bar}>
			<div class={styles.brand}>
				{#if brand}{@render brand()}{:else}<Mark />{/if}
			</div>
			<nav aria-label="Main" class={styles.links}>{@render list(false)}</nav>
			{#if actions}<div class={styles.actions}>{@render actions()}</div>{/if}
			<Drawer bind:open>
				<DrawerTrigger>
					{#snippet child({ props })}
						<Button {...props} variant="quiet" size="icon" aria-label="Menu" class={styles.menu}>
							<Menu aria-hidden="true" />
						</Button>
					{/snippet}
				</DrawerTrigger>
				<DrawerContent title="Menu" side={nav === 'sidebar' ? 'left' : 'right'} size="sm">
					<nav aria-label="Main">{@render list(true)}</nav>
					{#snippet footer()}
						{#if actions}<div class={styles.drawerActions}>{@render actions()}</div>{/if}
					{/snippet}
				</DrawerContent>
			</Drawer>
		</div>
	</header>
	<div class={styles.page}>
		<main id="{id}-main" tabindex="-1" class={styles.main}>{@render children()}</main>
		{#if footer}<footer class={styles.footer}>{@render footer()}</footer>{/if}
	</div>
</div>

src/lib/components/shells/site-shell/site-shell.module.css

@layer composition {
	.root {
		--site-header-h: calc(var(--control-md) + var(--space-5) * 2);
		--site-side-w: 15rem;
		display: grid;
		min-height: 100dvh;
		grid-template-rows: auto minmax(0, 1fr);
		background: var(--surface-ground);
		color: var(--ink);
		font-family: var(--font-sans);
		font-size: var(--text-body, var(--text-13));
	}
	.skip {
		position: absolute;
		z-index: var(--z-toast);
		top: var(--space-3);
		left: var(--space-3);
		padding: var(--space-3) var(--space-5);
		border-radius: var(--radius-2);
		background: var(--fill);
		color: var(--fill-ink);
		transform: translateY(-200%);
	}
	.skip:focus {
		transform: none;
	}

	/* ── The header: brand | links | actions, the menu button when narrow ── */
	.header {
		position: sticky;
		z-index: var(--z-sticky);
		top: 0;
	}
	.bar {
		display: flex;
		/* Lines up with a Container of width "content" below it. */
		width: min(100%, calc(var(--content-max) + 2 * var(--gutter)));
		min-height: var(--site-header-h);
		align-items: center;
		gap: var(--space-7);
		margin-inline: auto;
		padding: var(--space-5) var(--gutter);
	}
	.brand {
		display: flex;
		flex: none;
		align-items: center;
	}
	.brand a {
		color: inherit;
		text-decoration: none;
	}
	.links {
		min-width: 0;
		flex: 1;
	}
	.list {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: var(--space-2);
		margin: 0;
		padding: 0;
		list-style: none;
	}
	.list[data-in='menu'] {
		flex-direction: column;
		align-items: stretch;
	}
	.actions {
		display: flex;
		flex: none;
		align-items: center;
		gap: var(--space-3);
	}
	.menu {
		display: none;
		margin-left: auto;
	}
	.drawerActions {
		display: flex;
		flex-wrap: wrap;
		gap: var(--space-3);
	}
	.page {
		display: grid;
		min-width: 0;
		grid-template-rows: minmax(0, 1fr) auto;
	}
	.main {
		min-width: 0;
	}
	.main:focus {
		outline: none;
	}
	.footer {
		border-top: 1px solid var(--line);
	}

	/* Bar: across the top, over the page as it scrolls under. */
	.root[data-nav='bar'] .header {
		border-bottom: 1px solid var(--line);
		background: color-mix(in oklab, var(--surface-ground) 82%, transparent);
		backdrop-filter: blur(12px);
	}

	/* Floating: a pill that rides above the page. */
	.root[data-nav='floating'] .header {
		padding: var(--space-5) var(--gutter) 0;
		pointer-events: none;
	}
	.root[data-nav='floating'] .bar {
		width: min(100%, 60rem);
		min-height: auto;
		padding: var(--space-3) var(--space-3) var(--space-3) var(--space-6);
		border: 1px solid var(--line);
		border-radius: var(--radius-pill);
		background: color-mix(in oklab, var(--surface-panel) 86%, transparent);
		box-shadow: var(--shadow);
		backdrop-filter: blur(12px);
		pointer-events: auto;
	}
	.root[data-nav='floating'] .list {
		justify-content: center;
	}
	/* The pill overlaps the page's first section, which starts beneath it. */
	.root[data-nav='floating'] {
		--site-pill-h: calc(var(--control-md) + var(--space-3) * 2 + 2px);
	}
	.root[data-nav='floating'] .page {
		margin-top: calc(-1 * (var(--site-pill-h) + var(--space-5)));
	}

	/* Sidebar: a column down the side, with the doors at its foot. */
	@media (min-width: 52.001rem) {
		.root[data-nav='sidebar'] {
			grid-template-columns: var(--site-side-w) minmax(0, 1fr);
			grid-template-rows: none;
		}
		.root[data-nav='sidebar'] .header {
			height: 100dvh;
			border-right: 1px solid var(--line);
			background: var(--surface-rail, var(--surface-panel));
		}
		.root[data-nav='sidebar'] .bar {
			width: 100%;
			height: 100%;
			flex-direction: column;
			align-items: stretch;
			gap: var(--space-8);
			padding: var(--space-7) var(--space-5);
		}
		.root[data-nav='sidebar'] .brand {
			padding-inline: var(--space-4);
		}
		.root[data-nav='sidebar'] .list {
			flex-direction: column;
			align-items: stretch;
		}
		.root[data-nav='sidebar'] .actions {
			flex-direction: column;
			align-items: stretch;
		}
	}

	/* Narrow: every style becomes a bar with a menu button; the links and
     doors move into the menu. */
	@media (max-width: 52rem) {
		.root[data-nav='sidebar'] .header {
			border-bottom: 1px solid var(--line);
			background: var(--surface-ground);
		}
		.links,
		.actions {
			display: none;
		}
		.menu {
			display: inline-flex;
		}
	}
}

Editorial, side navigation

SiteShell nav=sidebar · sign-up card in the hero

Navigation never disappears; it moves. Narrow, all three become a bar with a Menu button that opens the links and the doors in a drawer.

Source src/lib/components/shells/site-shell/doc.ts · src/lib/components/shells/site-shell/SiteShell.svelte · src/lib/components/shells/site-shell/site-shell.module.css

src/lib/components/shells/site-shell/doc.ts

/**
 * SiteShell — the frame a public page sits in.
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § CONTRACT — the oracle. Names no library, contains no code.              │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * # Shape
 *
 *     links        REQUIRED, { href, label }[] — the site's sections
 *     current?     the href of the link where the reader is
 *     nav?         "bar" (default) | "floating" | "sidebar"
 *     actions?     the doors: sign in, sign up, the theme toggle
 *     brand?, renderLink?, footer?, children
 *
 * # Behaviour
 *
 * R1  Follows every shell rule: one main, a skip link first, navigation
 *     passed in and never lost at narrow widths.
 * R2  bar: across the top, sticky, the page scrolling under it. floating: a
 *     pill over the page's first section, which starts beneath it. sidebar:
 *     a sticky column with the doors at its foot.
 * R3  Narrow, every style becomes a bar with a Menu button; the links and
 *     the doors move into a drawer (from the left for sidebar), which
 *     closes when a link is followed.
 * R4  The current link is marked with aria-current, not colour alone.
 * R5  The footer, when given, is the page's contentinfo landmark.
 */
export {};

src/lib/components/shells/site-shell/SiteShell.svelte

<script lang="ts" module>
	export type SiteLink = { href: string; label: string };
</script>

<script lang="ts">
	import type { Snippet } from 'svelte';
	import { Mark } from '$lib/components/chrome/mark';
	import { Button } from '$lib/components/forms/button';
	import { NavLink } from '$lib/components/navigation/nav-link';
	import { Drawer, DrawerContent, DrawerTrigger } from '$lib/components/overlays/drawer';
	import { Menu } from '$lib/components/utility/icon';
	import { cn } from '$lib/utils/cn';
	import styles from './site-shell.module.css';

	/* The frame a public page sits in: a header with the site's links and its
	   doors (sign in, sign up), one main, and a footer. Owns the arrangement
	   only; the sections belong to the page. */
	let {
		links,
		current,
		nav = 'bar',
		actions,
		brand,
		footer,
		children,
		class: className = ''
	}: {
		/** The site's sections: a handful of links, as data. */
		links: readonly SiteLink[];
		/** Which link is where the reader is, by href. */
		current?: string;
		/** How the navigation sits: a bar across the top, a floating pill, or a
		 *  column down the side. All three move into a menu when narrow. */
		nav?: 'bar' | 'floating' | 'sidebar';
		/** The header's end: sign in, sign up, the theme toggle. */
		actions?: Snippet;
		/** Replaces the wordmark; wrap it in a link home. */
		brand?: Snippet;
		/** The page's footer, in the contentinfo landmark. */
		footer?: Snippet;
		children: Snippet;
		class?: string;
	} = $props();

	const id = $props.id();
	let open = $state(false);
</script>

{#snippet list(inMenu: boolean)}
	<ul class={styles.list} data-in={inMenu ? 'menu' : 'bar'}>
		{#each links as link (link.href)}
			<li>
				<NavLink
					href={link.href}
					active={link.href === current}
					onclick={inMenu ? () => (open = false) : undefined}>{link.label}</NavLink
				>
			</li>
		{/each}
	</ul>
{/snippet}

<div class={cn(styles.root, className)} data-nav={nav}>
	<a href="#{id}-main" class={styles.skip}>Skip to content</a>
	<header class={styles.header}>
		<div class={styles.bar}>
			<div class={styles.brand}>
				{#if brand}{@render brand()}{:else}<Mark />{/if}
			</div>
			<nav aria-label="Main" class={styles.links}>{@render list(false)}</nav>
			{#if actions}<div class={styles.actions}>{@render actions()}</div>{/if}
			<Drawer bind:open>
				<DrawerTrigger>
					{#snippet child({ props })}
						<Button {...props} variant="quiet" size="icon" aria-label="Menu" class={styles.menu}>
							<Menu aria-hidden="true" />
						</Button>
					{/snippet}
				</DrawerTrigger>
				<DrawerContent title="Menu" side={nav === 'sidebar' ? 'left' : 'right'} size="sm">
					<nav aria-label="Main">{@render list(true)}</nav>
					{#snippet footer()}
						{#if actions}<div class={styles.drawerActions}>{@render actions()}</div>{/if}
					{/snippet}
				</DrawerContent>
			</Drawer>
		</div>
	</header>
	<div class={styles.page}>
		<main id="{id}-main" tabindex="-1" class={styles.main}>{@render children()}</main>
		{#if footer}<footer class={styles.footer}>{@render footer()}</footer>{/if}
	</div>
</div>

src/lib/components/shells/site-shell/site-shell.module.css

@layer composition {
	.root {
		--site-header-h: calc(var(--control-md) + var(--space-5) * 2);
		--site-side-w: 15rem;
		display: grid;
		min-height: 100dvh;
		grid-template-rows: auto minmax(0, 1fr);
		background: var(--surface-ground);
		color: var(--ink);
		font-family: var(--font-sans);
		font-size: var(--text-body, var(--text-13));
	}
	.skip {
		position: absolute;
		z-index: var(--z-toast);
		top: var(--space-3);
		left: var(--space-3);
		padding: var(--space-3) var(--space-5);
		border-radius: var(--radius-2);
		background: var(--fill);
		color: var(--fill-ink);
		transform: translateY(-200%);
	}
	.skip:focus {
		transform: none;
	}

	/* ── The header: brand | links | actions, the menu button when narrow ── */
	.header {
		position: sticky;
		z-index: var(--z-sticky);
		top: 0;
	}
	.bar {
		display: flex;
		/* Lines up with a Container of width "content" below it. */
		width: min(100%, calc(var(--content-max) + 2 * var(--gutter)));
		min-height: var(--site-header-h);
		align-items: center;
		gap: var(--space-7);
		margin-inline: auto;
		padding: var(--space-5) var(--gutter);
	}
	.brand {
		display: flex;
		flex: none;
		align-items: center;
	}
	.brand a {
		color: inherit;
		text-decoration: none;
	}
	.links {
		min-width: 0;
		flex: 1;
	}
	.list {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: var(--space-2);
		margin: 0;
		padding: 0;
		list-style: none;
	}
	.list[data-in='menu'] {
		flex-direction: column;
		align-items: stretch;
	}
	.actions {
		display: flex;
		flex: none;
		align-items: center;
		gap: var(--space-3);
	}
	.menu {
		display: none;
		margin-left: auto;
	}
	.drawerActions {
		display: flex;
		flex-wrap: wrap;
		gap: var(--space-3);
	}
	.page {
		display: grid;
		min-width: 0;
		grid-template-rows: minmax(0, 1fr) auto;
	}
	.main {
		min-width: 0;
	}
	.main:focus {
		outline: none;
	}
	.footer {
		border-top: 1px solid var(--line);
	}

	/* Bar: across the top, over the page as it scrolls under. */
	.root[data-nav='bar'] .header {
		border-bottom: 1px solid var(--line);
		background: color-mix(in oklab, var(--surface-ground) 82%, transparent);
		backdrop-filter: blur(12px);
	}

	/* Floating: a pill that rides above the page. */
	.root[data-nav='floating'] .header {
		padding: var(--space-5) var(--gutter) 0;
		pointer-events: none;
	}
	.root[data-nav='floating'] .bar {
		width: min(100%, 60rem);
		min-height: auto;
		padding: var(--space-3) var(--space-3) var(--space-3) var(--space-6);
		border: 1px solid var(--line);
		border-radius: var(--radius-pill);
		background: color-mix(in oklab, var(--surface-panel) 86%, transparent);
		box-shadow: var(--shadow);
		backdrop-filter: blur(12px);
		pointer-events: auto;
	}
	.root[data-nav='floating'] .list {
		justify-content: center;
	}
	/* The pill overlaps the page's first section, which starts beneath it. */
	.root[data-nav='floating'] {
		--site-pill-h: calc(var(--control-md) + var(--space-3) * 2 + 2px);
	}
	.root[data-nav='floating'] .page {
		margin-top: calc(-1 * (var(--site-pill-h) + var(--space-5)));
	}

	/* Sidebar: a column down the side, with the doors at its foot. */
	@media (min-width: 52.001rem) {
		.root[data-nav='sidebar'] {
			grid-template-columns: var(--site-side-w) minmax(0, 1fr);
			grid-template-rows: none;
		}
		.root[data-nav='sidebar'] .header {
			height: 100dvh;
			border-right: 1px solid var(--line);
			background: var(--surface-rail, var(--surface-panel));
		}
		.root[data-nav='sidebar'] .bar {
			width: 100%;
			height: 100%;
			flex-direction: column;
			align-items: stretch;
			gap: var(--space-8);
			padding: var(--space-7) var(--space-5);
		}
		.root[data-nav='sidebar'] .brand {
			padding-inline: var(--space-4);
		}
		.root[data-nav='sidebar'] .list {
			flex-direction: column;
			align-items: stretch;
		}
		.root[data-nav='sidebar'] .actions {
			flex-direction: column;
			align-items: stretch;
		}
	}

	/* Narrow: every style becomes a bar with a menu button; the links and
     doors move into the menu. */
	@media (max-width: 52rem) {
		.root[data-nav='sidebar'] .header {
			border-bottom: 1px solid var(--line);
			background: var(--surface-ground);
		}
		.links,
		.actions {
			display: none;
		}
		.menu {
			display: inline-flex;
		}
	}
}

Sign up

AuthShell centred · AuthShell split with an aside

One form, two layouts. The split page puts the same form on the right half and the product on the left, on the preset's gradient. The left half is supplementary: it follows the form in reading order and is left out on narrow screens. Both read ?email= from the landing pages' forms.

Source src/lib/components/shells/auth-shell/doc.ts · src/lib/components/shells/auth-shell/AuthShell.svelte · src/lib/components/shells/auth-shell/auth-shell.module.css

src/lib/components/shells/auth-shell/doc.ts

/**
 * AuthShell — the frame every signed-out screen shares.
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § CONTRACT — the oracle. Names no library, contains no code.              │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * # Shape
 *
 *     title         REQUIRED, the page's h1
 *     description?  under the title
 *     actions?      beside the brand: theme, language
 *     brand?        replaces the wordmark
 *     footer?       under the card: the link to the other door
 *     layout?       "centered" (default) | "split"
 *     aside?        split only: the other half — the product, a quote
 *     children      the form
 *
 * # Behaviour
 *
 * R1  Centred in the viewport in one narrow column; on a short viewport it
 *     scrolls instead of clipping.
 * R2  The title is required and is the h1: the brand is not a heading.
 * R3  The shell is the main landmark. It owns arrangement only; the form,
 *     its state, and its errors belong to the page.
 * R4  Split puts the form on the right half, without a card, and the aside
 *     on the left, held in view while the form scrolls. The aside is
 *     supplementary: it follows the form in reading order, and below a
 *     tablet's width it is left out and the form takes the screen.
 */
export {};

src/lib/components/shells/auth-shell/AuthShell.svelte

<script lang="ts">
	import type { Snippet } from 'svelte';
	import { Mark } from '$lib/components/chrome/mark';
	import { Card } from '$lib/components/display/card';
	import { Container } from '$lib/components/layout/container';
	import { Heading } from '$lib/components/typography/heading';
	import { Text } from '$lib/components/typography/text';
	import { cn } from '$lib/utils/cn';
	import styles from './auth-shell.module.css';

	/* The frame every signed-out screen shares: centred, one narrow column,
	   one card. It owns the arrangement only — the form belongs to the page. */
	let {
		title,
		description,
		actions,
		brand,
		footer,
		layout = 'centered',
		aside,
		children,
		class: className = ''
	}: {
		/** Required, and rendered as the page's h1: the wordmark is not a
		 *  heading, and a reader needs something to orient on. */
		title: string;
		description?: string;
		/** Above the card, beside the brand: the theme toggle, a language menu. */
		actions?: Snippet;
		/** Replaces the wordmark; wrap it in a link home if there is one. */
		brand?: Snippet;
		/** Under the card: the link to the other door. */
		footer?: Snippet;
		/** One centred card (default), or the form on one half of the screen
		 *  and `aside` on the other. */
		layout?: 'centered' | 'split';
		/** The other half, in split: the product, a promise, a quote. It is
		 *  supplementary, so it follows the form in reading order and is left
		 *  out when the screen is narrow. */
		aside?: Snippet;
		children: Snippet;
		class?: string;
	} = $props();
</script>

{#snippet head()}
	<div class={styles.head}>
		<Heading level={1} size={layout === 'split' ? 'lg' : 'md'}>{title}</Heading>
		{#if description}<Text tone="muted">{description}</Text>{/if}
	</div>
{/snippet}

{#if layout === 'split'}
	<main class={cn(styles.root, styles.split, className)}>
		<div class={styles.pane}>
			<div class={styles.top}>
				{#if brand}{@render brand()}{:else}<Mark />{/if}
				{@render actions?.()}
			</div>
			<div class={styles.body}>
				{@render head()}
				{@render children()}
				{#if footer}<div class={styles.footer}>{@render footer()}</div>{/if}
			</div>
		</div>
		{#if aside}<aside class={styles.aside}>{@render aside()}</aside>{/if}
	</main>
{:else}
	<main class={cn(styles.root, className)}>
		<Container width="narrow">
			<div class={styles.column}>
				<div class={styles.top}>
					{#if brand}{@render brand()}{:else}<Mark />{/if}
					{@render actions?.()}
				</div>
				<Card as="div" elevation="raised" class={styles.card}>
					{@render head()}
					{@render children()}
				</Card>
				{#if footer}<div class={styles.footer}>{@render footer()}</div>{/if}
			</div>
		</Container>
	</main>
{/if}

src/lib/components/shells/auth-shell/auth-shell.module.css

@layer composition {
	.root {
		display: grid;
		min-height: 100dvh;
		align-content: center;
		padding-block: var(--space-9);
		background: var(--surface-ground);
		color: var(--ink);
		font-family: var(--font-sans);
		font-size: var(--text-body, var(--text-13));
	}
	.column {
		display: grid;
		gap: var(--space-7);
	}
	.top {
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: var(--space-5);
	}
	.card {
		display: grid;
		gap: var(--space-7);
		padding: var(--space-9);
	}
	.head {
		display: grid;
		gap: var(--space-3);
	}
	.footer {
		color: var(--ink-2);
		font-size: var(--text-body, var(--text-13));
		text-align: center;
	}

	/* ── Split: the form on one half, the aside on the other ── */
	.split {
		grid-template-areas: 'aside pane';
		grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
		align-content: stretch;
		padding-block: 0;
	}
	.pane {
		display: grid;
		min-width: 0;
		grid-area: pane;
		grid-template-rows: auto 1fr;
		padding: var(--space-7) var(--gutter);
	}
	.body {
		display: grid;
		width: min(100%, var(--narrow-max));
		align-content: center;
		gap: var(--space-7);
		margin-inline: auto;
		padding-block: var(--space-10);
	}
	.split .footer {
		text-align: start;
	}
	/* In reading order it comes after the form; on screen it is the left
     half, held in view while the form scrolls. */
	.aside {
		position: sticky;
		top: 0;
		height: 100dvh;
		min-width: 0;
		grid-area: aside;
		overflow: hidden;
		border-right: 1px solid var(--line);
	}
	@media (max-width: 56rem) {
		.split {
			grid-template-areas: 'pane';
			grid-template-columns: minmax(0, 1fr);
		}
		.aside {
			display: none;
		}
	}
}

Sign in

the same two layouts · the side panel says what is new

The side panel changes with the door. Sign-up says what you are signing up for; sign-in says what is new since you were last here. The two split pages link to each other, so a reader stays in one layout.

Source src/lib/components/shells/auth-shell/doc.ts · src/lib/components/shells/auth-shell/AuthShell.svelte · src/lib/components/shells/auth-shell/auth-shell.module.css

src/lib/components/shells/auth-shell/doc.ts

/**
 * AuthShell — the frame every signed-out screen shares.
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § CONTRACT — the oracle. Names no library, contains no code.              │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * # Shape
 *
 *     title         REQUIRED, the page's h1
 *     description?  under the title
 *     actions?      beside the brand: theme, language
 *     brand?        replaces the wordmark
 *     footer?       under the card: the link to the other door
 *     layout?       "centered" (default) | "split"
 *     aside?        split only: the other half — the product, a quote
 *     children      the form
 *
 * # Behaviour
 *
 * R1  Centred in the viewport in one narrow column; on a short viewport it
 *     scrolls instead of clipping.
 * R2  The title is required and is the h1: the brand is not a heading.
 * R3  The shell is the main landmark. It owns arrangement only; the form,
 *     its state, and its errors belong to the page.
 * R4  Split puts the form on the right half, without a card, and the aside
 *     on the left, held in view while the form scrolls. The aside is
 *     supplementary: it follows the form in reading order, and below a
 *     tablet's width it is left out and the form takes the screen.
 */
export {};

src/lib/components/shells/auth-shell/AuthShell.svelte

<script lang="ts">
	import type { Snippet } from 'svelte';
	import { Mark } from '$lib/components/chrome/mark';
	import { Card } from '$lib/components/display/card';
	import { Container } from '$lib/components/layout/container';
	import { Heading } from '$lib/components/typography/heading';
	import { Text } from '$lib/components/typography/text';
	import { cn } from '$lib/utils/cn';
	import styles from './auth-shell.module.css';

	/* The frame every signed-out screen shares: centred, one narrow column,
	   one card. It owns the arrangement only — the form belongs to the page. */
	let {
		title,
		description,
		actions,
		brand,
		footer,
		layout = 'centered',
		aside,
		children,
		class: className = ''
	}: {
		/** Required, and rendered as the page's h1: the wordmark is not a
		 *  heading, and a reader needs something to orient on. */
		title: string;
		description?: string;
		/** Above the card, beside the brand: the theme toggle, a language menu. */
		actions?: Snippet;
		/** Replaces the wordmark; wrap it in a link home if there is one. */
		brand?: Snippet;
		/** Under the card: the link to the other door. */
		footer?: Snippet;
		/** One centred card (default), or the form on one half of the screen
		 *  and `aside` on the other. */
		layout?: 'centered' | 'split';
		/** The other half, in split: the product, a promise, a quote. It is
		 *  supplementary, so it follows the form in reading order and is left
		 *  out when the screen is narrow. */
		aside?: Snippet;
		children: Snippet;
		class?: string;
	} = $props();
</script>

{#snippet head()}
	<div class={styles.head}>
		<Heading level={1} size={layout === 'split' ? 'lg' : 'md'}>{title}</Heading>
		{#if description}<Text tone="muted">{description}</Text>{/if}
	</div>
{/snippet}

{#if layout === 'split'}
	<main class={cn(styles.root, styles.split, className)}>
		<div class={styles.pane}>
			<div class={styles.top}>
				{#if brand}{@render brand()}{:else}<Mark />{/if}
				{@render actions?.()}
			</div>
			<div class={styles.body}>
				{@render head()}
				{@render children()}
				{#if footer}<div class={styles.footer}>{@render footer()}</div>{/if}
			</div>
		</div>
		{#if aside}<aside class={styles.aside}>{@render aside()}</aside>{/if}
	</main>
{:else}
	<main class={cn(styles.root, className)}>
		<Container width="narrow">
			<div class={styles.column}>
				<div class={styles.top}>
					{#if brand}{@render brand()}{:else}<Mark />{/if}
					{@render actions?.()}
				</div>
				<Card as="div" elevation="raised" class={styles.card}>
					{@render head()}
					{@render children()}
				</Card>
				{#if footer}<div class={styles.footer}>{@render footer()}</div>{/if}
			</div>
		</Container>
	</main>
{/if}

src/lib/components/shells/auth-shell/auth-shell.module.css

@layer composition {
	.root {
		display: grid;
		min-height: 100dvh;
		align-content: center;
		padding-block: var(--space-9);
		background: var(--surface-ground);
		color: var(--ink);
		font-family: var(--font-sans);
		font-size: var(--text-body, var(--text-13));
	}
	.column {
		display: grid;
		gap: var(--space-7);
	}
	.top {
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: var(--space-5);
	}
	.card {
		display: grid;
		gap: var(--space-7);
		padding: var(--space-9);
	}
	.head {
		display: grid;
		gap: var(--space-3);
	}
	.footer {
		color: var(--ink-2);
		font-size: var(--text-body, var(--text-13));
		text-align: center;
	}

	/* ── Split: the form on one half, the aside on the other ── */
	.split {
		grid-template-areas: 'aside pane';
		grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
		align-content: stretch;
		padding-block: 0;
	}
	.pane {
		display: grid;
		min-width: 0;
		grid-area: pane;
		grid-template-rows: auto 1fr;
		padding: var(--space-7) var(--gutter);
	}
	.body {
		display: grid;
		width: min(100%, var(--narrow-max));
		align-content: center;
		gap: var(--space-7);
		margin-inline: auto;
		padding-block: var(--space-10);
	}
	.split .footer {
		text-align: start;
	}
	/* In reading order it comes after the form; on screen it is the left
     half, held in view while the form scrolls. */
	.aside {
		position: sticky;
		top: 0;
		height: 100dvh;
		min-width: 0;
		grid-area: aside;
		overflow: hidden;
		border-right: 1px solid var(--line);
	}
	@media (max-width: 56rem) {
		.split {
			grid-template-areas: 'pane';
			grid-template-columns: minmax(0, 1fr);
		}
		.aside {
			display: none;
		}
	}
}

Coming soon

one page · six presets · --gradient-1…3

Each preset brings its own gradient. Three hues from its palette (--gradient-1 to --gradient-3) are mixed over the ground at --gradient-strength, which each mode sets, so the text keeps its contrast in light and dark. The mode follows the gallery's; the preset is fixed per frame with ?preset=. It drifts slowly, never under reduced motion.

Source src/lib/components/site/doc.ts · src/lib/components/site/hero/Hero.svelte · src/lib/components/site/feature-grid/FeatureGrid.svelte · src/lib/components/site/cta-band/CtaBand.svelte · src/lib/components/site/site-footer/SiteFooter.svelte · src/lib/components/site/backdrop/backdrop.module.css

src/lib/components/site/doc.ts

/**
 * site — the sections a public page is built from.
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § CONTRACT — the oracle. Names no library, contains no code.              │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * # Members
 *
 *     Hero          title (the page's h1), eyebrow?, description?, actions?,
 *                   media?, layout "split" | "centered", backdrop?
 *     FeatureGrid   title (h2), eyebrow?, intro?, features { title,
 *                   description, icon? }[], columns 2 | 3, id?
 *     CtaBand       title (h2), description?, actions — on the gradient
 *     SiteFooter    brand, tagline?, columns { title, links }[], legal?
 *     backdrop      the preset's gradient mesh, as a class
 *
 * # Rules for every member
 *
 * R1  Each section is labelled by its own heading, and headings follow the
 *     outline: one h1 (the Hero's), h2 per section, h3 inside.
 * R2  Sections line up with the SiteShell's header: content width, the
 *     same gutter.
 * R3  The gradient is the preset's own (--gradient-1…3), mixed over the
 *     ground at --gradient-strength, which each mode sets so text on it
 *     keeps its contrast. It drifts only when asked and never under
 *     reduced motion.
 * R4  Columns collapse to one as the page narrows; nothing scrolls
 *     sideways.
 * R5  Sections own arrangement only; the words are the page's.
 */
export {};

src/lib/components/site/hero/Hero.svelte

<script lang="ts">
	import type { Snippet } from 'svelte';
	import { Container } from '$lib/components/layout/container';
	import { Heading } from '$lib/components/typography/heading';
	import { Text } from '$lib/components/typography/text';
	import { cn } from '$lib/utils/cn';
	import { backdrop } from '../backdrop';
	import styles from './hero.module.css';

	/* A page's opening section: the headline, what it is, and what to do next. */
	let {
		title,
		eyebrow,
		description,
		actions,
		media,
		layout = 'split',
		backdrop: washed = false,
		children,
		class: className = ''
	}: {
		/** The page's headline, rendered as its h1. */
		title: string;
		/** A short line above the title: a category, an announcement. */
		eyebrow?: Snippet;
		description?: string;
		/** The doors: sign up, sign in, a secondary link. */
		actions?: Snippet;
		/** Beside the text (split) or under it (centered): a product picture, a
		 *  form. Decorative pictures should be aria-hidden by the caller. */
		media?: Snippet;
		/** Text beside the media, or centred above it. Stacks when narrow. */
		layout?: 'split' | 'centered';
		/** Wash the section in the preset's gradient. */
		backdrop?: boolean;
		/** Under the actions: fine print, a sign-up form. */
		children?: Snippet;
		class?: string;
	} = $props();

	const id = $props.id();
</script>

<section
	aria-labelledby="{id}-title"
	data-layout={layout}
	data-strength="soft"
	class={cn(styles.root, washed && backdrop.mesh, className)}
>
	<Container width="content" class={styles.inner}>
		<div class={styles.copy}>
			{#if eyebrow}<div class={styles.eyebrow}>{@render eyebrow()}</div>{/if}
			<Heading level={1} size="display" id="{id}-title">{title}</Heading>
			{#if description}
				<Text size="lg" tone="muted" class={styles.description}>{description}</Text>
			{/if}
			{#if actions}<div class={styles.actions}>{@render actions()}</div>{/if}
			{@render children?.()}
		</div>
		{#if media}<div class={styles.media}>{@render media()}</div>{/if}
	</Container>
</section>

src/lib/components/site/feature-grid/FeatureGrid.svelte

<script lang="ts" module>
	import type { LayoutGrid } from '$lib/components/utility/icon';

	export type Feature = {
		title: string;
		description: string;
		icon?: typeof LayoutGrid;
	};
</script>

<script lang="ts">
	import type { Snippet } from 'svelte';
	import { Container } from '$lib/components/layout/container';
	import { Heading } from '$lib/components/typography/heading';
	import { Text } from '$lib/components/typography/text';
	import { cn } from '$lib/utils/cn';
	import styles from './feature-grid.module.css';

	/* What the product does, as a titled grid of short points. */
	let {
		title,
		eyebrow,
		intro,
		features,
		columns = 3,
		id,
		class: className = ''
	}: {
		/** The section's h2. */
		title: string;
		eyebrow?: Snippet;
		intro?: string;
		features: readonly Feature[];
		/** Most columns at full width; fewer as it narrows. */
		columns?: 2 | 3;
		/** For in-page links from the navigation. */
		id?: string;
		class?: string;
	} = $props();

	const heading = $props.id();
</script>

<section {id} aria-labelledby={heading} class={cn(styles.root, className)}>
	<Container width="content" class={styles.inner}>
		<div class={styles.head}>
			{#if eyebrow}<div class={styles.eyebrow}>{@render eyebrow()}</div>{/if}
			<Heading level={2} size="lg" id={heading}>{title}</Heading>
			{#if intro}<Text tone="muted">{intro}</Text>{/if}
		</div>
		<ul class={styles.grid} data-columns={columns}>
			{#each features as feature (feature.title)}
				<li class={styles.item}>
					{#if feature.icon}
						<span class={styles.icon}><feature.icon aria-hidden="true" /></span>
					{/if}
					<Heading level={3} size="sm">{feature.title}</Heading>
					<Text tone="muted">{feature.description}</Text>
				</li>
			{/each}
		</ul>
	</Container>
</section>

src/lib/components/site/cta-band/CtaBand.svelte

<script lang="ts">
	import type { Snippet } from 'svelte';
	import { Container } from '$lib/components/layout/container';
	import { Heading } from '$lib/components/typography/heading';
	import { Text } from '$lib/components/typography/text';
	import { cn } from '$lib/utils/cn';
	import { backdrop } from '../backdrop';
	import styles from './cta-band.module.css';

	/* The closing ask, on the preset's gradient: one line, one or two doors. */
	let {
		title,
		description,
		actions,
		class: className = ''
	}: {
		/** The section's h2. */
		title: string;
		description?: string;
		actions: Snippet;
		class?: string;
	} = $props();

	const id = $props.id();
</script>

<section aria-labelledby={id} class={cn(styles.root, className)}>
	<Container width="content">
		<div class={cn(styles.panel, backdrop.mesh)}>
			<div class={styles.copy}>
				<Heading level={2} size="lg" {id}>{title}</Heading>
				{#if description}<Text tone="muted">{description}</Text>{/if}
			</div>
			<div class={styles.actions}>{@render actions()}</div>
		</div>
	</Container>
</section>

src/lib/components/site/site-footer/SiteFooter.svelte

<script lang="ts" module>
	export type FooterLink = { href: string; label: string };
	export type FooterColumn = { title: string; links: readonly FooterLink[] };
</script>

<script lang="ts">
	import type { Snippet } from 'svelte';
	import { Container } from '$lib/components/layout/container';
	import { Text } from '$lib/components/typography/text';
	import { cn } from '$lib/utils/cn';
	import styles from './site-footer.module.css';

	/* The site's index: the brand, columns of links, and the fine print. Goes
	   in SiteShell's footer, which is the contentinfo landmark. */
	let {
		brand,
		tagline,
		columns,
		legal,
		class: className = ''
	}: {
		brand: Snippet;
		/** A line under the brand: what the product is. */
		tagline?: string;
		columns: readonly FooterColumn[];
		/** The bottom row: copyright, legal links. */
		legal?: Snippet;
		class?: string;
	} = $props();
</script>

<Container width="content" class={cn(styles.root, className)}>
	<div class={styles.top}>
		<div class={styles.brand}>
			{@render brand()}
			{#if tagline}<Text size="sm" tone="muted">{tagline}</Text>{/if}
		</div>
		<div class={styles.columns}>
			{#each columns as column (column.title)}
				<nav aria-label={column.title}>
					<p class={styles.title}>{column.title}</p>
					<ul class={styles.list}>
						{#each column.links as link (link.href + link.label)}
							<li>
								<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -- the caller resolves href -->
								<a href={link.href}>{link.label}</a>
							</li>
						{/each}
					</ul>
				</nav>
			{/each}
		</div>
	</div>
	{#if legal}<div class={styles.legal}>{@render legal()}</div>{/if}
</Container>

src/lib/components/site/backdrop/backdrop.module.css

@layer composition {
	/* The preset's gradient as a soft mesh behind content: three radial washes
     of --gradient-1…3 over the ground. Each is mixed at --gradient-strength,
     which each mode sets so text on top keeps its contrast. */
	.mesh {
		position: relative;
		isolation: isolate;
	}
	.mesh::before {
		position: absolute;
		z-index: -1;
		inset: 0;
		border-radius: inherit;
		background:
			radial-gradient(
				55% 65% at 12% 18%,
				color-mix(in oklab, var(--gradient-1) var(--gradient-strength), transparent),
				transparent 70%
			),
			radial-gradient(
				50% 60% at 88% 12%,
				color-mix(in oklab, var(--gradient-2) var(--gradient-strength), transparent),
				transparent 70%
			),
			radial-gradient(
				70% 70% at 55% 100%,
				color-mix(in oklab, var(--gradient-3) var(--gradient-strength), transparent),
				transparent 72%
			);
		background-size: 140% 140%;
		content: '';
		pointer-events: none;
	}
	/* Quieter: behind a hero that has text and a picture to carry. */
	.mesh[data-strength='soft']::before {
		opacity: 0.55;
	}
	/* Drifting: the washes wander slowly. Never under reduced motion. */
	@media (prefers-reduced-motion: no-preference) {
		.mesh[data-drift]::before {
			animation: drift 28s ease-in-out infinite alternate;
		}
	}
	@keyframes drift {
		from {
			background-position:
				0% 0%,
				100% 0%,
				50% 100%;
		}
		to {
			background-position:
				30% 40%,
				60% 30%,
				20% 60%;
		}
	}
}