Skip to main content

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.

Migrating from styled-components

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:

  1. 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.
  2. styles/global.css — imports Tailwind, the platform theme bridge, the UI-Kit base styles, and theme.css, and registers the UI-Kit Tailwind plugin.
  3. DynamicThemeProvider (runtime) — overrides the token fallbacks with the live platform theme using inline styles, which win over the @layer base fallbacks. This is what makes a widget match the host's actual brand colors and theme.
  4. Your component — uses Tailwind utility classes (bg-background, text-foreground, text-destructive/50) and @invent/wl-ui-kit-next/ui components. 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).

TokenUtilitiesPurpose
--background / --foregroundbg-background / text-foregroundBase page surface and text
--primary / --primary-foregroundbg-primary / text-primary-foregroundBrand color, primary actions
--secondary / --secondary-foregroundbg-secondary / …Supporting / secondary actions
--accent / --accent-foregroundbg-accent / …Highlights, call-to-actions
--muted / --muted-foregroundbg-muted / text-muted-foregroundSubtle backgrounds, secondary text
--destructive / --destructive-foregroundbg-destructive / …Errors, delete, critical (was danger)
--success / --success-foregroundbg-success / …Success states
--warning / --warning-foregroundbg-warning / …Warnings
--card / --card-foregroundbg-card / …Card surfaces
--popover / --popover-foregroundbg-popover / …Floating surfaces (tooltips, dropdowns)
--sidebar*bg-sidebar, text-sidebar-foreground, …Sidebar surface + variants
--borderborder-borderBorders, dividers
--inputborder-inputInput field borders
--ringring-ringFocus rings
--chart-1 … --chart-5bg-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 base that DynamicThemeProvider overrides 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 with color-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)UtilitiesNotes
--spacingp-4, gap-2, m-3, size-10Base spacing unit (0.25rem); drives the whole Tailwind scale
--radius (+ --radius-sm/md/lg/xl)rounded-sm / rounded-md / rounded-lg / rounded-xlCorner radius (--radius defaults to 0.65rem)
--font-sans / --font-mono / --font-seriffont-sans, font-mono, font-serifFont families
--font-weight-*font-light … font-blackWeights (light 300 → black 900)
--shadow*shadow-sm … shadow-2xl, shadow-tooltipElevation
--breakpoint-xxs (20rem), --breakpoint-xs (26.5625rem)xxs:, xs: prefixesExtra-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)]" />
note

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 componentclassName="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) → 8pxp-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-…)