FAQ & troubleshooting
Installation
The UI has no colors and uses the system font
You haven't imported the CSS. Add it once at your app's entry point:
import '@thl-ui/styles/thl-ui.css';Angular: add node_modules/@thl-ui/styles/dist/thl-ui.css to styles in angular.json. Also add the thl-body class to <body> to get the font and background color.
npm install reports a peer dependency error
thl-ui requires Vue ≥ 3.5.2, React ≥ 18 or Angular ≥ 20. Older versions are not supported: Angular 19 can't compile the library, and Vue < 3.5.2 produces TypeScript type errors. Upgrade your framework, or if you really must, install with --legacy-peer-deps (not recommended).
TypeScript can't find component types
Use "moduleResolution": "Bundler" (or "NodeNext") in tsconfig.json. The legacy "node" mode can't read the package's exports field.
Do I need Bootstrap, jQuery or Google Fonts?
No. thl-ui's CSS is written entirely from scratch, and the Open Sans font is bundled in @thl-ui/styles (self-hosted, no external requests).
Theming
Change the brand color
Override CSS variables after importing thl-ui.css:
:root {
--thl-theme: #7c3aed; /* background of buttons, active tabs… */
--thl-on-theme: #fff; /* text placed on that color */
--thl-theme-text: #6d28d9; /* that color when used as text on white */
}Keep a contrast ratio of at least 4.5:1 between --thl-on-theme and --thl-theme, and between --thl-theme-text and white. See Theme & dark mode.
Change the theme for just one area
<section data-thl-theme="red" data-thl-color-mode="dark">…</section>My CSS is overridden by the library CSS (or vice versa)
Every class is prefixed with thl-, and the default link color has zero specificity (:where(...)), so a normal class selector of yours will win. To change the prefix and avoid clashes: set prefix: 'my' in the config and rebuild the SCSS with $prefix: my (@use "@thl-ui/styles/scss/index" with ($prefix: "my")).
The bundle is too large
- Vue: avoid the default
app.use(ThlUI)(it registers every component). Useimport { ThlUI } from '@thl-ui/vue'for configuration, then import only the components you need. Using only Button: ~120 KB instead of ~230 KB. - React and Angular: import individual components; the bundler tree-shakes the rest.
- Per-component CSS:
@thl-ui/styles/components/button.css… (remember to also add@thl-ui/styles/tokens.cssand@thl-ui/styles/fonts.css).
Behavior
User settings (language, skin, dark mode) aren't remembered
Enable persist: true in the config. Data is stored in localStorage under the key thl-ui-config (to rename the key: persist: { key: 'my-app-ui' }). Note that saved values override the config values on the next load.
Toast or confirm() doesn't show up
AppLayout already mounts the hosts. If you don't use AppLayout, place these once at the app root:
<ThlToaster />
<ThlConfirmHost />(React: <Toaster /> and <ConfirmHost />; Angular: <thl-toaster /> and <thl-confirm-host />.) Without a ConfirmHost, confirm() falls back to the browser's window.confirm.
Clicking the sidebar menu reloads the whole page
The sidebar uses <a href> by default. Pass the link slot (Vue), the renderLink prop (React) or the thlMenuLink template (Angular) to use router links. See step 3 in the Tutorial.
Form errors don't show
Errors only appear after the first Submit. Each ThlField needs name, value and rules, and must be inside a ThlForm with :values. To show server-side errors, pass error="…" to ThlField.
Validation messages are still in English
Messages follow the current language. Check locale in the config, or the value saved in localStorage if persist is enabled. Languages without a translation fall back to fallbackLocale (default en).
Add a new language
app.use(ThlUI, {
locales: ['vi', 'en', 'ko'],
messages: { ko: { common: { ok: '확인', cancel: '취소' } /* … */ } },
});Untranslated keys fall back to fallbackLocale. Right-to-left languages (ar, he, fa, ur) automatically set dir="rtl" and the layout mirrors itself.
Accessibility
Icon-only buttons
Always provide a label for screen readers:
<ThlButton icon="trash-2" aria-label="Delete" />Inputs without a visible label
Wrap them in ThlField label="…" (the label is linked to the input automatically), or pass aria-label.
Still stuck?
Open the matching playground (npm run dev:vue / dev:react / dev:angular) and compare with the sample page source in apps/playground-*: every component has a working example there.