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.
cmp_cluster_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_cluster_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-9195e1d1d9bd852471899ac0e0032af67d568114d763a3a5948a72e736abbd69
<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>
Usage#
On this pagePass 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.
- Suggested location
src/lib/components/cluster-layout-01- Required props
children
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: 0and 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 ownmin-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.
Example
<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>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:
<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 and content inputs#
On this page| 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 | None | Extra classes on the cluster element, for margins or a width cap. |
children | Snippet | Yes | None | The items. Each direct child is one item. |
Customization#
On this pageThe 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-labeloraria-labelledbyso the list has a name.
Accessibility#
On this page- 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-labeloraria-labelledbywhen 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-labelon 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.
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.