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.
cmp_stack_layout_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_stack_layout_01 version 1.0.0 with variant "default", 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
- Default
- Version
- 1.0.0
- Digest
Full digest
sha256-9079eb6f97552dd2f97e46dfd0f358459352c76be37d54b51e2b1f532c453650
<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>
Usage#
On this pagePass 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.
- Suggested location
src/lib/components/stack-layout-01- Required props
children
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 putmt-autoon 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-0m-0through 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.
Example
<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>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:
<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:
<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 and content inputs#
On this page| 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 | None | Extra classes on the stack, for margins, a width cap, padding or a height to push a child against. |
children | Snippet | Yes | None | The children. Each direct child is one row of the stack. |
Customization#
On this pageThe 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-fulland the footermt-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-labeloraria-labelledbywhen the page holds more than one list.
Accessibility#
On this page- 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-labelledbypointing 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.
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 · 1 October 2026
Only the current release is available. Keep downloaded source and its receipt if you need to use it again later.