Qure UI

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 | border

This 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:

Qure UIbg-accent text-accent-foreground
qtrackbg-sds-brand-tertiary text-sds-brand-default
/** * The same row written twice: once in this library's vocabulary, once in * qtrack's. They compile to the same declarations because both names point at * the same Figma token. */export default function ThemingSds() {  return (    <div className="flex w-full max-w-lg flex-col gap-4">      <div className="bg-accent text-accent-foreground border-input flex items-center justify-between rounded-md border p-4">        <span className="text-body-small-strong">Qure UI</span>        <code className="text-label-base">bg-accent text-accent-foreground</code>      </div>      <div className="bg-sds-brand-tertiary text-sds-brand-default border-sds-base-tertiary border-sds-thick rounded-sds-200 p-sds-400 flex items-center justify-between">        <span className="text-style-body-small-strong">qtrack</span>        <code className="text-style-label-base">bg-sds-brand-tertiary text-sds-brand-default</code>      </div>    </div>  )}

Use 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.

GroupWritten as
Backgroundbg-sds-brand-tertiary, bg-sds-urgent-default-hover, bg-sds-scrim
Texttext-sds-base-secondary, text-sds-attention-on-attention
Iconicon-sds-base-tertiary — sets color, kept separate from text on purpose
Borderborder-sds-success-secondary, border-sds-swatch
Stroke widthborder-sds-thin (0.5px), -thick, -focus, -selected
Spacep-sds-400, gap-sds-200, m-sds-800
Radiusrounded-sds-200, rounded-sds-full
Depthz-sds-400 (dropdowns), z-sds-1200 (modals), z-sds-n100
Shadowshadow-sds-400, inset-shadow-sds-200
Typetext-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.

On this page