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.
| Skin | Theme | Topbar 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:
| Token | Use | Example |
|---|---|---|
--thl-on-{color} | text/icons drawn on a --thl-color-{color} fill | white on primary, dark ink #1d2326 on success / info / warning |
--thl-color-{color}-text | the color used as text on surfaces | #18713d for success text, #b5361a for danger text |
--thl-on-theme, --thl-theme-text | the same pair for the current skin | the 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-*):
| Token | Light | Purpose |
|---|---|---|
--thl-theme | skin | accent: active menu, focus, links on hover, primary highlights |
--thl-topbar-1, --thl-topbar-2 | skin | topbar gradient |
--thl-color-{primary,success,info,warning,danger,inverse,purple,…} | palette | semantic colors |
--thl-body-bg | #f4f8f9 | page background |
--thl-surface, --thl-surface-muted | #fff, #f7fafc | cards, inputs, table stripes |
--thl-border-color | #e5ebec | borders |
--thl-text, --thl-text-strong, --thl-heading, --thl-link | typography colors | |
--thl-sidebar-bg, --thl-sidebar-text, --thl-sidebar-active | side menu | |
--thl-font-family, --thl-font-size, --thl-line-height | Open Sans, 14px, 1.6 | |
--thl-radius-sm, --thl-radius, --thl-radius-lg | 2 / 4 / 6px | radius: 'sharp' → 0 |
--thl-topbar-height, --thl-sidebar-width, --thl-sidebar-mini-width | 60 / 265 / 60px | layout |
--thl-z-base | 1000 | from zIndex config |
Full list: node_modules/@thl-ui/styles/dist/tokens.css.
Custom brand
Override tokens in your own CSS — globally or per skin:
: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:
<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:
@use '@thl-ui/styles/scss/index' with ($p: 'acme');Layouts
layout switches AppLayout between three shells:
| Value | Shell |
|---|---|
vertical | full sidebar (265px) |
mini | icon sidebar (60px), flyout submenus |
horizontal | top menu bar |
Below 1170px the sidebar becomes mini automatically, and below 768px it becomes an off-canvas drawer (configurable via components.Sidebar.breakpoint).