The family’s one colourless stoneA design system that is generated
Every design system asks you to assemble it: pick the palette, hand-tune dark mode, hope the contrast holds. UI-Kit takes one brand colour and emits the rest. A contrast-verified theme in both modes, and fifty components that already speak it. Try it below; the theme builds in your browser, and this page is wearing it.
Try it
Press a colour. The whole page is rebuilt.
Not a preview pane. The page. Every component, both modes, all eleven ramp steps and every contrast pair are regenerated in the browser, because the generator has no dependencies and no build step to hide behind.
success-9.9° from canonicalwarning-2.5° from canonicaldanger+4.8° from canonicalinfo+10.9° from canonical
| Role | Light | Dark | Lc | Target |
|---|---|---|---|---|
fg-default | #1d1915 | #f6f3f1 | 104.4 | 90 |
fg-muted | #5c5753 | #d1cdca | 84.8 | 75 |
fg-subtle | #857f7a | #b8b3b0 | 67.0 | 60 |
fg-on-accent | #fefdfd | #fefdfd | 96.8 | 75 |
fg-accent | #5f5a55 | #d1cdca | 83.7 | 75 |
fg-on-danger | #fffdfc | #fffdfc | 75.5 | 75 |
fg-on-warning | #fffdfb | #fffdfb | 75.5 | 75 |
border-strong | #afa8a2 | #9d9792 | 46.4 | 45 |
Typography
One word about voice, the way one colour is the palette.
Leading and tracking are not a table you pick from. They are functions of size: large type carries its own separation, small type needs more room, and a face is spaced for one size and wrong at every other. Choose a voice and watch every number below change.
A serif for the headings and a sans for the text. The oldest pairing there is, and it still works because the contrast is structural rather than decorative.
The layout does not move when the font arrives
A web face swapping in reflows the text and shoves everything below it, one of the three things Core Web Vitals measures. Every face here gets a @font-face that reshapes the local fallback until it occupies the same room. It downloads nothing: local() is a handle on a font the machine already has.
Those rules are on this document right now, generated for the voice you picked and replaced when you pick another, which is why changing voice re-types the whole page without the sections below jumping.
Same shape as the contrast pass above: a measurable failure, corrected, and the correction reported rather than assumed.
The catalogue
50 components. Here are eight.
Everything ships, plus the contracts. Each specimen below is live and each Code tab shows the file that rendered it. Read from disk at build time, so the sample and the demo cannot disagree.
Button
A control that performs an action when activated.
import { Button } from '@quxkit/ui-kit-registry'
export default function ButtonDemo() {
return (
<div className="demo-row">
<Button>Save changes</Button>
<Button variant="soft">Duplicate</Button>
<Button variant="outline">Export</Button>
<Button variant="ghost">Cancel</Button>
<Button tone="danger">Delete</Button>
<Button loading>Publishing</Button>
<Button disabled>Unavailable</Button>
</div>
)
}
Field, Select, Switch, Checkbox
Every control that takes a label routes its label, description and error through one shared shell, so none of them can be accessible while another is not.
We only use this for billing.
import { Checkbox, Field, Select, Switch } from '@quxkit/ui-kit-registry'
export default function FieldDemo() {
return (
<div className="demo-stack">
<Field
label="Work email"
type="email"
description="We only use this for billing."
defaultValue="[email protected]"
/>
<Select
label="Plan"
placeholder="Choose a plan"
items={[
{ value: 'free', label: 'Free' },
{ value: 'pro', label: 'Pro' },
{ value: 'scale', label: 'Scale' },
]}
/>
<Switch label="Two-factor authentication" defaultChecked />
<Checkbox label="Email me about outages" defaultChecked />
</div>
)
}
Alert
A message about the state of the system or the result of an action.
import { Alert, Button } from '@quxkit/ui-kit-registry'
export default function AlertDemo() {
return (
<div className="demo-stack">
<Alert
tone="danger"
title="Upload failed"
actions={
<Button size="sm" tone="danger" variant="outline">
Retry
</Button>
}
>
The file is larger than the 25 MB limit.
</Alert>
<Alert tone="success" title="Deployment complete">
Version 4.2.0 is live on production.
</Alert>
<Alert tone="info" variant="outline" title="Scheduled maintenance">
The API will be read-only on Sunday from 02:00 to 04:00 UTC.
</Alert>
</div>
)
}
Table
Rows of records with columns that can be sorted and selected.
ScopeSorting is reported and requested, not performed — you sort the data. No virtualisation, column resizing, or pinned columns.
| Customer | |||
|---|---|---|---|
| INV-0041 | Acme Corp | $4,210.00 | |
| INV-0040 | Globex | $980.00 | |
| INV-0039 | Initech | $12,400.00 |
import { Table, type SortState } from '@quxkit/ui-kit-registry'
import { useState } from 'react'
const INVOICES = [
{ id: '1', number: 'INV-0041', customer: 'Acme Corp', amount: '$4,210.00' },
{ id: '2', number: 'INV-0040', customer: 'Globex', amount: '$980.00' },
{ id: '3', number: 'INV-0039', customer: 'Initech', amount: '$12,400.00' },
]
export default function TableDemo() {
const [selected, setSelected] = useState<string[]>(['2'])
const [sort, setSort] = useState<SortState>({ column: 'amount', direction: 'descending' })
return (
<Table
caption="Invoices from the last 30 days"
columns={[
{ key: 'number', header: 'Invoice', sortable: true },
{ key: 'customer', header: 'Customer' },
{ key: 'amount', header: 'Amount', align: 'end', sortable: true },
]}
rows={INVOICES}
getRowId={(row) => row.id}
sort={sort}
onSortChange={setSort}
selected={selected}
onSelectionChange={setSelected}
/>
)
}
Modal, Dropdown, Toast
Overlays are native <dialog> elements, so the focus trap, the inert background and Escape come from the platform rather than from a hook.
import { Button, Dropdown, Modal, useToast } from '@quxkit/ui-kit-registry'
import { useState } from 'react'
export default function OverlayDemo() {
const toast = useToast()
const [open, setOpen] = useState(false)
return (
<div className="demo-row">
<Button onClick={() => setOpen(true)}>Delete project</Button>
<Dropdown
trigger={
<Button variant="outline" tone="neutral">
Actions
</Button>
}
items={[
{ label: 'Rename', onSelect: () => toast.show({ tone: 'success', title: 'Renamed' }), shortcut: 'R' },
{ label: 'Duplicate', onSelect: () => toast.show({ tone: 'success', title: 'Duplicated' }) },
{ separator: true },
{ label: 'Delete project', onSelect: () => setOpen(true), tone: 'danger' },
]}
/>
<Modal
open={open}
onClose={() => setOpen(false)}
title="Delete this project?"
description="Its 1,204 documents will be permanently removed."
footer={
<>
<Button variant="ghost" onClick={() => setOpen(false)}>
Keep project
</Button>
<Button tone="danger" onClick={() => setOpen(false)}>
Delete project
</Button>
</>
}
>
<p style={{ margin: 0 }}>This cannot be undone.</p>
</Modal>
</div>
)
}
Tabs
Alternative views of the same region, one shown at a time.
4,210 of 10,000 credits used this month.
import { Tabs } from '@quxkit/ui-kit-registry'
export default function TabsDemo() {
return (
<Tabs
items={[
{
value: 'overview',
label: 'Overview',
content: <p className="demo-body">4,210 of 10,000 credits used this month.</p>,
},
{
value: 'usage',
label: 'Usage',
content: <p className="demo-body">Peak was Tuesday, at 612 credits.</p>,
},
{
value: 'billing',
label: 'Billing',
content: <p className="demo-body">Next invoice on 1 September.</p>,
},
]}
/>
)
}
Chip, Badge, Avatar, User
The small pieces that carry status and identity.
import { Avatar, AvatarGroup, Badge, Button, Chip, User } from '@quxkit/ui-kit-registry'
export default function DisplayDemo() {
return (
<div className="demo-stack demo-stack--center">
<div className="demo-row">
<Chip tone="success">Active</Chip>
<Chip tone="warning">Expiring</Chip>
<Chip tone="danger">Failed</Chip>
<Chip>Draft</Chip>
<Chip tone="accent" variant="solid">
Pro
</Chip>
</div>
<div className="demo-row">
<Badge content={3} tone="danger" label="3 unread">
<Button variant="outline" tone="neutral" size="sm">
Inbox
</Button>
</Badge>
<AvatarGroup max={3} size="sm">
<Avatar name="Ada Lovelace" />
<Avatar name="Grace Hopper" />
<Avatar name="Katherine Johnson" />
<Avatar name="Annie Easley" />
</AvatarGroup>
<User name="Ada Lovelace" description="Owner" />
</div>
</div>
)
}
Calendar
A month grid for choosing a date.
ScopeGregorian calendar only, with locale-aware month and weekday names. No week numbers and no multi-month view.
August 2026
| Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday |
|---|---|---|---|---|---|---|
import { Calendar } from '@quxkit/ui-kit-registry'
import { useState } from 'react'
export default function CalendarDemo() {
const [date, setDate] = useState('2026-08-19')
return <Calendar label="Delivery date" value={date} onChange={setDate} />
}
Pricing
The system is free. The shortcuts are not.
Everything that makes the components work is open source. What you can pay for is the time you would otherwise spend building the surrounding material.
$180per seat, once
- All 50 components, the theme engine, agent contracts and MCP server
- Figma library generated from the theme
- Twelve application templates
- Brand kit export for stakeholders
Talk to usorg-wide
- Everything in Studio
- Private component registry
- Theme review and migration
- Priority support
Want the engineering? The Oklch ramp construction, the APCA contrast repair, the agent contracts and MCP server, and the five-package layout live in the docs. And if you are after finished billing screens rather than a design system, billing-kit-components is that: UI-Kit is the layer underneath, for the rest of the product.
Give it one colour.
A verified theme, fifty components and the agent contracts come back. See them carrying a real application on QuxCloud, then take a seat: bought once, it covers a developer for good. No renewal, no expiry, and no licence check anywhere in the code you ship.