Skip to content
Palette Default
Download ZIP

Default palette · 5.7 KB ZIP File receipt View as Markdown View code

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.
cmp_responsive_grid_01 · version 1.0.0 · Default palette
Using the PageSugar MCP server, fetch component cmp_responsive_grid_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-82abc83ced21ca39ab72dedcd7982c540a6bf8f474402f68d82ecb207da8e0c7
ResponsiveGrid.svelte Svelte · 5.5 KB Raw
<script lang="ts" module>
	export type ResponsiveGridColumns = {
		/** Columns below the first breakpoint. Default 1. */
		base?: 1 | 2;
		/** From 40rem (640 px) of viewport, or of container width in container mode. */
		sm?: 1 | 2 | 3;
		/** From 48rem (768 px). */
		md?: 2 | 3 | 4;
		/** From 64rem (1024 px). */
		lg?: 2 | 3 | 4 | 6;
	};
</script>

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

	interface Props extends Omit<HTMLAttributes<HTMLElement>, 'class' | 'children'> {
		/** `auto` fits as many columns as the minimum item width allows; `columns` sets counts per breakpoint. */
		mode?: 'auto' | 'columns';
		/** Auto mode: the narrowest an item may get before the grid drops a column. */
		minItemWidth?: '12rem' | '16rem' | '20rem' | '24rem';
		/** Auto mode: `fit` collapses unused columns so fewer items widen to fill the row; `fill` keeps them. */
		repeat?: 'fit' | 'fill';
		/** Columns mode: column count per breakpoint. */
		columns?: ResponsiveGridColumns;
		/** Space between items; each preset steps up once the grid has room. */
		gap?: 'sm' | 'md' | 'lg';
		/** Respond to the width of the grid's own box instead of the viewport. */
		container?: boolean;
		/** Line each item's first N children up with its neighbours on shared rows (CSS subgrid). */
		itemRows?: 2 | 3 | 4 | 5;
		/** `ul` for a list of items; children are then `li` elements. */
		as?: 'div' | 'ul';
		/** Extra classes on the outermost element (the query wrapper in container mode), for margins or a width cap. */
		class?: ClassValue;
		/** The items. Each direct child is one grid item. */
		children: Snippet;
	}

	let {
		mode = 'auto',
		minItemWidth = '16rem',
		repeat = 'fit',
		columns,
		gap = 'md',
		container = false,
		itemRows,
		as = 'div',
		class: className,
		children,
		...rest
	}: Props = $props();

	/*
	 * Every class below is a complete static string so Tailwind can see it. `min(100%, …)` lets
	 * the minimum give way inside anything narrower than itself, so 24rem never overflows 360 px.
	 */
	const AUTO = {
		fit: {
			'12rem': 'grid-cols-[repeat(auto-fit,minmax(min(100%,12rem),1fr))]',
			'16rem': 'grid-cols-[repeat(auto-fit,minmax(min(100%,16rem),1fr))]',
			'20rem': 'grid-cols-[repeat(auto-fit,minmax(min(100%,20rem),1fr))]',
			'24rem': 'grid-cols-[repeat(auto-fit,minmax(min(100%,24rem),1fr))]'
		},
		fill: {
			'12rem': 'grid-cols-[repeat(auto-fill,minmax(min(100%,12rem),1fr))]',
			'16rem': 'grid-cols-[repeat(auto-fill,minmax(min(100%,16rem),1fr))]',
			'20rem': 'grid-cols-[repeat(auto-fill,minmax(min(100%,20rem),1fr))]',
			'24rem': 'grid-cols-[repeat(auto-fill,minmax(min(100%,24rem),1fr))]'
		}
	} as const;

	const BASE = { 1: 'grid-cols-1', 2: 'grid-cols-2' } as const;

	/* Viewport breakpoints, and the same widths measured against the grid's container. */
	const VIEWPORT = {
		sm: { 1: 'sm:grid-cols-1', 2: 'sm:grid-cols-2', 3: 'sm:grid-cols-3' },
		md: { 2: 'md:grid-cols-2', 3: 'md:grid-cols-3', 4: 'md:grid-cols-4' },
		lg: { 2: 'lg:grid-cols-2', 3: 'lg:grid-cols-3', 4: 'lg:grid-cols-4', 6: 'lg:grid-cols-6' }
	} as const;

	const CONTAINER = {
		sm: {
			1: '@min-[40rem]:grid-cols-1',
			2: '@min-[40rem]:grid-cols-2',
			3: '@min-[40rem]:grid-cols-3'
		},
		md: {
			2: '@min-[48rem]:grid-cols-2',
			3: '@min-[48rem]:grid-cols-3',
			4: '@min-[48rem]:grid-cols-4'
		},
		lg: {
			2: '@min-[64rem]:grid-cols-2',
			3: '@min-[64rem]:grid-cols-3',
			4: '@min-[64rem]:grid-cols-4',
			6: '@min-[64rem]:grid-cols-6'
		}
	} as const;

	/* 12 → 16, 16 → 24 and 24 → 32 px: tight on phones and in sidebars, roomier with width. */
	const GAP = {
		viewport: { sm: 'gap-3 sm:gap-4', md: 'gap-4 sm:gap-6', lg: 'gap-6 sm:gap-8' },
		container: {
			sm: 'gap-3 @min-[40rem]:gap-4',
			md: 'gap-4 @min-[40rem]:gap-6',
			lg: 'gap-6 @min-[40rem]:gap-8'
		}
	} as const;

	/*
	 * Each item spans N rows of the parent and adopts them, so its parts share lines across a row.
	 * This owns the item's display: wrap an item that needs its own flex layout in a plain element.
	 */
	const ROWS = {
		2: '*:row-span-2 *:grid *:grid-rows-subgrid',
		3: '*:row-span-3 *:grid *:grid-rows-subgrid',
		4: '*:row-span-4 *:grid *:grid-rows-subgrid',
		5: '*:row-span-5 *:grid *:grid-rows-subgrid'
	} as const;

	const DEFAULT_COLUMNS: ResponsiveGridColumns = { base: 1, sm: 2, lg: 3 };

	const tracks = $derived.by(() => {
		if (mode === 'auto') return AUTO[repeat][minItemWidth];
		const cols = columns ?? DEFAULT_COLUMNS;
		const steps = container ? CONTAINER : VIEWPORT;
		return [
			BASE[cols.base ?? 1],
			cols.sm && steps.sm[cols.sm],
			cols.md && steps.md[cols.md],
			cols.lg && steps.lg[cols.lg]
		];
	});
