Theming
How a token reaches a component, and what to change when you want a different look.
Three layers, all copied into your repository, and knowing which is which tells you where to make a change.
The token layer
registry/styles/tokens.css here, components/ui/qure-tokens.css once installed — 516 CSS custom
properties. Names describe a role, never a value:
--background-brand-default
│ │ └── emphasis: default | secondary | tertiary | hover | on-brand
│ └──────── family: base | brand | success | attention | urgent | disabled | neutral
└─────────────────── role: background | text | icon | borderThis file is the design system. There is no upstream to sync from and nothing here is regenerated
from a drawing — a value is correct because it is in tokens.css, and changing the Qure look means
changing it here, in this repository, where a designer and Claude can work on it directly.
In your own app it is an ordinary file you own, so change it there — or redeclare the token afterwards. See Installation.
The bridge
registry/styles/theme-qure.css — aliases, and nothing else. Components are written against a
small role vocabulary; this is where it meets Qure's names.
--color-primary: var(--background-brand-default);
--color-primary-foreground: var(--text-brand-on-brand);
--color-input: var(--border-base-tertiary);If a colour looks wrong, the token is wrong — the bridge only renames.
It is also the one layer that cannot be a file in your project. Its @theme block is what makes
text-body-small and rounded-md exist as Tailwind utilities, and a @theme rule only takes
effect in the stylesheet that imports Tailwind. So shadcn add writes it into your globals.css
directly, the same way shadcn ships its own thirty variables.
--color-input is border-base-tertiary rather than the softer base-default on purpose. It
is the lightest border token clearing the 3:1 that WCAG 2.2 SC 1.4.11 asks of a control
boundary — 3.63:1 light, 4.17:1 dark. An unchecked checkbox is nothing but its boundary.
The look
registry/styles/components/<name>.css — one stylesheet per component. shadcn add button writes
button.css next to button.tsx, and the component imports it. Each block quotes the Figma node
and dimensions it implements:
/* Figma "Button" (node 3223:563)
md h40 px16 gap6 fs16/24 icon20 radius-200 */
.cn-button-size-md {
height: 40px;
padding-inline: 16px;
gap: 6px;
font-size: 16px;
border-radius: var(--radius-200);
}These are the files you edit to reskin the library. A .tsx references class names and never
values, so a change here reaches every instance of that component without touching a line of React.
Notice what is and is not written out in the block above. The geometry is literal — 40px, 16px
— but the radius is a var() pointing back at the token layer, and so is every colour in the
library. Across all 62 stylesheets there are 1054 var() references and 36 literal colours, and
those 36 are shadow tints.
That ratio is why a copied stylesheet is not a fork of the brand. What a copy freezes is 40px —
the part a product might reasonably want to differ on anyway. Change a colour in tokens.css and
an app picks it up by re-installing the token file alone, without touching any of the sixty-two
stylesheets it has copied.
Overriding one instance
className wins — tailwind-merge resolves the conflict in the caller's favour.
<Button className="w-full rounded-none">Sign report</Button>The sds-* vocabulary
The qtrack app writes the same design system under different class names — bg-sds-brand-tertiary
rather than bg-accent, text-style-body-small rather than text-body-small. Both are available
here, and they mean the same thing:
bg-accent text-accent-foregroundbg-sds-brand-tertiary text-sds-brand-defaultUse whichever your team already types. The sds-* names exist so that a developer moving between
qtrack and a project built on this library does not have to hold two vocabularies at once, and so
that a component lifted out of qtrack keeps working when it lands here.
| Group | Written as |
|---|---|
| Background | bg-sds-brand-tertiary, bg-sds-urgent-default-hover, bg-sds-scrim |
| Text | text-sds-base-secondary, text-sds-attention-on-attention |
| Icon | icon-sds-base-tertiary — sets color, kept separate from text on purpose |
| Border | border-sds-success-secondary, border-sds-swatch |
| Stroke width | border-sds-thin (0.5px), -thick, -focus, -selected |
| Space | p-sds-400, gap-sds-200, m-sds-800 |
| Radius | rounded-sds-200, rounded-sds-full |
| Depth | z-sds-400 (dropdowns), z-sds-1200 (modals), z-sds-n100 |
| Shadow | shadow-sds-400, inset-shadow-sds-200 |
| Type | text-style-body-base, text-style-label-caps, text-sds-16 |
Two things worth knowing.
They alias our tokens, not qtrack's values. compat-sds.css is generated from
tokens.css, so bg-sds-brand-default resolves to var(--background-brand-default) rather
than to a copied hex. The compat layer cannot drift from the design system, and dark mode works
without a second definition because the variable already switches.
They cost nothing unused. Every one is a Tailwind @utility, so the 173 colour aliases emit
CSS only for the names you actually write, and compose with variants —
hover:bg-sds-brand-default-hover, dark:text-sds-base-default.
icon-sds-* sets color, the same property text-sds-* sets. That is not a mistake. Twenty-two
of the thirty pairs are identical, but eight are not, and they are the interesting ones: on a
tinted status surface the icon stays saturated while the text goes dark. On a success-tertiary
fill, --icon-success-on-success-tertiary is the full green #00c86e and
--text-success-on-success-tertiary is #053d2b — a glyph reads as a glyph at full strength,
a sentence at that colour does not read at all. Reach for the one that says what you are
colouring.
Why the split
Two independent axes of change. Upstream improves a component's behaviour, and you replace one
.tsx. Design changes the brand, and you edit one .css. Neither touches the other, which is
the entire reason the styling does not live in the component files.