UtherealUI

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 variableDefaultValues / purpose
surface
--ui-surface
#ffffffCSS color
Trigger background
surfaceRaised
--ui-surface-raised
#ffffffCSS color
Popup background
foreground
--ui-text
#26272bCSS color
Primary text and icons
mutedForeground
--ui-muted
#6b6e76CSS color
Shortcut text
border
--ui-border
#e3e4e8CSS color
Control edges and separators
highlight
--ui-highlight
#f2f3f5CSS color
Hovered trigger and active menu item
accent
--ui-accent
#36383eCSS color
Solid trigger background
accentForeground
--ui-accent-foreground
#ffffffCSS color
Text on a solid trigger
danger
--ui-danger
#b23e38CSS color
Destructive action text
focusRing
--ui-focus-ring
#36383eCSS color
Keyboard focus outline
radius
--ui-radius
80–48
Trigger corner radius, px
popupRadius
--ui-popup-radius
100–48
Popup corner radius, px
itemRadius
--ui-item-radius
50–48
Item corner radius, px
borderWidth
--ui-border-width
10–4
Control and popup border, px
fontFamily
--ui-font
inheritFont family list
CSS font family list; fonts are supplied by the host
fontSize
--ui-font-size
1410–24
Trigger and item text, px
fontWeight
--ui-font-weight
500100–900
Trigger text weight
shortcutSize
--ui-shortcut-size
1110–20
Shortcut text, px
triggerHeight
--ui-trigger-height
3624–80
Minimum trigger height, px
triggerPadding
--ui-trigger-padding
120–40
Trigger inline padding, px
triggerGap
--ui-trigger-gap
120–32
Trigger label-to-chevron gap, px
popupPadding
--ui-popup-padding
60–24
Popup inner padding, px
itemHeight
--ui-item-height
3624–80
Minimum item height, px
itemPaddingX
--ui-item-padding-x
100–32
Item inline padding, px
itemPaddingY
--ui-item-padding-y
80–24
Item block padding, px
itemGap
--ui-item-gap
80–24
Item label-to-icon gap, px
popupMinWidth
--ui-popup-min-width
220120–640
Preferred minimum popup width, px; viewport clamped
popupMaxWidth
--ui-popup-max-width
320120–800
Preferred maximum popup width, px; viewport clamped
shadow
--ui-shadow
softnone / soft / floating
Popup elevation
disabledOpacity
--ui-disabled-opacity
0.450.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.

On this page