</script>

{#snippet grid()}
	<!--
		Items flow in DOM order (no dense packing), so reading and tab order match what is seen.
		`min-w-0` stops a long URL or filename widening its column past the track.
	-->
	<svelte:element
		this={as}
		{...rest}
		role={as === 'ul' ? 'list' : rest.role}
		class={[
			'responsive-grid grid *:min-w-0',
			tracks,
			GAP[container ? 'container' : 'viewport'][gap],
			itemRows && ROWS[itemRows],
			!container && className
		]}
	>
		{@render children?.()}
	</svelte:element>
{/snippet}

{#if container}
	<!--
		An element cannot query its own width, so container mode measures this wrapper. The
		consumer's classes land here, so a width cap or flex-1 sizes what is measured.
	-->
	<div class={['responsive-grid-container @container', className]}>
		{@render grid()}
	</div>
{:else}
	{@render grid()}
{/if}

Pass items as children; each direct child is one grid item, and with as="ul" each must be an li. The grid paints nothing of its own: no card, colour, heading or empty state. It does no masonry, no mixed spans and no reordering; items always appear in source order.

Suggested location
src/lib/components/responsive-grid-01
Required props
children

Limitations

  • No masonry and no items spanning several columns; an asymmetric layout is a bento grid, a different component.
  • itemRows needs every item to have exactly that many direct children, in the same order. A subgrid cannot add rows, so an extra child overlaps the last row; group optional content inside an existing part.
  • With itemRows, the grid's gap also separates the rows inside an item unless the item sets its own row gap (gap-y-*), which the example does.
  • itemRows uses CSS subgrid (Chrome 117, Safari 16, Firefox 71). In older browsers each item still spans its rows as its own grid, so the layout holds but parts no longer line up across items.
  • Container mode wraps the grid in a div with container-type: inline-size and queries that div's width. The class prop lands on the wrapper in this mode, so a width cap or flex-1 there sizes what is measured; inside a flex row the wrapper has no intrinsic width until you give it one.
  • In auto mode with repeat="fit", columns that no item occupies collapse, so one or two items widen to fill the row. Once a row is full, a short last row keeps the column width. Use repeat="fill" to keep every item one column wide.
  • An empty grid renders an empty element. Render your own empty state instead of the grid when there are no items.
  • With itemRows, each direct child's display is set to grid, which overrides a flex or hidden class on it. Wrap an item that needs its own layout in a plain element, and filter hidden items out rather than hiding them.

Example

Svelte
<script lang="ts">
	import ResponsiveGrid from '$lib/components/responsive-grid-01/ResponsiveGrid.svelte';

	const templates = [
		{ title: 'Product roadmap', body: 'Quarters across the top, one lane per team.', meta: 'Timeline · 4 lanes' },
		{ title: 'Sprint board', body: 'To do, In progress, Review and Done.', meta: 'Board · 4 columns' },
		{ title: 'Launch plan', body: 'Every step from beta to general availability.', meta: 'Checklist · 18 tasks' }
	];
</script>

<ResponsiveGrid as="ul" minItemWidth="16rem" itemRows={3} aria-label="Templates">
	{#each templates as t (t.title)}
		<li class="gap-y-2 rounded-xl bg-white p-5 ring-1 ring-black/8">
			<h3 class="text-base font-semibold">{t.title}</h3>
			<p class="text-sm text-zinc-600">{t.body}</p>
			<p class="text-xs text-zinc-500">{t.meta}</p>
		</li>
	{/each}
</ResponsiveGrid>

Responsive grid#

A layout primitive: it places its children in columns and paints nothing itself.

Auto or columns#

  • Auto (mode="auto", the default) fits as many columns as minItemWidth allows. The minimum is wrapped in min(100%, …), so a 24rem minimum inside a 320 px column becomes one full-width column instead of overflowing.
  • Columns (mode="columns") takes counts per step: base, sm (640 px), md (768 px) and lg (1024 px). A step you leave out inherits the one below.

repeat="fit" collapses columns that no item occupies, so one or two items widen to fill the row. Once a row is full, a short last row keeps the column width. Use repeat="fill" when items should stay one column wide, for example a gallery that usually holds one or two items.

Container mode#

With container, breakpoints and gap steps are measured against the grid's own width, at the same 40, 48 and 64rem thresholds as the viewport steps. The grid is wrapped in a div with container-type: inline-size, because an element cannot query itself, and class lands on that wrapper, so max-w-md or flex-1 sizes the width being measured. Inside a flex row the wrapper has no intrinsic width until you give it one.

Aligned card parts#

Set itemRows to the number of parts each item has. Every item then spans that many rows as a subgrid, so the second part of each card starts on the same line as the second part of its neighbour, whatever the copy length above it:

Svelte
<ResponsiveGrid as="ul" itemRows={3} aria-label="Treatments">
	{#each treatments as t (t.title)}
		<li class="gap-y-2 rounded-xl p-5 ring-1 ring-black/8">
			<h3>{t.title}</h3>
			<p>{t.body}</p>
			<p class="self-end">{t.price}</p>
		</li>
	{/each}
</ResponsiveGrid>

Give each item its own gap-y-*; without it the rows inside an item are as far apart as the items themselves. Every item needs exactly itemRows direct children: a subgrid cannot add rows, so an extra child overlaps the last one. itemRows also sets each item's display to grid, so wrap an item that needs its own flex layout in a plain element.

Subgrid shipped in Chrome 117, Safari 16 and Firefox 71. Older browsers keep the layout but lose the shared lines.

Empty#

The grid renders an empty element when it has no children. Render your own empty state in its place.

Props and content inputs#

On this page
NameTypeRequiredDefaultDescription
mode'auto' | 'columns'No'auto'auto fits as many columns as minItemWidth allows; columns uses the counts in columns.
minItemWidth'12rem' | '16rem' | '20rem' | '24rem'No'16rem'Auto mode: the narrowest an item may get before the grid drops a column. Capped at the grid's width, so it never overflows.
repeat'fit' | 'fill'No'fit'Auto mode: fit collapses columns no item occupies, so a single item widens to fill the row; fill keeps them, so every item stays one column wide.
columns{ base?: 1 | 2; sm?: 1 | 2 | 3; md?: 2 | 3 | 4; lg?: 2 | 3 | 4 | 6 }No{ base: 1, sm: 2, lg: 3 }Columns mode: count per breakpoint (base, 640, 768 and 1024 px). Unset steps inherit the one below.
gap'sm' | 'md' | 'lg'No'md'Space between items: 12 to 16 px, 16 to 24 px or 24 to 32 px, stepping up from 640 px.
containerbooleanNofalseTake breakpoints and gap steps from the grid's own width (container queries) instead of the viewport, for sidebars and unknown columns.
itemRows2 | 3 | 4 | 5NoNoneEach item spans this many rows as a subgrid, so its first N children line up with the same parts of its neighbours. Omitted, items share a height only.
as'div' | 'ul'No'div'Element to render. ul adds role="list" (not overridable) so Safari keeps list semantics; its children must be li.
classClassValueNoNoneExtra classes on the outermost element: the grid, or the query wrapper in container mode, so a width cap limits what the container query measures.
childrenSnippetYesNoneThe items. Each direct child is one grid item.

Customization#

On this page

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

  • Pick auto mode when items should be about the same width wherever they land; pick columns mode when the design fixes a count per breakpoint.
  • Sidebars and unknown columns: set container so the column count follows the space the grid actually has.
  • Aligned cards: give each item the same N parts (media, title, text, footer) and set itemRows to N. Set the item's own gap-y-* for the spacing inside it.
  • Other widths: the class maps at the top of the script are plain lookups. Add a width by adding one complete class string, for example 'grid-cols-[repeat(auto-fit,minmax(min(100%,14rem),1fr))]'.
  • Labelling: with as="ul", pass aria-label or aria-labelledby so the list has a name.

Accessibility#

On this page
  • Items render in source order: no dense packing, no order or placement utilities. Reading order, tab order and visual order match.
  • as="ul" renders a list with role="list", because Safari drops list semantics from a ul whose list-style is none. Name it with aria-label or aria-labelledby when the page has more than one.
  • The grid is not interactive and adds no roles beyond the list. Headings, links and focus styles inside the items are the consumer's.
  • Items can shrink below their content width, so long words wrap only if the item's text allows it; add break-words or overflow-wrap: anywhere to titles that may hold filenames or URLs.
  • A div grid is a generic element: an aria-label on it names nothing. Add role="group" with the label when the items form a named group.

Known limitations

  • No keyboard grid navigation; items are reached with Tab in the usual way. A widget grid with arrow keys is a different pattern.

Release details#

On this page
Integration
  • Presentational
  • Works without client-side JavaScript
  • Server-side rendering supported
Dependencies
No additional runtime packages beyond Svelte and Tailwind CSS
License

MIT. Default license approval is pending; see the license status before adopting the source.

Version history
  • 1.0.0 (Published) Current release · 30 September 2026

Only the current release is available. Keep downloaded source and its receipt if you need to use it again later.