# Inline cluster

> A wrapping flex row for tags, buttons, links and meta items. Four gap presets tuned to what they hold, start/center/end/between justification and baseline alignment. No client JavaScript.

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

## Variants

| Variant | Label | Default | Artifact digest |
| --- | --- | --- | --- |
| `default` | Default | yes | `sha256-9195e1d1d9bd852471899ac0e0032af67d568114d763a3a5948a72e736abbd69` |

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

## Dependencies

No third-party runtime packages.

## Services

No external services required.

## Usage

Pass items as children; each direct child is one item, and with as="ul" each must be an li. The cluster paints nothing of its own: no colour, border, separator or empty state. It does no truncation, no overflow menu and no reordering; items always appear in source order.

Required props: `children`

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

	const labels = ['Guest access', 'Timeline', 'Permissions', 'Q4 2026'];
</script>

<Cluster as="ul" gap="sm" aria-label="Labels">
	{#each labels as label (label)}
		<li class="rounded-md bg-zinc-100 px-2.5 py-1 text-[13px] text-zinc-600">{label}</li>
	{/each}
</Cluster>

<Cluster gap="md" justify="end" class="mt-8">
	<button type="button" class="h-11 rounded-lg px-4 text-sm font-medium ring-1 ring-black/12">Back</button>
	<button type="submit" class="h-11 rounded-lg bg-zinc-900 px-4 text-sm font-medium text-white">Confirm booking</button>
</Cluster>
```

Limitations:

- justify="between" spreads every line, a short last one included. A line with one item starts at the start edge, so a heading and one link degrade to start when they wrap; with three or more items, two left on the last line sit at opposite edges. CSS cannot detect a wrapped line, so for longer rows keep start and give the item that should push away ms-auto.
- No separators. A dot or slash between items would be stranded at the start of a wrapped line; space between items does the separating instead.
- No truncation and no overflow menu. Every item is always shown; a row that must stay on one line is a different component.
- Each direct child gets min-width: 0 and max-width: 100%, so it can shrink to the line, and the cluster sets overflow-wrap: break-word, which its items inherit. That wraps a long word in plain text. Text inside a nested flex or grid item inside the child needs its own min-w-0, and an item with white-space: nowrap or a fixed width still overflows.
- An empty cluster renders an empty element. Render your own empty state instead of the cluster when there are no items.
- Gaps are fixed per preset and do not step up with the viewport. For another gap, add a preset to the GAP map in the script; a second gap class passed through class conflicts with the preset rather than replacing it.

## Usage guide

### Inline cluster

A layout primitive: it lays its children out in a row that wraps onto more lines when it runs
out of room, and paints nothing itself.

#### Picking a gap

Each preset is sized for what it usually holds, and the space between lines is never larger
than the space that separates items, so a wrapped cluster still reads as one set:

| Preset | Across | Down  | For                                  |
| ------ | ------ | ----- | ------------------------------------ |
| `xs`   | 4 px   | 4 px  | avatars, icon buttons, swatches      |
| `sm`   | 8 px   | 8 px  | tags, chips, filter pills            |
| `md`   | 12 px  | 12 px | buttons (the default)                |
| `lg`   | 24 px  | 12 px | text links, meta lines, price detail |

Text items need air between them on a line but only a line's worth between lines, which is why
`lg` is wider than it is tall.

#### Justify and wrapping

`start`, `center` and `end` behave the same on every line. `start` and `end` follow the page
direction, so a right-to-left page starts at the right edge.

`between` spreads each line to both edges, the last line included. A line with a single item
starts at the start edge, so a heading with one link beside it degrades cleanly: on a phone the
link drops below the heading and lines up with it. With three or more items, a short last line
spreads out awkwardly, and CSS has no way to tell a wrapped line from the first. Keep `start`
and push the last item over with `ms-auto` instead; it sits at the end edge of whichever line
it lands on:

```svelte
<Cluster gap="md">
	<button type="button">Back</button>
	<button type="button">Save draft</button>
	<button type="submit" class="ms-auto">Publish</button>
</Cluster>
```

#### Baseline

`align="baseline"` sets the first line of text in every item on one baseline. Use it when the
items are different sizes: a large price beside its unit, a heading beside a link, a status
beside a date. Give links vertical padding rather than a fixed height to reach a 44 px target;
padding keeps the baseline where the text is.

#### Long items

Every direct child can shrink to the width of the line, and the cluster sets
`overflow-wrap: break-word`, which its items inherit, so a filename or URL wraps inside its tag
instead of pushing past the edge at 360 px. Text that is itself a flex or grid item inside the
child (a name beside an icon, say) needs its own `min-w-0`, because `break-word` does not lower a
flex item's minimum width. An item with `white-space: nowrap` or a fixed width still overflows.

#### No separators

Dots or slashes between items end up at the start of a wrapped line. Let the gap separate
items; if a separator is essential, keep the row to one line in a different layout and mark the
separator `aria-hidden`.

#### Empty

An empty cluster renders an empty element. Render your own empty state in its place.

## Props

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `gap` | 'xs' \| 'sm' \| 'md' \| 'lg' | no | `'md'` | Space between items: 4 px (avatars, icon buttons), 8 px (tags, chips), 12 px (buttons), or 24 px across and 12 px down (text links and meta). |
| `justify` | 'start' \| 'center' \| 'end' \| 'between' | no | `'start'` | Where items sit along each line. start and end follow the page direction. between spreads each line to both edges; a line with one item starts at the start edge. |
| `align` | 'start' \| 'center' \| 'baseline' | no | `'center'` | How items of different heights line up within a line. baseline sets their first lines of text on one baseline. |
| `as` | 'div' \| 'ul' | no | `'div'` | Element to render. ul adds list-style: none and role="list" (not overridable) so Safari keeps list semantics; its children must be li. |
| `class` | `ClassValue` | no |  | Extra classes on the cluster element, for margins or a width cap. |
| `children` | `Snippet` | yes |  | The items. Each direct child is one item. |

## Customization

The cluster 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 element.

- Pick the gap by what the cluster holds: xs for avatars and icon buttons, sm for tags, md for buttons, lg for text links and meta.
- Mixed sizes on one line (a large price and its unit, a heading and a link): set align="baseline".
- A heading with one action pushed to the far edge: justify="between". With three or more items, keep start and give the item that should push away ms-auto.
- Other gaps: add a preset to the GAP map at the top of the script as one complete class string, for example xl: 'gap-x-8 gap-y-4', and add it to the gap prop's type. Passing a second gap class through class conflicts with the preset rather than replacing it.
- Labelling: with as="ul", pass aria-label or aria-labelledby so the list has a name.

No public CSS variables.

## Accessibility

- Items render in source order: no row-reverse and no order utilities, so reading order, tab order and visual order match. Under dir="rtl" the row mirrors on its own.
- 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 cluster is not interactive and adds no roles beyond the list. Buttons, links, focus styles and touch target sizes inside it are the consumer's; the md preset's 12 px gap keeps 44 px buttons from touching.
- A div cluster is a generic element: an aria-label on it names nothing. Add role="group" with the label when the items form a named group, such as a set of related buttons.

Known limitations:

- No keyboard navigation between items; they are reached with Tab in the usual way. A toolbar with arrow keys is a different pattern.

## License

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

## Source

- Palette: Default (`default`)
- Entry: `Cluster.svelte`
- Suggested directory: `src/lib/components/cluster-layout-01`
- Files: 1
- Artifact digest: `sha256-9195e1d1d9bd852471899ac0e0032af67d568114d763a3a5948a72e736abbd69`

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

#### `Cluster.svelte`

Role: entry · 2459 bytes · SHA-256 `7022cbbedcf8df0d8b0ba254b3d40b8934a551924ca53bbe102951d0dbce7921`

```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 items, sized for what the cluster holds. */
		gap?: 'xs' | 'sm' | 'md' | 'lg';
		/** Where items sit along the row. `between` spreads a short last line; see usage. */
		justify?: 'start' | 'center' | 'end' | 'between';
		/** How items of different heights line up within a line. */
		align?: 'start' | 'center' | 'baseline';
		/** `ul` for a list of items; children are then `li` elements. */
		as?: 'div' | 'ul';
		/** Extra classes on the cluster element, for margins or a width cap. */
		class?: ClassValue;
		/** The items. Each direct child is one item. */
		children: Snippet;
	}

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

	/*
	 * Each preset is tuned to what it usually holds, and the row gap never falls below what
	 * separates items within a line, so wrapped lines read as one set:
	 * xs 4 px for avatars and icon buttons, sm 8 px for tags and chips, md 12 px for buttons,
	 * lg 24 px across and 12 px down for text links and meta, which need air between items but
	 * only a line's worth between lines.
	 */
	const GAP = {
		xs: 'gap-1',
		sm: 'gap-2',
		md: 'gap-3',
		lg: 'gap-x-6 gap-y-3'
	} as const;

	/* Logical by nature: start is the right edge in a right-to-left page. */
	const JUSTIFY = {
		start: 'justify-start',
		center: 'justify-center',
		end: 'justify-end',
		between: 'justify-between'
	} as const;

	const ALIGN = {
		start: 'items-start',
		center: 'items-center',
		baseline: 'items-baseline'
	} as const;
