# Vertical stack

> A flex column that spaces its children from a six-step gap scale and aligns them on the cross axis. Replaces margins between siblings in card bodies, forms and lists, with list semantics kept on ul and ol.

- ID: `cmp_stack_layout_01`
- Slug: `stack-layout-01`
- Version: `1.0.0` (current)
- Status: published
- Published: 2026-10-01T05:22:14Z
- Updated: 2026-10-01
- Available versions: `1.0.0`
- Kind: control
- Primary category: `layout`
- Detail page: https://pagesugar.com/components/stack-layout-01?variant=default
- Preview: https://pagesugar.com/preview/stack-layout-01

## Variants

| Variant | Label | Default | Artifact digest |
| --- | --- | --- | --- |
| `default` | Default | yes | `sha256-9079eb6f97552dd2f97e46dfd0f358459352c76be37d54b51e2b1f532c453650` |

## 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/stack-layout-01`

## Dependencies

No third-party runtime packages.

## Services

No external services required.

## Usage

Pass children; each direct child is one row of the stack, and with as="ul" or as="ol" each must be an li. The stack paints nothing of its own: no surface, colour, divider or empty state. It never reverses or reorders children, and it does not wrap or lay out columns; use an inline cluster or a responsive grid for those.

Required props: `children`

```svelte
<script lang="ts">
	import Stack from '$lib/components/stack-layout-01/Stack.svelte';

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

<Stack gap="lg" class="max-w-md rounded-2xl bg-white p-6 ring-1 ring-black/8">
	<Stack gap="sm">
		<h2 class="text-lg font-semibold">Invite guests to Q4 roadmap</h2>
		<p class="text-sm text-zinc-600">Guests can comment on the timeline but can't move work.</p>
	</Stack>
	<Stack gap="sm">
		<label for="{id}-emails" class="text-sm font-medium">Email addresses</label>
		<input id="{id}-emails" class="h-10 rounded-lg border border-zinc-300 px-3 text-sm" />
	</Stack>
	<Stack gap="sm" align="start">
		<button type="button" class="h-10 rounded-lg bg-zinc-900 px-4 text-sm font-medium text-white">Send invites</button>
		<p class="text-xs text-zinc-500">Invite links expire after 14 days.</p>
	</Stack>
</Stack>
```

Limitations:

- No dividers between children; a divided list is the section divider pattern or a hairline list, a different component.
- No wrapping and no columns. A horizontal row that wraps is an inline cluster; columns are a responsive grid.
- No reverse direction or reordering: visual order always matches DOM order, so reading and tab order stay correct.
- The large and extra-large gaps step at the viewport's 640 px, not at the stack's own width, so a stack in a narrow sidebar on a wide screen uses the larger value. Pick a smaller preset there.
- To push a child to the end (a card footer), give the stack a height (h-full, or stretch it in a flex or grid parent) and put mt-auto on the child. With no extra height there is nothing to push into.
- ul and ol lose their markers, so an ordered list renders no numbers; put step numbers in the item markup. List padding and margin are reset by Tailwind's preflight; without preflight, add p-0 m-0 through class.
- An empty stack renders an empty element with no height. Render your own empty state instead when there are no children.
- With align set to start, center or end, each child is held to the stack's width but long unbroken text still needs break-words or overflow-wrap: anywhere on the child to wrap.

## Usage guide

### Vertical stack

A layout primitive: it spaces its children in one column and paints nothing itself. Use it
wherever you would otherwise put `mt-*` or `space-y-*` on a run of siblings.

#### The gap scale

| Preset | Gap                      | Typical use                                  |
| ------ | ------------------------ | -------------------------------------------- |
| `0`    | 0                        | Rows that draw their own boundaries          |
| `xs`   | 4 px                     | A value and its caption, tight totals        |
| `sm`   | 8 px                     | Label to field, heading to description       |
| `md`   | 16 px                    | Paragraphs, list items, fields in a dense UI |
| `lg`   | 24 px, 32 px from 640 px | Groups in a form or card, text to its action |
| `xl`   | 48 px, 64 px from 640 px | Whole groups in a section                    |

