Adding a component

This page is for working on the library itself. If you only want to use a component in your own project, the installation page is what you want.

1. The component

Svelte 5 runes, a Props interface extending the matching HTML attributes, class merged last so callers can override, and ...rest spread onto the root element.

src/lib/components/Callout.svelte
<script lang="ts">
	import type { Snippet } from 'svelte';
	import type { HTMLAttributes } from 'svelte/elements';
	import { toneSoft, type Tone } from '../tones.js';
	import { useLocale } from '../locale.svelte.js';

	interface Props extends HTMLAttributes<HTMLDivElement> {
		tone?: Tone;
		children: Snippet;
	}

	let { tone = 'brand', class: className = '', children, ...rest }: Props = $props();

	const t = useLocale();
</script>

<div class="p-4 font-sans {toneSoft[tone]} {className}" {...rest}>
	{@render children()}
</div>

Take colours from the tone maps, never as raw hex. Take any user-visible string from useLocale() rather than writing it inline — that is what keeps the library translatable.

2. Export it

Put it under the right category comment — the site reads those comments to group the sidebar and the catalogue, so the placement is the categorisation.

src/lib/index.ts
/* --- feedback --- */
export { default as Callout } from './components/Callout.svelte';
export type { CalloutTone } from './components/Callout.svelte';

3. A demo

One file, named after the subpath. It is rendered live on the component's page and shown as the code sample, so the two can never drift apart. Import from $lib/components/…, not from the package.

src/site/demos/callout.svelte
<script lang="ts">
	import Callout from '$lib/components/Callout.svelte';
</script>

<Callout tone="accent">Something worth noticing.</Callout>

4. Regenerate

terminal
bun run registry     # picks up the new component
bun run exports      # adds @nqmcreative/ui/callout
bun run lint         # fails if either is stale
bun run build        # rebuilds dist/, which is committed

After that the component appears in the nav, the index, the ⌘K palette and the CLI without any further wiring. dist/ is committed, so rebuild and commit it with your change — bun cannot build the package at install time.

Add a one-line description in scripts/generate-registry.mjs; the whole catalogue is kept there so the wording can be read together.

Two traps that fail silently

Interpolated class names

Tailwind scans source text. A class it never sees written out is never generated — no error, just an unstyled element.

never do the first
<!-- Tailwind only sees literal class strings -->
<div class="bg-{tone}">…</div>          <!-- generates nothing -->
<div class={toneFill[tone]}>…</div>     <!-- correct -->

Two colours for one property

Which wins is decided by the order the rules appear in the stylesheet, not the order in your class attribute. Set the colour once per variant, or use a side-specific utility.

the fix is on the right
<!-- two colours for the same property: CSS order decides, not class order -->
<div class="border border-hairline border-brand">…</div>   <!-- unpredictable -->
<div class="border border-hairline border-l-brand">…</div> <!-- fine -->

The guards will catch some of this

bun run lint fails when the exports map or the registry is stale, so a component cannot ship without its subpath. It cannot catch a missing Tailwind class — only looking at the result does.
esc

layout & navigation

Accordion Wrapper that stacks collapsible items with hairlines between them.
AccordionItem A single collapsible row, built on <details> so it works without JS.
Breadcrumb Trail of links to the current page.
Divider Hairline rule, horizontal or vertical, with an optional caption.
Pagination Page numbers that collapse to an ellipsis around the current one.
Steps Progress through a flow, horizontal or vertical, with a failed state.
Tabs Underline, pill or segmented tabs bound to a value.

feedback

Alert Inline message in any tone, optionally dismissible.
EmptyState Placeholder for an empty list, with a glyph and an action slot.
Progress Determinate or indeterminate bar, in three sizes.
Skeleton Pulsing placeholder in text, block or circle form.
Spinner Rotating ring that inherits the surrounding colour.
Toaster Renders the toast queue. Mount once, near the root.

data display

Avatar Image or initials, circular or squared, in five sizes.
AvatarGroup Overlapping row of avatars with a ring cut out of the background.
Badge Pill label for status and counts. Solid, soft or outline.
Card Bordered, filled or tinted surface with an optional header and footer.
Kbd Keyboard key, styled with a thicker bottom edge.
Stat Figure with a label, delta and trend arrow.
Table Data-driven table with sorting, row selection and a cell snippet.

actions

Button Five variants and four sizes, with a loading state and an href mode.
Link Anchor in any tone, with an external variant.

forms

Calendar Month grid with keyboard navigation, bounds and disabled days.
Checkbox Single checkbox with a label, description and indeterminate state.
CheckboxGroup A set of checkboxes bound to a string array, with an optional cap.
Combobox Single-select with a text filter and keyboard navigation.
DatePicker Text field plus a calendar popover. Value is a YYYY-MM-DD string.
Dropzone Drag-and-drop file input with type, size and count validation.
Field Wraps any control with a label, hint and error message.
Input Text field with prefix and suffix slots, sizes and an invalid state.
InputGroup Joins an input to a button or addon with a single shared border.
Label Form label with an optional required marker.
MultiSelect Multi-select with chips, grouping and a selection cap.
NumberInput Number field with steppers, clamping and decimal handling.
PasswordInput Password field with a visibility toggle and a strength meter.
Radio Single radio bound to a shared group value.
RadioGroup A set of radios, optionally rendered as selectable cards.
SegmentedControl Exclusive choice as one joined row, with arrow-key movement.
Select Native select with a chevron and data-driven options.
Slider Range input with ticks, a value bubble and dual-handle mode.
Switch On/off toggle with a label and description.
Textarea Multi-line field that can grow with its content.

overlay

CommandPalette Cmd+K launcher with grouping, shortcuts and hidden keywords.
ConfirmDialog Small dialog that stays busy until an async confirm settles.
ContextMenu Right-click menu placed at the pointer, flipped near the edges.
Drawer Panel that slides in from any edge, on the native dialog top layer.
Dropdown Menu anchored to a trigger, with focus trap and viewport flipping.
MenuItem Row inside a Dropdown or ContextMenu, with a shortcut hint.
MenuSeparator Rule between menu groups, with an optional caption.
Modal Centred dialog on the native top layer, with a footer slot.
Popover Anchored panel that opens on click or hover.
ThemeToggle Light, dark and system, remembered in localStorage.
Tooltip CSS-only label on hover and focus, in four placements.

app shell

Footer Site footer with link columns and a bottom bar.
Navbar Top bar that collapses into a drawer below the md breakpoint.
Sidebar Nested, collapsible navigation rail for an app shell.

locale

LocaleProvider Scopes a locale to a subtree — the SSR-safe way to translate.