</script>

<!--
	Items keep DOM order (no row-reverse, no order utilities), so reading, tab and visual order
	match, and right-to-left pages mirror the row on their own. `*:min-w-0 *:max-w-full` lets a
	long tag or URL shrink to the line instead of pushing past it; `break-words` then wraps it.
	`list-none` with `role="list"`: Safari drops list semantics from an unstyled ul otherwise.
-->
<svelte:element
	this={as}
	{...rest}
	role={as === 'ul' ? 'list' : rest.role}
	class={[
		'cluster-layout flex flex-wrap break-words *:max-w-full *:min-w-0',
		as === 'ul' && 'list-none',
		GAP[gap],
		JUSTIFY[justify],
		ALIGN[align],
		className
	]}
>
	{@render children?.()}
</svelte:element>
```

## Artifacts

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

- Artifact digest: `sha256-9195e1d1d9bd852471899ac0e0032af67d568114d763a3a5948a72e736abbd69`
- Entry: `Cluster.svelte`
- Receipt: https://pagesugar.com/artifacts/cmp_cluster_layout_01/1.0.0/default/sha256-9195e1d1d9bd852471899ac0e0032af67d568114d763a3a5948a72e736abbd69/manifest.json
- Bundle: https://pagesugar.com/artifacts/cmp_cluster_layout_01/1.0.0/default/sha256-9195e1d1d9bd852471899ac0e0032af67d568114d763a3a5948a72e736abbd69/bundle.zip (4288 bytes, sha256 `0413e3a12e07d5218180a945e746599e98992cf56e12a1fd0c23a76dee48731b`)

Files:

- `Cluster.svelte` (entry, 2459 bytes): https://pagesugar.com/artifacts/cmp_cluster_layout_01/1.0.0/default/sha256-9195e1d1d9bd852471899ac0e0032af67d568114d763a3a5948a72e736abbd69/source/Cluster.svelte