Each step is at least three times the step two below it, which is the proximity ratio that makes
a group read as a group. Nest them:

```svelte
<Stack gap="lg">
	<Stack gap="sm">
		<h2>Invite guests to Q4 roadmap</h2>
		<p>Guests can comment on the timeline but can't move work.</p>
	</Stack>
	<Stack gap="sm">
		<label for="emails">Email addresses</label>
		<input id="emails" />
	</Stack>
</Stack>
```

The two large steps change at the viewport's 640 px breakpoint. In a narrow sidebar on a wide
screen, pick one step smaller.

#### Alignment

`align="stretch"` (the default) makes every child as wide as the stack, which suits fields and
cards. A button or badge stretches too, so use `align="start"` on the stack that holds them.
In every alignment a child is held to the stack's width, so a long URL cannot push it out;
the text itself still needs `break-words` to wrap. That cap sits in the components layer, so a
`max-w-*` class on a child wins.

#### Pushing a child to the end

Give the stack a height and put `mt-auto` on the child:

```svelte
<Stack gap="md" class="h-full">
	<h3>Team</h3>
	<p>For teams up to 50.</p>
	<a class="mt-auto" href="/signup">Choose Team</a>
</Stack>
```

#### Lists

`as="ul"` and `as="ol"` drop the markers and add `role="list"`, because Safari stops announcing
a list once its markers are gone. An ordered list therefore shows no numbers: write them into
each `li`. Padding and margin on the list are reset by Tailwind's preflight.

#### Empty

An empty stack renders an element with no height. Render your own empty state instead.

## Props

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `gap` | '0' \| 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' | no | `'md'` | Space between children: 0, 4, 8 or 16 px; lg is 24 px stepping to 32 px from 640 px, xl 48 px stepping to 64 px. |
| `align` | 'stretch' \| 'start' \| 'center' \| 'end' | no | `'stretch'` | Cross-axis alignment. stretch makes every child the stack's width; start, center and end let children keep their own width, held to the stack's. |
| `as` | 'div' \| 'ul' \| 'ol' \| 'section' | no | `'div'` | Element to render. ul and ol drop their markers and add role="list" (not overridable) so Safari keeps list semantics; their children must be li. Name a section with aria-labelledby. |
| `class` | `ClassValue` | no |  | Extra classes on the stack, for margins, a width cap, padding or a height to push a child against. |
| `children` | `Snippet` | yes |  | The children. Each direct child is one row of the stack. |

## Customization

The stack has no colours or surfaces, so it declares no tokens. Change spacing and alignment through props; anything else (aria-label, id, data attributes) passes through to the element.

- Nest stacks to group by proximity: an outer lg stack between groups, an inner sm stack between a label and its field, or a heading and its description.
- Inside a card or panel, put the card's padding, radius and surface on the stack itself through class, so the card and its spacing are one element.
- Buttons and badges stretch to the full width under the default align. Use align="start" on the stack that holds them, or wrap them in a row of their own.
- Footer at the bottom of equal-height cards: give the stack h-full and the footer mt-auto.
- Other values: the GAP map at the top of the script is a plain lookup. Change a preset by replacing its complete class string, for example 'gap-5' for 20 px, and keep each step at least three times the step two below it.
- Lists: with as="ul" or as="ol", pass aria-label or aria-labelledby when the page holds more than one list.

No public CSS variables.

## Accessibility

- Children render in DOM order with no reverse or order utilities, so reading order, tab order and visual order match.
- as="ul" and as="ol" render with role="list", because Safari drops list semantics from a list whose list-style is none. Each child must be an li.
- as="section" is a landmark only when named: pass aria-labelledby pointing at the section's heading, or use a div.
- The stack is not interactive and adds no roles beyond the list. Headings, labels, focus styles and contrast inside it are yours.
- align="center" centres the children's boxes, not the text inside them; centre text only in short single columns such as an empty state, and set text-center on the children yourself.

## License

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

## Source

- Palette: Default (`default`)
- Entry: `Stack.svelte`
- Suggested directory: `src/lib/components/stack-layout-01`
- Files: 1
- Artifact digest: `sha256-9079eb6f97552dd2f97e46dfd0f358459352c76be37d54b51e2b1f532c453650`

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

