Theme
Color, radius, border width, field height, font and motion go through CSS variables prefixed with
--tuc-, and all of them live in :root. Changing the look means overriding variables, with nothing
to recompile: no color, radius, border or height value is hard-coded in the components' CSS.
Accent color for this page
In the swatches, each color also changes --tuc-accent-fg, and --tuc-accent-text too — the accent
shade used as text color in the active menu item and in the select's tag. Changing only --tuc-accent would
leave unreadable text on the primary button.
:root {
--tuc-accent: #4f46e5;
--tuc-accent-hover: #4338ca;
--tuc-accent-fg: #ffffff; /* text on top of the accent */
--tuc-accent-text: #4338ca; /* accent as text color, in light mode */
--tuc-radius: 1rem; /* rounder corners */
--tuc-control-height: 2.5rem; /* taller fields */
}
.dark {
--tuc-accent-text: #818cf8; /* in dark mode, a lighter shade for contrast */
}The accent and the text on it
The default accent is neutral: black in light mode and near-white in dark mode. The color belongs to your project — the package doesn't impose a brand, and it fits any brand until you change it. The orange you see in this documentation is the site's color, set the same way you would set yours.
The accent has a separate token for each role, because a bright color rarely works for all of them. On this site's
orange, text is white (--tuc-accent-fg), with a contrast of 2.7 — a brand choice; if you need 4.5, use
#0a0a0a (7.4). And bright orange as text color on a light background also gives 2.7, so links, the active
menu item, the select's tag, "today" in the calendar and active icons read --tuc-accent-text, here
#B84300 in light mode (5.5 on white) and #FF7501 itself in dark mode (6.7).
/* This site's orange */
:root {
--tuc-accent: #FF7501;
--tuc-accent-hover: #FF8A2A;
--tuc-accent-fg: #ffffff;
--tuc-accent-text: #B84300;
--tuc-thumb: #ffffff;
}
.dark {
--tuc-accent: #FF7501;
--tuc-accent-hover: #FF8A2A;
--tuc-accent-fg: #ffffff;
--tuc-accent-text: #FF7501;
--tuc-thumb: #ffffff;
}.dark repeats the accent because the neutral default changes with the theme: without it, dark mode
would go back to near-white.
| What gets painted | Token |
|---|---|
| Background, border, check mark, underline | --tuc-accent |
The color property of accented text | --tuc-accent-text |
| Text and icons on top of the accent | --tuc-accent-fg |
| The switch knob | --tuc-thumb — separate, so whoever switches --tuc-accent-fg to dark doesn't get a black knob |
When you change the accent, check three contrasts
The text on top of it (--tuc-accent-fg on --tuc-accent), and the accent text on white, on --tuc-hover and on --tuc-accent-soft. A light color calls for a dark --tuc-accent-fg; a dark color, white.
Semantic tones never read the accent. Success, warning, danger and info have their own tokens, because the brand can
be any color and the meaning must not change with it. That's why info got
--tuc-info: reading the accent, with orange the info alert looked the same as the warning one, and the
"New" badge was indistinguishable from "Under review".
Tokens
The complete list, with the value for each theme. The source of truth is
src/styles/core/tokens.css; llms.txt has the same list, generated from the code.
Surface and text
| Token | Light | Dark | Controls |
|---|---|---|---|
--tuc-bg | #ffffff | #171717 | Background of fields, panels and dialogs |
--tuc-fg | #0a0a0a | #fafafa | Main text |
--tuc-muted | #737373 | #a3a3a3 | Secondary text: hint, table header, timeline time |
--tuc-subtle | #a3a3a3 | #737373 | Faded text and icons: days outside the month, sort arrow, the timeline's neutral dot |
--tuc-border | #e5e5e5 | #2e2e2e | Lines and borders |
--tuc-hover | #f5f5f5 | #262626 | Background under the cursor |
--tuc-elevated | #fafafa | #1f1f1f | Background of inner blocks: table header, zebra stripes, neutral badge |
Accent
| Token | Light | Dark | Controls |
|---|---|---|---|
--tuc-accent | #0a0a0a | #fafafa | Primary button, selected day, checked box, active tab, focus border |
--tuc-accent-hover | #262626 | #e5e5e5 | Primary button under the cursor |
--tuc-accent-fg | #ffffff | #0a0a0a | Text and icons on top of the accent |
--tuc-accent-text | #0a0a0a | #fafafa | Accent as text color: link, active menu, select tag, "today", table avatar |
--tuc-thumb | #ffffff | #171717 | Switch knob |
--tuc-accent-soft | 12% of the accent | same | Range band, select tag, selected table row, avatar background |
--tuc-accent-ring | 35% of the accent | same | Focus ring of fields and buttons |
Semantic tones
| Token | Light | Dark | Controls |
|---|---|---|---|
--tuc-success | #16a34a | #4ade80 | Success badge, alert, toast and timeline |
--tuc-warning | #d97706 | #fbbf24 | The same, for warning |
--tuc-danger | #dc2626 | #f87171 | The same, for danger; also the border and message of a field with an error |
--tuc-info | #1d4ed8 | #60a5fa | The same, for info — 6.7 on white and 5.6 on its own soft background |
--tuc-danger-fill | #dc2626 | same | Solid fill: danger button |
--tuc-success-soft | 12% of the tone | 12% of the tone | Soft badge background |
--tuc-warning-soft | 14% of the tone | 14% of the tone | Soft badge background |
--tuc-danger-soft | 12% of the tone | 12% of the tone | Soft badge background |
--tuc-info-soft | 12% of the tone | 12% of the tone | Soft badge background |
Text tone and solid fill are different tokens on purpose. --tuc-danger is lighter in
dark mode to have contrast on a dark background — and a button painted with it would look pastel with white text on top.
--tuc-danger-fill is the same in both themes.
Shape and size
| Token | Default | Up to 40rem | Controls |
|---|---|---|---|
--tuc-radius | 0.875rem | — | Corners of panels and inner blocks |
--tuc-radius-md | 0.625rem | — | Controls and day cells |
--tuc-radius-sm | 0.5rem | — | Buttons and options |
--tuc-radius-xs | 0.375rem | — | Tags and details |
--tuc-border-width | 1px | — | Width of every border in the package |
--tuc-control-height | 2.375rem | 2.75rem | Height of field, button, select, color field, segmented tabs and pagination |
--tuc-swatch | 2rem | 2.5rem | Swatch, value and eyedropper inside the color picker |
--tuc-cell | 2.25rem | — | Calendar day cell |
--tuc-text | 0.8125rem | 1rem in fields | Text size |
--tuc-font | inherit | — | Font family — by default, the project's |
Depth and motion
| Token | Light | Dark | Controls |
|---|---|---|---|
--tuc-ring | rgb(0 0 0 / 0.12) | rgb(255 255 255 / 0.14) | Thin outline of panels, part of the shadow |
--tuc-shadow | three layers | three layers, denser | Elevation of calendar, select, color picker, menu and toast |
--tuc-ease | cubic-bezier(0.16, 1, 0.3, 1) | same | Entry curve: decelerates to a stop |
--tuc-ease-in | cubic-bezier(0.4, 0, 0.9, 0.3) | same | Exit curve: accelerates until it disappears |
--tuc-duration | 160ms | same | Panels and state changes |
--tuc-duration-lg | 280ms | same | What crosses the screen: modal, drawer, toast |
--tuc-duration-out | 170ms | same | Exits |
Entry and exit use different curves on purpose: using the same curve for both makes the exit feel sluggish.
Dark mode
Dark mode follows the .dark class on <html>, the convention of Tailwind and of Django's
modern admin. data-theme="dark" works too.
<html class="dark">
<html data-theme="dark">To follow the operating system, mark the root with data-tuc-theme="auto". It's opt-in on purpose: following
the system by default made the component turn dark on its own in a light page. The project decides the theme, not
the operating system.
<html data-tuc-theme="auto">Dark mode also sets color-scheme: dark, which makes the browser draw scrollbars and native fields in a
dark tone — only inside the components, so it doesn't force the color scheme on the whole page.
Scoping by container
Since they are variables, the scope is up to you: in :root they apply to the whole page, and declared on a
container they apply only inside it. That's how you get a panel with its own color, or a dark band on a light page.
Accent only in this block
The variables go on the container itself.
Dark only in this block
.dark on a container, with its own background.
On a container, redeclare the derived tokens
--tuc-accent-soft, --tuc-accent-ring and the tones' -soft tokens are computed from another token where they were declared, in :root — and are inherited by children already computed. At the root, changing --tuc-accent recomputes everything; on a container, the soft background would stay in the old color. That's why both blocks above repeat the calculation along with it.
.panel-green {
--tuc-accent: #0f766e;
--tuc-accent-hover: #115e59;
--tuc-accent-fg: #ffffff;
--tuc-accent-text: #0f766e;
--tuc-accent-soft: color-mix(in oklab, var(--tuc-accent) 12%, transparent);
--tuc-accent-ring: color-mix(in oklab, var(--tuc-accent) 35%, transparent);
}
.dark .panel-green {
--tuc-accent-text: #2dd4bf; /* accent text readable on a dark background */
}
/* .dark on a container: the tones change, and their soft background has to be redone there */
.band.dark {
background: var(--tuc-bg);
--tuc-success-soft: color-mix(in oklab, var(--tuc-success) 12%, transparent);
--tuc-warning-soft: color-mix(in oklab, var(--tuc-warning) 14%, transparent);
--tuc-danger-soft: color-mix(in oklab, var(--tuc-danger) 12%, transparent);
--tuc-info-soft: color-mix(in oklab, var(--tuc-info) 12%, transparent);
}A panel that opens outside the container — calendar, select list, menu, toast — is inserted at the end of
<body> and doesn't inherit its variables. For those, declare the theme in :root, or use
appendTo where the component offers it.
Compact layout
Below 40rem (640px) the layout becomes compact. The same breakpoint applies to the CSS and to the
date picker's JavaScript, and it's based on width — not on pointer: coarse —, because a laptop with a
touchscreen shouldn't get phone behavior.
| Changes | To | Why |
|---|---|---|
--tuc-text | 1rem, everywhere you type or tap: fields, select, date picker, color picker, upload, editor, checkboxes, tabs, pagination and button | iOS Safari zooms in when focusing any field under 16px, and the whole page jumps. Toast and tooltip are left out: they have no fields |
--tuc-control-height | 2.75rem (44px) | Comfortable touch target |
--tuc-swatch | 2.5rem | Keeps up with the larger text on touch |
| Date picker | On a narrow and touch screen, the field doesn't receive focus and the panel itself is the input | Without focus the keyboard doesn't come up, which would cover the calendar. The touch condition is there so typing isn't turned off in a narrow desktop window |
If your project switches layout at another breakpoint, the library's compact mode won't follow — it's fixed at
40rem. Your own fields should use the same 16px on phones for the same reason; it's shadcn's
text-base md:text-sm pattern.