Theming
Semantic colors, light and dark palettes, shape, spacing, and type.
Start with a recipe. Choose a color mode and accent. Override only the tokens your design needs. Every option is typed, serializable, and available to agents through the same component contract.
Explore theme and motion together in the Design Playground.
Color roles
Colors describe a purpose: surface for the trigger, surfaceRaised for the popup, foreground for text, and highlight for the active item. accent and accentForeground are a pair; focusRing and danger remain independent so interaction and destructive actions stay legible.
<DropdownMenu
items={items}
variant="solid"
theme={{ mode: 'light', accent: 'blue' }}
/>Use neutral, blue, violet, green, amber, or coral. Omitting the accent keeps the recipe's palette. A supplied accent updates the solid trigger and keyboard focus ring; surfaces remain neutral.
Dark mode
Pass theme.mode="dark" as an object property. The dropdown uses a separately defined dark palette, including its portalled and nested menus. It does not change the page theme or read local storage.
<DropdownMenu
items={items}
theme={{ mode: isDark ? 'dark' : 'light', accent: 'violet' }}
/>Your application owns its theme preference. Pass the same mode to each instance through a shared preset; changing it does not reset menu state.
Shape, spacing, and typography
recipe chooses studio or soft; size chooses sm, md, or lg. These supply defaults for the same token vocabulary. Explicit tokens win, even when their value is zero.
<DropdownMenu
items={items}
recipe="soft"
size="sm"
theme={{
tokens: {
radius: 6,
popupRadius: 12,
itemRadius: 4,
fontFamily: 'Inter, sans-serif',
fontSize: 13,
triggerPadding: 14,
popupPadding: 8,
itemPaddingY: 7,
popupMinWidth: 240,
shadow: 'floating',
},
}}
/>Numeric geometry and font sizes use pixels. Opacity is unitless; font weight is numeric. Fonts are provided by the host application. Popup widths remain clamped to the viewport; if the preferred minimum exceeds the maximum, the maximum wins.
Token reference
These values are generated from the schema and resolveTheme(). Defaults below are studio / md / light, without an accent override. The Design Playground starts with blue. soft changes its palette, shape, spacing, and elevation; size changes density. Call resolveTheme(theme, recipe, size) to inspect any combination.
| Token / CSS variable | Default | Values / purpose |
|---|---|---|
surface--ui-surface | #ffffff | CSS color Trigger background |
surfaceRaised--ui-surface-raised | #ffffff | CSS color Popup background |
foreground--ui-text | #26272b | CSS color Primary text and icons |
mutedForeground--ui-muted | #6b6e76 | CSS color Shortcut text |
border--ui-border | #e3e4e8 | CSS color Control edges and separators |
highlight--ui-highlight | #f2f3f5 | CSS color Hovered trigger and active menu item |
accent--ui-accent | #36383e | CSS color Solid trigger background |
accentForeground--ui-accent-foreground | #ffffff | CSS color Text on a solid trigger |
danger--ui-danger | #b23e38 | CSS color Destructive action text |
focusRing--ui-focus-ring | #36383e | CSS color Keyboard focus outline |
radius--ui-radius | 8 | 0–48 Trigger corner radius, px |
popupRadius--ui-popup-radius | 10 | 0–48 Popup corner radius, px |
itemRadius--ui-item-radius | 5 | 0–48 Item corner radius, px |
borderWidth--ui-border-width | 1 | 0–4 Control and popup border, px |
fontFamily--ui-font | inherit | Font family list CSS font family list; fonts are supplied by the host |
fontSize--ui-font-size | 14 | 10–24 Trigger and item text, px |
fontWeight--ui-font-weight | 500 | 100–900 Trigger text weight |
shortcutSize--ui-shortcut-size | 11 | 10–20 Shortcut text, px |
triggerHeight--ui-trigger-height | 36 | 24–80 Minimum trigger height, px |
triggerPadding--ui-trigger-padding | 12 | 0–40 Trigger inline padding, px |
triggerGap--ui-trigger-gap | 12 | 0–32 Trigger label-to-chevron gap, px |
popupPadding--ui-popup-padding | 6 | 0–24 Popup inner padding, px |
itemHeight--ui-item-height | 36 | 24–80 Minimum item height, px |
itemPaddingX--ui-item-padding-x | 10 | 0–32 Item inline padding, px |
itemPaddingY--ui-item-padding-y | 8 | 0–24 Item block padding, px |
itemGap--ui-item-gap | 8 | 0–24 Item label-to-icon gap, px |
popupMinWidth--ui-popup-min-width | 220 | 120–640 Preferred minimum popup width, px; viewport clamped |
popupMaxWidth--ui-popup-max-width | 320 | 120–800 Preferred maximum popup width, px; viewport clamped |
shadow--ui-shadow | soft | none / soft / floating Popup elevation |
disabledOpacity--ui-disabled-opacity | 0.45 | 0.2–1 Disabled trigger and item opacity |
Color values accept hex, numeric rgb() / rgba(), hsl() / hsla(), oklch() / oklab(), transparent, currentColor, or a simple var(--token) reference. Use valid CSS color syntax. Define referenced variables on a shared ancestor such as :root, since a portal can leave a locally scoped container. Keep foreground/background contrast when overriding colors.
Shared application defaults
import { DropdownMenu, type DropdownPreset } from '@uthereal/ui/dropdown-menu'
const appMenu = {
recipe: 'studio',
size: 'sm',
theme: {
mode: 'light',
accent: 'blue',
tokens: { fontFamily: 'Inter, sans-serif', popupRadius: 12 },
},
} satisfies DropdownPreset
<DropdownMenu
preset={appMenu}
items={items}
theme={{ tokens: { popupRadius: 0 } }}
/>Resolution order is library defaults → application preset → instance arguments. Nested option objects merge by field, arrays replace, and undefined leaves the inherited value alone. The example keeps the font and accent but uses square popup corners. Resolved design values then follow recipe and size → color mode and accent → explicit tokens.
React CSS escape hatch
The existing tokens prop still accepts CSS custom properties and inline styles, applied to the trigger container and every popup positioner. It wins over typed theme values. Use it for host-only CSS integration; agents use the validated theme object instead.
<DropdownMenu
items={items}
tokens={{ '--ui-accent': 'var(--brand)', '--ui-radius': '10px' }}
/>The legacy --ui-surface shorthand also colors the popup, and --ui-radius derives popup and item radii unless those CSS variables are supplied separately. Prefer typed tokens for new components: their names, limits, and meaning are exported in the agent schema.