Theming and labels
Built-in palettes, surface tones, class-driven dark mode, and replacing every built-in string for i18n.
Theming
// built-in palette per list
defineListConfig({ colorTheme: 'teal', /* … */ })
// global default
<ListKitProvider theme="teal">…</ListKitProvider>
// custom theme (brand colors) — pass a ThemeClasses object anywhere a theme is accepted
const brand: ThemeClasses = {
primaryBg: 'bg-[#121c38]',
primaryText: 'text-white',
focusRing: 'focus:ring-indigo-500',
focusBorder: 'focus:border-indigo-500',
/* … */
}
defineListConfig({ colorTheme: brand, /* … */ })Table & card tones
colorTheme drives the accents; tones drives the neutral chrome — the
table's header, dividers, row hover/selected states, and the panel around the
table and the default cards. Pick a built-in preset ('gray' is the default,
'contrast' inverts the header) or pass a full SurfaceTones object of
Tailwind classes. The two axes compose: colorTheme: 'teal', tones: 'slate'.
// preset: 'gray' | 'slate' | 'zinc' | 'contrast'
defineListConfig({ tones: 'slate' /* … */ })
// custom — keep every background opaque: pinned cells inherit the row's
defineListConfig({
tones: {
container: 'border-indigo-100 bg-white shadow-sm',
headerBg: 'bg-indigo-50',
headerText: 'text-indigo-700',
headerDivider: 'border-indigo-200',
rowBg: 'bg-white',
rowHover: 'hover:bg-indigo-50',
rowSelected: 'bg-indigo-100 hover:bg-indigo-100',
divider: 'divide-indigo-100',
},
/* … */
})The Table primitive takes the same value as a tones prop for standalone
use, and getSurfaceTones resolves a preset name if you need the classes
yourself.
Dark mode
Every component ships additive dark: variants keyed on a .dark class on an
ancestor (usually <html>) — not on prefers-color-scheme — so your app owns
the toggle. listkit/tailwind.css registers the variant for you:
@custom-variant dark (&:where(.dark, .dark *));Importing that file is enough. Without it Tailwind v4 reads dark: as
prefers-color-scheme, and a light-only app renders its lists dark for every
reader whose OS is — the rest of the page unchanged.
Toggle document.documentElement.classList.toggle('dark') and every list —
table, cards, menus, filters, dialogs, skeletons — follows. The built-in
palettes carry dark accent variants; a custom ThemeClasses can append its own
dark: classes inside each field. Light rendering is untouched when the class
is absent.
Labels (i18n)
Controls describable by an icon (view toggle, filter button, results count) are
icon-only, so they read the same in any language; their names live in
aria-label/title. Every other built-in string comes from a labels object
that defaults to English — override it app-wide on the provider, or per list
via config.labels.
Quickest path — DEFAULT_LABELS (English) and ES_LABELS (Spanish) cover the
common cases in one line (override individual keys on top if needed):
import { ListKitProvider, ES_LABELS } from 'listkit'
// whole app in Spanish (English is the default, so no prop needed for English)
;<ListKitProvider labels={ES_LABELS}>…</ListKitProvider>Or hand-pick the strings:
// app-wide (the app's language)
;<ListKitProvider
labels={{
tableView: 'Vista tabla',
cardsView: 'Vista tarjetas',
filters: 'Filtros',
applyFilters: 'Aplicar',
clearFilters: 'Limpiar',
empty: 'Sin resultados',
yes: 'Sí',
no: 'No',
results: n => `${n} resultado${n === 1 ? '' : 's'}`,
}}
>
…
</ListKitProvider>
// or per list (wins over the provider)
defineListConfig({ labels: { empty: 'Sin pedidos' } /* … */ })NextListView accepts the same labels (and theme) prop and forwards it to
its internal provider: <NextListView labels={ES_LABELS} config={…} adapter={…} />.
Resolution order: config.labels → provider labels → DEFAULT_LABELS. The
existing per-item props still win where they exist (config.emptyMessage,
config.filtersTitle, a filter's trueLabel/falseLabel). See ListLabels
for the full key list.