Skip to content

Theming & dark mode ​

All visuals come from CSS custom properties generated by @thl-ui/tokens. Components never hard-code colors, so skins, dark mode and your own brand are just token overrides.

Skins ​

12 skins are built in: 6 brand colors × light / dark sidebar.

SkinThemeTopbar gradient
default#0277b9#0078bc → #00beda
green#2ecc71#25bf67 → #3fdd82
yellow#ffb136#f2a225 → #ffbb51
red#d6411d#d93c17 → #ff704f
purple#4a23ad#4a23ad → #693fd5
black#282f32#282f32 → #4b555b

Append -dark (e.g. green-dark) for the dark #4F5467 sidebar. Set with theme in config or set({ theme }) at runtime. Try the skin selector in the top bar of this site.

Accessible color pairs ​

Bright fills such as #2ecc71 cannot carry white text at WCAG AA (2.1:1). thl-ui keeps the hues for fills and adds companion tokens:

TokenUseExample
--thl-on-{color}text/icons drawn on a --thl-color-{color} fillwhite on primary, dark ink #1d2326 on success / info / warning
--thl-color-{color}-textthe color used as text on surfaces#18713d for success text, #b5361a for danger text
--thl-on-theme, --thl-theme-textthe same pair for the current skinthe green and yellow skins use dark text on active tabs

All pairs are ≥ 4.5:1 in light and dark mode. A unit test enforces this (packages/core/src/core.test.ts → token contrast). Primary (#0277b9) and danger (#d6411d) are dark enough for white text.

Dark mode ​

colorMode: 'dark' swaps surface, text and border tokens; 'auto' follows the OS (prefers-color-scheme) and updates live. This site syncs the VitePress appearance toggle to colorMode.

Tokens ​

The most useful tokens (--thl-*):

TokenLightPurpose
--thl-themeskinaccent: active menu, focus, links on hover, primary highlights
--thl-topbar-1, --thl-topbar-2skintopbar gradient
--thl-color-{primary,success,info,warning,danger,inverse,purple,…}palettesemantic colors
--thl-body-bg#f4f8f9page background
--thl-surface, --thl-surface-muted#fff, #f7fafccards, inputs, table stripes
--thl-border-color#e5ebecborders
--thl-text, --thl-text-strong, --thl-heading, --thl-linktypography colors
--thl-sidebar-bg, --thl-sidebar-text, --thl-sidebar-activeside menu
--thl-font-family, --thl-font-size, --thl-line-heightOpen Sans, 14px, 1.6
--thl-radius-sm, --thl-radius, --thl-radius-lg2 / 4 / 6pxradius: 'sharp' → 0
--thl-topbar-height, --thl-sidebar-width, --thl-sidebar-mini-width60 / 265 / 60pxlayout
--thl-z-base1000from zIndex config

Full list: node_modules/@thl-ui/styles/dist/tokens.css.

Custom brand ​

Override tokens in your own CSS — globally or per skin:

css
:root {
  --thl-font-family: 'Inter', system-ui, sans-serif;
  --thl-color-primary: #6d28d9;
}

/* a new skin: set config.theme = 'brand' */
[data-thl-theme='brand'] {
  --thl-theme: #6d28d9;
  --thl-topbar-1: #5b21b6;
  --thl-topbar-2: #8b5cf6;
}

/* tweak dark mode only */
[data-thl-color-mode='dark'] {
  --thl-body-bg: #0b0d12;
}

theme is typed as ThlSkin; for custom skins cast or widen it: set({ theme: 'brand' as never }).

Scoped theming ​

Tokens are plain CSS variables, so any subtree can use a different look:

html
<section data-thl-theme="red" style="--thl-radius: 12px">
  <!-- components here use the red skin and rounder corners -->
</section>

Class prefix ​

All classes follow BEM with a configurable prefix: .thl-btn, .thl-btn--primary, .thl-btn__icon. To ship a different prefix, set prefix in config and compile the SCSS sources with $p:

scss
@use '@thl-ui/styles/scss/index' with ($p: 'acme');

Layouts ​

layout switches AppLayout between three shells:

ValueShell
verticalfull sidebar (265px)
miniicon sidebar (60px), flyout submenus
horizontaltop menu bar

Below 1170px the sidebar becomes mini automatically, and below 768px it becomes an off-canvas drawer (configurable via components.Sidebar.breakpoint).