Theming and Design Tokens
Widgets are themed with Tailwind CSS v4 and a set of CSS design tokens. You style your UI with Tailwind utility classes (bg-primary, text-muted-foreground, rounded-lg) and the shadcn-based component library shipped in @invent/wl-ui-kit-next. The host application injects the live platform theme at runtime, so the same token names automatically pick up the end-user's colors, fonts, and dark/light mode.
Earlier widgets used styled-components with a JavaScript theme object and helper functions like getColor() / getSizeBy() from @invent/wl-ui-kit. That approach is deprecated. There is no useTheme(), no styled.* template literals, and no getColor/getSizeBy/getTypography helpers in current widgets. Everything below uses CSS variables and Tailwind utilities instead. See Mapping from the old helpers at the bottom.
How theming fits together
There are four layers, from the token definitions up to what you write in a component:
styles/theme.css— defines the design tokens as CSS custom properties (in OKLCH) for the light theme, overrides them under.dark, and maps them to Tailwind utilities via@theme inline. These are fallback values.styles/global.css— imports Tailwind, the platform theme bridge, the UI-Kit base styles, andtheme.css, and registers the UI-Kit Tailwind plugin.DynamicThemeProvider(runtime) — overrides the token fallbacks with the live platform theme using inline styles, which win over the@layer basefallbacks. This is what makes a widget match the host's actual brand colors and theme.- Your component — uses Tailwind utility classes (
bg-background,text-foreground,text-destructive/50) and@invent/wl-ui-kit-next/uicomponents. You never read a theme object in JS.
theme.css tokens (fallbacks, OKLCH)
│ @theme inline
▼
Tailwind utilities ──► your className="bg-primary text-primary-foreground"
▲
│ inline styles (higher priority)
DynamicThemeProvider ◄── live platform theme (host)
global.css
@import 'tailwindcss' source('../../');
@import 'tw-animate-css';
@import '@invent/tailwind-integration/css/widget-theme.css'; /* platform theme bridge */
@import '@invent/wl-ui-kit-next/widget-base.css'; /* UI-Kit base styles */
@import './theme.css'; /* your token fallbacks */
@plugin "@invent/wl-ui-kit-next/tailwind-plugin";
The exact import list varies per widget — some omit tw-animate-css or widget-base.css, and some declare the dark variant explicitly with @custom-variant dark (&:is(.dark *));. The two that always matter are @invent/tailwind-integration/css/widget-theme.css (the platform theme bridge that publishes the --theme-* and --color-* variables) and the @invent/wl-ui-kit-next/tailwind-plugin.
Mounting: WidgetRootWrapper
New widgets mount through WidgetRootWrapper from @invent/wl-ui-kit-next/widget (optionally inside a Shadow DOM), with the compiled CSS injected inline so it's present on first paint:
import { WidgetRootWrapper } from '@invent/wl-ui-kit-next/widget';
import globalStyles from './styles/global.css?inline';
<WidgetRootWrapper widgetName={widgetConfig.name} styles={[globalStyles]}>
{children}
</WidgetRootWrapper>;
Some already-migrated widgets in core-widgets still wrap with a local ShadowDomWrapper instead — the mechanism differs, but the CSS-token model below is identical. WidgetRootWrapper is the current convention for new widgets.
Design tokens
Tokens are defined once in theme.css and exposed to Tailwind through @theme inline, which generates the matching utility classes. For a color token --primary, the mapping --color-primary: var(--primary) produces bg-primary, text-primary, border-primary, etc.
Color tokens
Colors follow the shadcn/ui convention: each surface/role token has a paired -foreground token for content placed on top of it (this replaces the old onPrimary / onSurface naming).
| Token | Utilities | Purpose |
|---|---|---|
--background / --foreground | bg-background / text-foreground | Base page surface and text |
--primary / --primary-foreground | bg-primary / text-primary-foreground | Brand color, primary actions |
--secondary / --secondary-foreground | bg-secondary / … | Supporting / secondary actions |
--accent / --accent-foreground | bg-accent / … | Highlights, call-to-actions |
--muted / --muted-foreground | bg-muted / text-muted-foreground | Subtle backgrounds, secondary text |
--destructive / --destructive-foreground | bg-destructive / … | Errors, delete, critical (was danger) |
--success / --success-foreground | bg-success / … | Success states |
--warning / --warning-foreground | bg-warning / … | Warnings |
--card / --card-foreground | bg-card / … | Card surfaces |
--popover / --popover-foreground | bg-popover / … | Floating surfaces (tooltips, dropdowns) |
--sidebar* | bg-sidebar, text-sidebar-foreground, … | Sidebar surface + variants |
--border | border-border | Borders, dividers |
--input | border-input | Input field borders |
--ring | ring-ring | Focus rings |
--chart-1 … --chart-5 | bg-chart-1, text-chart-2, … | Categorical chart series colors |
Widgets set these token fallbacks in one of two ways, both seen in production core-widgets:
- OKLCH fallbacks — literal values under
@layer basethatDynamicThemeProvideroverrides at runtime, e.g.--primary: oklch(0.62 0.21 259.23);(used by e.g.datagrid-next). - Direct platform mapping — map each shadcn token straight onto the platform variable with a hex fallback, e.g.
--primary: var(--theme-primary, #0167ff);and--background: var(--theme-surface, #ffffff);(used by e.g.pie-chart-next). Cards/accents are often derived withcolor-mix()from those platform values.
Either way the utility names (bg-primary, text-foreground, …) are the same — only how the fallback is expressed differs.
Non-color tokens
| Token(s) | Utilities | Notes |
|---|---|---|
--spacing | p-4, gap-2, m-3, size-10 | Base spacing unit (0.25rem); drives the whole Tailwind scale |
--radius (+ --radius-sm/md/lg/xl) | rounded-sm / rounded-md / rounded-lg / rounded-xl | Corner radius (--radius defaults to 0.65rem) |
--font-sans / --font-mono / --font-serif | font-sans, font-mono, font-serif | Font families |
--font-weight-* | font-light … font-black | Weights (light 300 → black 900) |
--shadow* | shadow-sm … shadow-2xl, shadow-tooltip | Elevation |
--breakpoint-xxs (20rem), --breakpoint-xs (26.5625rem) | xxs:, xs: prefixes | Extra-small viewport breakpoints (track the browser window). For layout that responds to the widget's own width, use container queries (@container + @[600px]: / @sm:) instead |
Loading gradient tokens
For loading/shimmer states there are gradient tokens that prefer the platform's published gradients and fall back to a local color-mix approximation, so a widget's loading state matches the platform loading overlay:
--color-gradient-1: var(--theme-gradients-gradient1, color-mix(in srgb, var(--primary) 35%, var(--background)));
--color-gradient-3: var(--theme-gradients-gradient3, var(--primary));
--color-gradient-4: var(--theme-gradients-gradient4, color-mix(in srgb, var(--primary) 50%, rgb(188, 43, 186)));
Use them as arbitrary values: bg-(--color-gradient-3), from-background to-(--color-gradient-1).
Using tokens in components
Style with Tailwind utility classes. Opacity modifiers and arbitrary values work with tokens too:
// Base surface + text
<div className="flex h-full w-full flex-col bg-background text-foreground">…</div>
// Token with an opacity modifier
<AlertTriangle className="size-10 text-destructive/50" />
// Muted secondary text
<p className="text-sm text-muted-foreground">There is nothing to show yet</p>
// Arbitrary radius / token value
<div className="rounded-[10px] outline outline-1 outline-foreground/10">…</div>
UI-Kit components
Prefer the ready-made, already-themed components from @invent/wl-ui-kit-next/ui (built on shadcn/ui). They consume the same tokens, so they inherit the platform theme and dark mode automatically:
import { Button, Card, CardContent, CardHeader, CardTitle, Badge, Select } from '@invent/wl-ui-kit-next/ui';
import { Maximize2 } from 'lucide-react'; // icons come from lucide-react
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
<Button>Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="ghost" size="icon-lg"><Maximize2 className="size-5" /></Button>
</div>
<Badge variant="outline">Outline</Badge>
</CardContent>
</Card>;
The package exposes several entry points — @invent/wl-ui-kit-next/ui (components, used above), @invent/wl-ui-kit-next/widget (WidgetRootWrapper, WidgetHeader, …), and @invent/wl-ui-kit-next/typography. Some widgets import components from the package root (@invent/wl-ui-kit-next) instead of the /ui subpath; both resolve to the same components.
Runtime theming and platform sync
At runtime, DynamicThemeProvider reads the host's theme and writes the token values as inline styles, overriding the @layer base fallbacks in theme.css. You don't call it directly — it's part of the widget runtime — but it's why every bg-primary/text-foreground in your widget matches the host's actual brand.
The bridge is provided by @invent/tailwind-integration/css/widget-theme.css. It publishes two families of variables you can read directly:
--theme-*— the raw platform theme values, e.g.--theme-primary,--theme-surface,--theme-onSurface,--theme-danger,--theme-link/--theme-linkHover, and--theme-gradients-gradient1..4.--color-*— the UI-Kit's MD3-style tokens, e.g.--color-surface,--color-on-surface-high/-medium/-low,--color-outline,--color-outline-variant,--color-primary.
When you need a value that has no shadcn utility, read it with var() and a sensible fallback — either as an inline style or a Tailwind arbitrary value:
// Inline style: platform link color, falling back to the primary token
<button style={{ color: 'var(--theme-link, var(--primary))' }} className="font-bold hover:underline">
error details
</button>
// Arbitrary value referencing a UI-Kit token directly (common in core-widgets)
<span className="text-[var(--color-on-surface-medium)]">Secondary label</span>
<div className="border border-[var(--color-outline)]" />
Exact --theme-* names can differ slightly between widgets (e.g. some use --theme-colors-link rather than --theme-link). Check the widget's own theme.css / global.css for the names it maps, and always provide a fallback in the var() call.
Dark mode
Dark mode is first-class: theme.css redefines every token under a .dark selector, and the host toggles that class. Because your components reference tokens (bg-card, text-foreground) rather than literal colors, they adapt with no extra work — avoid hardcoded hex/rgb values so dark mode keeps working.
:root { --card: oklch(1 0 0); --foreground: oklch(0.14 0 285.86); }
.dark { --card: oklch(0.21 0.01 285.93); --foreground: oklch(0.99 0 0); }
Customizing tokens
To adjust a widget's own defaults, edit the token in theme.css (and its .dark counterpart) — the change flows to every utility that references it. Note the platform theme still overrides these fallbacks at runtime via DynamicThemeProvider, so widget-level overrides mainly affect standalone development and hosts that don't publish that token.
@layer base {
:root {
--primary: oklch(0.62 0.21 259.23); /* change once → bg-primary, text-primary, ring-ring… all update */
}
}
Mapping from the old styled-components helpers
If you're porting a widget off styled-components / @invent/wl-ui-kit, here's the rough translation. All of these helpers are gone — use tokens/utilities instead.
| Old (styled-components) | New (Tailwind + tokens) |
|---|---|
getColor('primary') in a styled component | className="text-primary" |
getColor('secondary'), getColorShade('primary', 'lowEmphasis') | text-secondary, bg-primary/10 (opacity modifier) |
getColor('danger') | text-destructive / bg-destructive |
getChartColor(0) | bg-chart-1 (index 0 → chart-1) |
getSizeBy(2) → 8px | p-2 / gap-2 / m-2 (Tailwind spacing scale) |
getThemeProp('borderRadius') | rounded-md / rounded-lg |
getTypography('Heading1') | text-2xl font-bold (Tailwind type utilities) |
getFontFamily('primary') | font-sans |
getElevation(1) | shadow-sm / shadow-md |
getGradient('primary', …) | bg-(--color-gradient-3), from-background to-(--color-gradient-1) |
useTheme() (read JS theme) | Not needed — reference tokens directly, or read a platform var with var(--theme-…) |