Repository navigation
UI Components
@ultimat3/ui — SolidJS components on the semantic tokens in Theming. SCSS modules, logical properties, no Tailwind, no CSS-in-JS.
As of 2026-09. Stable API — semver from here (Upgrading).
packages/ui/CATALOG.md documents every component with every prop, every type, and the token scales, parsed straight out of packages/ui/src/components/*.tsx. It ships inside the npm tarball, so it is on disk at node_modules/@ultimat3/ui/CATALOG.md in any app.
How many there are is a question for the file, never for this page. It carried 52 for two majors:
head -7 node_modules/@ultimat3/ui/CATALOG.md # the count and every name, on one line| Concern | Answer |
|---|---|
| Regenerate | bun run --filter @ultimat3/ui catalog |
| Drift |
catalog.test.ts compares the committed file byte-for-byte against a fresh build; a prop change with no regen is a red x verify
|
| Hand edits | refused by the same test — the file carries a GENERATED banner |
This page does not restate the catalog. It covers the four page composites, because they are the ones an agent picks wrong.
Which component to reach for is not the same question as how it must behave. The four-state decision, skeleton-versus-spinner, response tiers, optimistic writes, forms, motion, focus, toasts and images are Interface rules — one page, every rule marked either with the guard, type or gate step that refuses it, or as judgement nothing checks.
A screen that hand-rolls a header grid is the bug these exist to prevent. All four hold no state — the route owns it.
The frame: skip link, landmarks, optional sidebar. Off-canvas navigation is Drawer, not a prop here.
| Prop | Type | Default |
|---|---|---|
children |
JSX.Element |
required |
header |
JSX.Element |
omitted → no <header>
|
sidebar |
JSX.Element |
omitted → no <nav>, and the grid collapses to one column |
footer |
JSX.Element |
omitted → no <footer>
|
sidebarLabel |
string |
t('ui.navigation') |
skipLabel |
string |
t('ui.skip') |
sidebarWidth |
string |
'16rem' |
stickyHeader |
boolean |
sticky unless explicitly false — undefined is sticky |
class |
string |
— |
Landmark order in the DOM: banner → navigation → main → contentinfo. <main> is unconditional.
The skip link is real, not decorative: <main> carries tabindex={-1} so focus actually moves there, and the link's href and the target's id are derived from one shellIds() call, so they cannot drift apart. It sits off-screen at translate: 0 -400% and slides in on :focus-visible.
| Prop | Type | Default |
|---|---|---|
title |
string |
required |
description |
string |
omitted |
actions |
JSX.Element |
omitted |
breadcrumbs |
readonly BreadcrumbItem[] |
omitted → no Breadcrumb
|
level |
1 | 2 | 3 | 4 | 5 | 6 |
1 |
media |
JSX.Element |
omitted |
class |
string |
— |
| Prop | Type | Default |
|---|---|---|
children |
JSX.Element |
required |
title |
string |
omitted → an unlabelled grouping, no aria-labelledby
|
description |
string |
omitted |
actions |
JSX.Element |
omitted |
level |
1 … 6
|
2 |
as |
'section' | 'article' | 'aside' |
'section' |
class |
string |
— |
A title wires aria-labelledby to the heading's generated id automatically. The head block disappears entirely when both title and actions are absent, so an untitled Section costs no markup.
A level outside 1–6 throws X_UI_INVALID_VALUE rather than emitting an <h7>.
| Prop | Type | Default |
|---|---|---|
children |
JSX.Element |
required |
actions |
JSX.Element |
omitted → no trailing group |
label |
string |
required |
surface |
boolean |
falsey |
class |
string |
— |
label is required because role="toolbar" with no accessible name is an unnamed group. Arrow keys move through the focusable children via a roving tabindex, horizontal, direction-aware, not looping.
Button has no href. A link that looks like a button is <Link appearance="button">; a pager on a server-rendered list is <Pagination hrefFor>. Neither needs an island.
<Link appearance="button" variant="secondary" size="sm" href="/posts/new">{t('posts.new')}</Link>
<Pagination
prevCursor={page.prevCursor}
nextCursor={page.nextCursor}
hrefFor={(cursor, direction) => `/posts?${direction === 'next' ? 'after' : 'before'}=${cursor}`}
/>| Component | Mode | Props | Renders |
|---|---|---|---|
Link |
text (default) |
underline, tone: 'accent' | 'inherit'
|
<a> with the link classes |
Link |
appearance="button" |
variant, tone (any Tone), size, fullWidth, iconStart, iconEnd
|
<a> with Button's classes — one set of button styles, two elements |
Pagination |
callbacks |
onCursor, or page + totalPages + onPage
|
two <button>s; needs an island |
Pagination |
links | hrefFor(cursor, direction) |
<a rel="prev"> / <a rel="next">; a side with no cursor is a disabled button |
DataTable |
callbacks / links |
onCursor, or hrefFor(cursor, direction)
|
its pager, in that mode — the prop is handed straight to Pagination
|
One mode per use, and the type holds it: hrefFor beside onCursor, onPage, page or totalPages does not compile, and neither does underline on a button-link or variant on a text link. Link mode pages by cursor only. Link also takes rel?: 'next' | 'prev', which joins the noopener noreferrer an external link always carries.
-
SCSS modules only.
Foo.tsx+Foo.module.scss, always paired. The only inlinestyleallowed is a CSS custom property. -
Logical properties.
margin-inline,inset-inline-start,text-align: start. Aleft/rightin a stylesheet is a bug. -
No hardcoded strings. Label props, or the built-in keys in
src/i18n-keys.ts→ I18n. -
No raw colours.
t.role('<role>')orvar(--color-*)→ Theming. -
No prop spreading. Components declare explicit props; there is no
{...rest}. -
solid-jsis a type-only import inside the package — reactive access goes through one registered adapter, so the design system does not pin a renderer version. - Formatting logic lives in a pure
*-view.tsbeside the component (money-view.ts,date-time-view.ts), testable with no renderer.
| Code | Means |
|---|---|
X_TOKEN_UNKNOWN |
a design token role that does not exist; cause lists the ones that do |
X_THEME_INVALID |
a theme that is not light or dark
|
X_UI_RUNTIME_MISSING |
a host capability the component needs is absent (IntersectionObserver, localStorage) |
X_UI_INVALID_VALUE |
a formatting component got an unrenderable value — NaN money, an invalid date, a heading level off the scale |
Full rows: Error codes.
Ultimate — v27.10.1 As of 2026-09. Stable API, semver from here. MIT licensed. What npm serves is npm view @ultimat3/core version, never this line.
This footer is the only page that stamps a version. It renders under every wiki page, so one release bumps one line; a stamp on a second page is 46 hand-copies of one fact, and every one of them goes stale on the next tag.
Repository · Issues · Changelog · llms.txt
Edits to these pages are synced from wiki/ in the repository — change the file there, not the wiki, or the next sync overwrites it.
Start
Tutorials
- 1 · First app
- 2 · First feature
- 3 · Auth and admin
- 4 · Jobs and realtime
- 5 · Deploy free
- 6 · Growing up
Primitives
- The eight primitives
- Building your own base
- Actions
- Entities and migrations
- Policies and authz
- Queries and live queries
- Client data
- Jobs and workflows
- Scheduled tasks
- Routes and render modes
Capabilities
- Realtime
- Caching and invalidation
- Batching and preloading
- N+1 detection
- PWA and offline
- Client navigation
- MCP and AI
- Agents
- Admin dashboard
- Scraping
- Auth
- Notify
- Storage and uploads
- Feature flags
- SEO
- Static assets
Cross-cutting
- I18n
- Theming
- UI components
- Interface rules
- Timezones and dates
- Money
- Resource management
- Migrations and backfills
- Testing
- CI: the gate across parallel jobs
Reference