Foundations
Colour
Semantic colour roles, status colours with words and icons, state layers, dark-mode rules, the primitive palette and the live contrast results for 72 pairs.
Colour works in three layers: primitives (raw hex values), semantic roles (primary, base-100, error) and Citia extensions (--ds-link, --ds-focus). Components use only the last two. Every role has a value in the light and the dark theme, and the swatches below are rendered live from tokens.json, so they are always current.
The source is tokens/tokens.json. Never edit Styles/generated/tokens.css by hand; run dotnet run tools/tokens.cs.
Semantic roles
The daisyUI 5 roles. Each swatch shows light on the left and dark on the right. Use them as var(--color-primary) or as Tailwind utilities (bg-primary, text-base-content).
--color-base-100#f9f9f6#151c25
Surface: cards, tables, inputs, document sidebar.--color-base-200#f1f2ee#0e141b
Page background, table headers, read-only fields.--color-base-300#dfe3dd#253040
Borders and dividers.--color-base-content#0f172a#e6edf3
Primary text.--color-primary#00838f#00a4b2
Primary actions, active navigation, brand focus.--color-primary-content#ffffff#0e141b
--color-secondary#00767f#05666f
Application sidebar background.--color-secondary-content#ffffff#ffffff
--color-accent#00a4b2#00d4df
Focus ring, active rail marker, charts.--color-accent-content#0f172a#0e141b
--color-neutral#0f172a#e6edf3
--color-neutral-content#f6f8fa#0e141b
--color-info#3b6fd4#7aa2f7
--color-info-content#ffffff#0e141b
--color-success#047857#34d399
--color-success-content#ffffff#0e141b
--color-warning#c2410c#f59e0b
--color-warning-content#ffffff#0e141b
--color-error#dc2626#f87171
--color-error-content#ffffff#0e141b
| Role | Use |
|---|---|
base-100 |
Surfaces: cards, tables, inputs, menus. |
base-200 |
Page background, table headers, quiet fills. |
base-300 |
Hairline borders and dividers (1px). |
base-content |
Body text and icons on the base surfaces. |
primary, primary-content |
The one primary action, selection, focus; text on top of it. |
secondary, accent |
Sidebar surface and active markers. Not focus rings. |
info, success, warning, error |
Status fills and borders (see below). |
Surfaces are neutral and warm. Teal appears on the primary action, the current navigation item, the selection and the focus ring, and nowhere else by default.
Citia extensions
Roles that daisyUI does not have. CSS names are --ds-<name>; Tailwind reads --color-<name> (text-link, bg-popover, border-control).
--ds-control-border#7b8a94#74848f
Border for inputs, selects and secondary buttons (≥ 3:1).--ds-quiet#006a73--color-primary
Quiet teal text: ghost buttons, eyebrows, empty-state icons.--ds-link#2b5cc4--color-info
Identity link (.ds-link-ref). Blue is only for links.--ds-error-text#b91c1c--color-error
--ds-warning-text#9a3412--color-warning
--ds-popover--color-base-100mix
Floating layer surface (dropdown, calendar, combobox).--ds-popover-border--ds-control-border--ds-control-border
--ds-popover-hovermixmix
--ds-subtlemixmix
Secondary text: field help, captions, meta labels.--ds-mutedmixmix
Tertiary text that still passes AA on base-100 (.text-muted).--ds-focus#00838f#00d4df
Focus ring. The older version used accent (#00a4b2 = 2.86:1 on base-100); the DS uses primary so it is ≥ 3:1 (WCAG 1.4.11).--ds-scrimrgb(8 18 28 / .4)rgb(8 18 28 / .4)
Modal backdrop (::backdrop).--ds-excel#1d6f42#1d6f42
--ds-excel-hover#185c37#185c37
--ds-pdf#b30b00#b30b00
--ds-pdf-hover#8f0900#8f0900
--ds-syntax-tag#006780#66d9e8
XML/HTML tag.--ds-syntax-attr#8c3a17#edba82
XML/HTML attribute.--ds-primary-softmixmix
Tonal primary fill for status surfaces (alerts, pills, chips, selected rows). Pair with --ds-primary-text or base content.--ds-info-softmixmix
Tonal info fill for status surfaces (alerts, pills, chips, selected rows). Pair with --ds-info-text or base content.--ds-success-softmixmix
Tonal success fill for status surfaces (alerts, pills, chips, selected rows). Pair with --ds-success-text or base content.--ds-warning-softmixmix
Tonal warning fill for status surfaces (alerts, pills, chips, selected rows). Pair with --ds-warning-text or base content.--ds-error-softmixmix
Tonal error fill for status surfaces (alerts, pills, chips, selected rows). Pair with --ds-error-text or base content.--ds-neutral-softmixmix
Tonal neutral fill: tags, inactive pills, inline code backgrounds.--ds-info-text#2b5cc4--color-info
Info text and icons on light surfaces (≥ 4.5:1).--ds-success-text#047857--color-success
Success text and icons on light surfaces (≥ 4.5:1).--ds-primary-text#006a73--color-primary
Primary-coloured text on light surfaces (≥ 4.5:1).--ds-chart-1--color-primary--color-primary
Categorical chart series 1. Graphic contrast on base-100 (≥ 3.9:1); use in this order, never as the only cue (label or legend).--ds-chart-2#3a61c4#86a9f7
Categorical chart series 2. Graphic contrast on base-100 (≥ 3.9:1); use in this order, never as the only cue (label or legend).--ds-chart-3#c26300#f0a646
Categorical chart series 3. Graphic contrast on base-100 (≥ 3.9:1); use in this order, never as the only cue (label or legend).--ds-chart-4#c72c53#f6838e
Categorical chart series 4. Graphic contrast on base-100 (≥ 3.9:1); use in this order, never as the only cue (label or legend).--ds-chart-5#137738#6cd092
Categorical chart series 5. Graphic contrast on base-100 (≥ 3.9:1); use in this order, never as the only cue (label or legend).--ds-chart-6#8048b6#be95ec
Categorical chart series 6. Graphic contrast on base-100 (≥ 3.9:1); use in this order, never as the only cue (label or legend).--ds-chart-7#576574#a0b0c1
Categorical chart series 7. Graphic contrast on base-100 (≥ 3.9:1); use in this order, never as the only cue (label or legend).--ds-chart-8#857d0b#c9c261
Categorical chart series 8. Graphic contrast on base-100 (≥ 3.9:1); use in this order, never as the only cue (label or legend).| Token | Use |
|---|---|
--ds-subtle |
Secondary text: hints, captions, metadata (8.7:1 in light). |
--ds-muted |
Tertiary text that still passes AA (6.04:1). Never for essential information. |
--ds-control-border |
Borders of inputs, selects, secondary buttons. At least 3:1 against the surface (3.37:1 light, 4.44:1 dark). |
--ds-link |
Links in running text and tables. Always underlined or otherwise marked. |
--ds-error-text, --ds-warning-text |
Text on pale error or warning tint; the base colours are too light for small text in the light theme. |
--ds-focus |
The 2px focus ring. The only focus colour. |
--ds-popover, --ds-popover-border, --ds-popover-hover |
Floating layers: solid, not transparent. |
--ds-scrim |
Modal backdrop. |
Important
Blue is for links. Teal is for the primary action, selection and focus. Do not use a link colour on a button or a status.
Status colours
Status uses four hues with fixed meaning. A tonal (pale) fill carries the area; the saturated colour appears only as a 1px border, an icon or a dot. A status always includes a word, and the icon shape differs per tone, so it survives colour-blindness and greyscale.
| Tone | Meaning | Icon | Example word |
|---|---|---|---|
info |
Neutral news, in progress | info |
Scheduled |
success |
Done, healthy | check-circle |
Paid |
warning |
Needs attention, not failed | alert-triangle |
Awaiting payment |
error |
Failed or destructive | x-circle |
Overdue |
View HTML code
<div class="flex flex-wrap gap-3">
<span class="ds-pill is-info">Scheduled</span>
<span class="ds-pill is-success">Paid</span>
<span class="ds-pill is-warning">Awaiting payment</span>
<span class="ds-pill is-error">Overdue</span>
<span class="ds-pill is-neutral">Draft</span>
</div>Rules:
- Write the status as a word. Colour and dot support it.
- Text on a tint uses
base-contentor the-textrole, never the base status colour in the light theme. - Do not mark an error with red alone: add the message beside the field, linked with
aria-describedby. - Do not change a meaning to suit a brand. A brand may adjust a tone, not a meaning.
State layers
Hover, press, selection and drag are expressed as a translucent layer of the content colour over the surface, not as new colours. Tokens:
| Token | Value | Used for |
|---|---|---|
--ds-opacity-state-hover |
0.06 | Hover on rows, ghost buttons, menu items |
--ds-opacity-state-pressed |
0.10 | Pointer down |
--ds-opacity-state-selected |
0.12 | Selected rows, current item, chosen option (with the primary colour) |
--ds-opacity-state-drag |
0.16 | Drop targets and dragged items |
.app-row:hover {
background: color-mix(in oklab, var(--color-base-content) calc(var(--ds-opacity-state-hover) * 100%), var(--color-base-100));
}
.app-row[aria-selected="true"] {
background: color-mix(in oklab, var(--color-primary) calc(var(--ds-opacity-state-selected) * 100%), var(--color-base-100));
}Because layers mix from tokens, they adjust to any theme with no new hex values. Tints stay at 16% or lower so text on them keeps 4.5:1.
Tint and border recipes
| Purpose | Range | Example |
|---|---|---|
| Soft fill on a surface | 6 to 12% | color-mix(in oklab, var(--color-error) 10%, var(--color-base-100)) |
| Soft fill on nothing | 4 to 16% | color-mix(in oklab, var(--color-primary) 10%, transparent) |
| Tone border | 22 to 45% | color-mix(in oklab, var(--color-info) 35%, var(--color-base-300)) |
| Text selection | 24% | color-mix(in oklab, var(--color-primary) 24%, transparent) |
Dark mode
The dark theme is designed, not inverted.
- Surfaces are blue-black (
base-100#151c25, page#0e141b), not pure black, so shadows and hairlines still read. primarybecomes#00a4b2with dark text#0e141b; light uses#00838fwith white.--ds-focusbecomes aqua#00d4df(9.38:1) because the light focus colour is too dark on a dark page.--ds-linkmoves to theinfoblue#7aa2f7(6.81:1).- Status colours get lighter (
success#34d399,error#f87171) and their-textroles equal the base colours, which already pass 4.5:1. - Popovers are lifted with a little white (
color-mixwith 6% white) so a layer separates from the page without relying on shadow; shadows are darker (.36) because the page already is. - Avoid pure white text (
base-contentis#e6edf3) and large saturated areas, which glare on dark.
Choose the theme in markup with data-theme="light" or "dark"; see Theming.
Primitive palette
The raw values behind the roles. Hover a chip for its note.
white
white#ffffffteal
teal.500#00a4b2teal.600#00838fteal.700#00767fteal.750#006a73teal.800#05666faqua
aqua.400#00d4dfpaper
paper.50#f9f9f6paper.100#f1f2eepaper.200#dfe3ddsteel
steel.400#7b8a94steel.450#74848fslate
slate.900#0f172afrost
frost.50#f6f8fafrost.100#e6edf3night
night.700#253040night.800#151c25night.900#0e141bblue
blue.400#7aa2f7blue.500#3b6fd4blue.600#2b5cc4emerald
emerald.400#34d399emerald.700#047857amber
amber.500#f59e0borange
orange.700#c2410corange.800#9a3412red
red.400#f87171red.600#dc2626red.700#b91c1cexcel
excel.600#1d6f42excel.700#185c37pdf.600#b30b00pdf.700#8f0900avatar
avatar.c0#00838favatar.c1#2b5cc4avatar.c2#7c3aedavatar.c3#c2410cavatar.c4#047857avatar.c5#be185davatar.c6#4d7c0favatar.c7#0e7490brand
brand.deep#04222abrand.ink#0b1720brand.amber#ffb938Warning
Components never use primitives. Write var(--color-primary), not #00838f or teal.600. When a theme changes, only the semantic roles are remapped; a component that used a primitive would be left behind.
Contrast results
dotnet run tools/tokens.cs computes the WCAG 2.2 ratio of 72 pairs in both themes and writes wwwroot/tokens/contrast.json. A required pair below 4.5:1 (text) or 3:1 (boundaries, focus ring, icons) fails the build. Pairs marked informational do not decide the result.
| Theme | Pair | Sample | Ratio | Minimum | Result |
|---|---|---|---|---|---|
light | base-content Body text on surfaces | Aa 12 #0f172a / #f9f9f6 | 16.93:1 | 4.5:1 | Pass |
light | base-content / base-200 Body text on the page background | Aa 12 #0f172a / #f1f2ee | 15.88:1 | 4.5:1 | Pass |
light | subtle Secondary text (field help) | Aa 12 #404858 / #f9f9f6 | 8.70:1 | 4.5:1 | Pass |
light | subtle / base-200 Secondary text on the background | Aa 12 #404858 / #f1f2ee | 8.16:1 | 4.5:1 | Pass |
light | muted Muted text on surfaces | Aa 12 #5a5f6b / #f9f9f6 | 6.04:1 | 4.5:1 | Pass |
light | muted / base-200 Muted text on the background | Aa 12 #575d69 / #f1f2ee | 5.87:1 | 4.5:1 | Pass |
light | primary-content Primary button label | Aa 12 #ffffff / #00838f | 4.52:1 | 4.5:1 | Pass |
light | secondary-content Sidebar item | Aa 12 #ffffff / #00767f | 5.39:1 | 4.5:1 | Pass |
light | sidebar active Active sidebar item | Aa 12 #ffffff / #005056 | 9.24:1 | 4.5:1 | Pass |
light | sidebar group Sidebar group header (11 px) | Aa 12 #e6f1f2 / #00767f | 4.68:1 | 4.5:1 | Pass |
light | accent-content Text on accent | Aa 12 #0f172a / #00a4b2 | 5.91:1 | 4.5:1 | Pass |
light | info-content Filled info alert or badge | Aa 12 #ffffff / #3b6fd4 | 4.76:1 | 4.5:1 | Pass |
light | success-content Filled success badge | Aa 12 #ffffff / #047857 | 5.48:1 | 4.5:1 | Pass |
light | warning-content Filled warning badge | Aa 12 #ffffff / #c2410c | 5.18:1 | 4.5:1 | Pass |
light | error-content Destructive button | Aa 12 #ffffff / #dc2626 | 4.83:1 | 4.5:1 | Pass |
light | link Link on surfaces | Aa 12 #2b5cc4 / #f9f9f6 | 5.80:1 | 4.5:1 | Pass |
light | link / base-200 Link on the background | Aa 12 #2b5cc4 / #f1f2ee | 5.44:1 | 4.5:1 | Pass |
light | link / row hover Link in a hovered table row | Aa 12 #2b5cc4 / #eeefed | 5.30:1 | 4.5:1 | Pass |
light | quiet Ghost button label | Aa 12 #006a73 / #f9f9f6 | 6.02:1 | 4.5:1 | Pass |
light | quiet / ghost hover Ghost button on hover | Aa 12 #006a73 / #dae4e3 | 4.89:1 | 4.5:1 | Pass |
light | error-text Error message, soft error badge | Aa 12 #b91c1c / #f9f9f6 | 6.13:1 | 4.5:1 | Pass |
light | warning-text Soft warning badge or alert | Aa 12 #9a3412 / #f9f9f6 | 6.93:1 | 4.5:1 | Pass |
light | success text Active status (text on tint) | Aa 12 #047857 / #e8eee8 | 4.66:1 | 4.5:1 | Pass |
light | error text (raw) Validation message using --color-error | Aa 12 #dc2626 / #f9f9f6 | 4.58:1 | 4.5:1 | Pass |
light | popover text Text on floating layers | Aa 12 #0f172a / #f9f9f6 | 16.93:1 | 4.5:1 | Pass |
light | excel Excel button | Aa 12 #ffffff / #1d6f42 | 6.17:1 | 4.5:1 | Pass |
light | pdf PDF button | Aa 12 #ffffff / #b30b00 | 7.08:1 | 4.5:1 | Pass |
light | control-border Input / secondary button border (non-text) | Aa 12 #7b8a94 / #f9f9f6 | 3.37:1 | 3:1 | Pass |
light | focus ring Focus ring (non-text) | Aa 12 #00838f / #f9f9f6 | 4.29:1 | 3:1 | Pass |
light | focus ring / base-200 Focus ring on the background | Aa 12 #00838f / #f1f2ee | 4.02:1 | 3:1 | Pass |
light | accent ring (v1) Version 1 focus ring — informational | Aa 12 #00a4b2 / #f9f9f6 | 2.86:1 | 3:1 | Info |
light | primary edge Edge of active controls (chips, segments) | Aa 12 #00838f / #f9f9f6 | 4.29:1 | 3:1 | Pass |
* | avatar-c0 White initials on an avatar | Aa 12 #ffffff / #00838f | 4.52:1 | 4.5:1 | Pass |
* | avatar-c1 White initials on an avatar | Aa 12 #ffffff / #2b5cc4 | 6.12:1 | 4.5:1 | Pass |
* | avatar-c2 White initials on an avatar | Aa 12 #ffffff / #7c3aed | 5.70:1 | 4.5:1 | Pass |
* | avatar-c3 White initials on an avatar | Aa 12 #ffffff / #c2410c | 5.18:1 | 4.5:1 | Pass |
* | avatar-c4 White initials on an avatar | Aa 12 #ffffff / #047857 | 5.48:1 | 4.5:1 | Pass |
* | avatar-c5 White initials on an avatar | Aa 12 #ffffff / #be185d | 6.04:1 | 4.5:1 | Pass |
* | avatar-c6 White initials on an avatar | Aa 12 #ffffff / #4d7c0f | 4.99:1 | 4.5:1 | Pass |
* | avatar-c7 White initials on an avatar | Aa 12 #ffffff / #0e7490 | 5.36:1 | 4.5:1 | Pass |
dark | base-content Body text on surfaces | Aa 12 #e6edf3 / #151c25 | 14.51:1 | 4.5:1 | Pass |
dark | base-content / base-200 Body text on the page background | Aa 12 #e6edf3 / #0e141b | 15.66:1 | 4.5:1 | Pass |
dark | subtle Secondary text (field help) | Aa 12 #acb3ba / #151c25 | 8.08:1 | 4.5:1 | Pass |
dark | subtle / base-200 Secondary text on the background | Aa 12 #acb3ba / #0e141b | 8.72:1 | 4.5:1 | Pass |
dark | muted Muted text on surfaces | Aa 12 #a3aab1 / #151c25 | 7.31:1 | 4.5:1 | Pass |
dark | muted / base-200 Muted text on the background | Aa 12 #a1a8ae / #0e141b | 7.66:1 | 4.5:1 | Pass |
dark | primary-content Primary button label | Aa 12 #0e141b / #00a4b2 | 6.13:1 | 4.5:1 | Pass |
dark | secondary-content Sidebar item | Aa 12 #ffffff / #05666f | 6.70:1 | 4.5:1 | Pass |
dark | sidebar active Active sidebar item | Aa 12 #ffffff / #02444b | 10.84:1 | 4.5:1 | Pass |
dark | sidebar group Sidebar group header (11 px) | Aa 12 #e6f0f1 / #05666f | 5.76:1 | 4.5:1 | Pass |
dark | accent-content Text on accent | Aa 12 #0e141b / #00d4df | 10.12:1 | 4.5:1 | Pass |
dark | info-content Filled info alert or badge | Aa 12 #0e141b / #7aa2f7 | 7.35:1 | 4.5:1 | Pass |
dark | success-content Filled success badge | Aa 12 #0e141b / #34d399 | 9.63:1 | 4.5:1 | Pass |
dark | warning-content Filled warning badge | Aa 12 #0e141b / #f59e0b | 8.62:1 | 4.5:1 | Pass |
dark | error-content Destructive button | Aa 12 #0e141b / #f87171 | 6.69:1 | 4.5:1 | Pass |
dark | link Link on surfaces | Aa 12 #7aa2f7 / #151c25 | 6.81:1 | 4.5:1 | Pass |
dark | link / base-200 Link on the background | Aa 12 #7aa2f7 / #0e141b | 7.35:1 | 4.5:1 | Pass |
dark | link / row hover Link in a hovered table row | Aa 12 #7aa2f7 / #1c232c | 6.30:1 | 4.5:1 | Pass |
dark | quiet Ghost button label | Aa 12 #00a4b2 / #151c25 | 5.68:1 | 4.5:1 | Pass |
dark | quiet / ghost hover Ghost button on hover | Aa 12 #00a4b2 / #1a2d36 | 4.74:1 | 4.5:1 | Pass |
dark | error-text Error message, soft error badge | Aa 12 #f87171 / #151c25 | 6.20:1 | 4.5:1 | Pass |
dark | warning-text Soft warning badge or alert | Aa 12 #f59e0b / #151c25 | 7.98:1 | 4.5:1 | Pass |
dark | success text Active status (text on tint) | Aa 12 #34d399 / #1a282e | 7.85:1 | 4.5:1 | Pass |
dark | error text (raw) Validation message using --color-error | Aa 12 #f87171 / #151c25 | 6.20:1 | 4.5:1 | Pass |
dark | popover text Text on floating layers | Aa 12 #e6edf3 / #202730 | 12.73:1 | 4.5:1 | Pass |
dark | excel Excel button | Aa 12 #ffffff / #1d6f42 | 6.17:1 | 4.5:1 | Pass |
dark | pdf PDF button | Aa 12 #ffffff / #b30b00 | 7.08:1 | 4.5:1 | Pass |
dark | control-border Input / secondary button border (non-text) | Aa 12 #74848f / #151c25 | 4.44:1 | 3:1 | Pass |
dark | focus ring Focus ring (non-text) | Aa 12 #00d4df / #151c25 | 9.38:1 | 3:1 | Pass |
dark | focus ring / base-200 Focus ring on the background | Aa 12 #00d4df / #0e141b | 10.12:1 | 3:1 | Pass |
dark | accent ring (v1) Version 1 focus ring — informational | Aa 12 #00d4df / #151c25 | 9.38:1 | 3:1 | Pass |
dark | primary edge Edge of active controls (chips, segments) | Aa 12 #00a4b2 / #151c25 | 5.68:1 | 3:1 | Pass |
Do and don't
- Do use roles through variables:
color: var(--ds-subtle),border-color: var(--ds-control-border). - Do pair every status colour with a word and an icon.
- Do keep one
btn-primaryper area; teal marks it. - Do check any new text-on-colour pair and add it to
tools/tokens.cs. - Don't use hex values, primitives or Tailwind palette classes (
bg-red-500) in components. - Don't tint above 16% behind text, or use gradients, glows or large saturated blocks.
- Don't use colour alone for state: selected rows also get a check, a bar or a weight change.
- Don't use blue for buttons or teal for links.
.app-ref { color: var(--ds-link); text-decoration: underline; }
.app-hint { color: var(--ds-subtle); }
.app-field { border: 1px solid var(--ds-control-border); background: var(--color-base-100); }<a asp-page="Details" asp-route-id="@Model.Id" class="text-link underline underline-offset-2">INV-2026-0418</a>
<p class="text-subtle">Format INV-YYYY-NNNN</p>
<input class="input border-control" />Those utilities come from generated/tailwind-theme.css and resolve per element, so a nested data-theme keeps its own colours.