#### `Stack.svelte`

Role: entry · 2567 bytes · SHA-256 `8071e0d66ff74404b160128157eeba36f90408c75d53b83b89a5776ae3576a16`

```svelte
<script lang="ts">
	import type { Snippet } from 'svelte';
	import type { ClassValue, HTMLAttributes } from 'svelte/elements';

	interface Props extends Omit<HTMLAttributes<HTMLElement>, 'class' | 'children'> {
		/** Space between children, from the proximity scale: 0, 4, 8, 16, 24 to 32, or 48 to 64 px. */
		gap?: '0' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
		/** Cross-axis alignment. `stretch` makes every child the stack's width. */
		align?: 'stretch' | 'start' | 'center' | 'end';
		/** Element to render. `ul` and `ol` drop their markers and keep list semantics. */
		as?: 'div' | 'ul' | 'ol' | 'section';
		/** Extra classes on the stack, for margins, a width cap or a height to push a child against. */
		class?: ClassValue;
		/** The children. Each direct child is one row of the stack. */
		children: Snippet;
	}

	let {
		gap = 'md',
		align = 'stretch',
		as = 'div',
		class: className,
		children,
		...rest
	}: Props = $props();

	/*
	 * The gap scale follows the proximity rule: each step is at least three times the step two
	 * below it, so a stack nested in a stack reads as a group. 4 and 8 px hold a label to its
	 * field or an eyebrow to its title; 16 px separates paragraphs and fields; 24 px parts a text
	 * block from its action; 48 px parts whole groups. The two larger steps open up from 640 px.
	 */
	const GAP = {
		'0': 'gap-0',
		xs: 'gap-1',
		sm: 'gap-2',
		md: 'gap-4',
		lg: 'gap-6 sm:gap-8',
		xl: 'gap-12 sm:gap-16'
	} as const;

	const ALIGN = {
		stretch: 'items-stretch',
		start: 'items-start',
		center: 'items-center',
		end: 'items-end'
	} as const;

	const list = $derived(as === 'ul' || as === 'ol');
</script>

<!--
	Flex gap rather than margins between siblings: a hidden or display: contents child leaves no
	phantom space, and nothing collapses. Children keep DOM order, so reading, tab and visual order
	match. Safari drops list semantics from a list without markers, so ul and ol carry role="list".
-->
<svelte:element
	this={as}
	{...rest}
	role={list ? 'list' : rest.role}
	class={['stack-layout flex flex-col', GAP[gap], ALIGN[align], list && 'list-none', className]}
>
	{@render children?.()}
</svelte:element>

<style>
	/*
	 * Children are your markup, so this sits in the components layer under :where(): any max-width
	 * utility you put on a child still wins. It keeps a start-, centre- or end-aligned child inside
	 * the stack when its content is wider than the column (a long URL, a wide table).
	 */
	@layer components {
		.stack-layout > :global(:where(*)) {
			max-inline-size: 100%;
		}
	}
</style>
```

## Artifacts

### Default (`default`) (default)

- Artifact digest: `sha256-9079eb6f97552dd2f97e46dfd0f358459352c76be37d54b51e2b1f532c453650`
- Entry: `Stack.svelte`
- Receipt: https://pagesugar.com/artifacts/cmp_stack_layout_01/1.0.0/default/sha256-9079eb6f97552dd2f97e46dfd0f358459352c76be37d54b51e2b1f532c453650/manifest.json
- Bundle: https://pagesugar.com/artifacts/cmp_stack_layout_01/1.0.0/default/sha256-9079eb6f97552dd2f97e46dfd0f358459352c76be37d54b51e2b1f532c453650/bundle.zip (4410 bytes, sha256 `035c4fff9a5ba987773254f616ea8ae5f9ab558fc78e191862349ad434b0baa1`)

Files:

- `Stack.svelte` (entry, 2567 bytes): https://pagesugar.com/artifacts/cmp_stack_layout_01/1.0.0/default/sha256-9079eb6f97552dd2f97e46dfd0f358459352c76be37d54b51e2b1f532c453650/source/Stack.svelte
