Yugen UI

Theming

Theme, brand and text direction are attributes on the DOM. YugenProvider sets them for the whole app or for one part of the UI.

data-theme & data-brand

Every color is read from a CSS custom property. Two attributes pick the set of values: data-theme picks the neutral and semantic palette, data-brand picks the brand ramp.

AttributeValueMeaning
data-themelightDefault (declared on :root), equivalent to webLightTheme.
data-themedarkEquivalent to webDarkTheme. Dark background, and the brand uses lighter steps (Background = brand70, Foreground1 = brand100).
data-themehigh-contrastHigh contrast mode (equivalent to teamsHighContrastTheme): black background, white text, cyan accent, yellow links, green for disabled; shadows become a 1px white border. Already declared in tokens.css. Pick High contrast in the Theme menu at the top to try it.
data-brandsakuraDefault. Cherry blossom pink 桜, Brand80 reaches 5.16:1 with white text.
data-brandwebCommunication Blue, the original Fluent 2 brand.
data-brandteamsIndigo “Dusk”.
data-brandyour-brandAny ramp you declare with [data-brand="your-brand"] (see the end of this page).
<!-- No JavaScript needed: the attributes on <html> are enough -->
<html lang="en" data-theme="dark" data-brand="web" dir="ltr">

YugenProvider

The equivalent of FluentProvider: applies theme, brand and text direction to a component tree, and adds Radix's DirectionProvider, TooltipProvider and (optionally) a Toaster.

Prop · TypeDefaultDescription
theme"light" | "dark" | "high-contrast"Default: "light"Written to data-theme.
brand"sakura" | "web" | "teams" | stringDefault: "sakura"Written to data-brand. Accepts custom brand names.
dir"ltr" | "rtl"Default: "ltr"Written as dir on the wrapper and passed to Radix as the text direction (keyboard, Slider, submenus…).
applyToDocumentbooleanDefault: falsetrue: writes data-theme / data-brand / dir to <html>. Use it on the root provider so portals get the theme too. false: applies to the wrapping div only (nested theme); the div gets bg-background-1 text-foreground-1.
withTooltipProviderbooleanDefault: trueWraps a TooltipProvider (300ms delay) around the Tooltips inside.
withToasterbooleanDefault: falseDeprecated in 1.0.0. Render <Toaster /> as a child of the root provider so toast() works and its position and limit are explicit; the default is bottom-end (bottom-start in RTL).
messagesYugenMessagesInputDefault: —Replaces components' built-in strings (English by default) by namespace, such as messagesVi for Vietnamese. Nested providers inherit, then override. See Interface language.
localestringDefault: "en-US"Intl locale for dates, times and numbers in components. Nested providers inherit it.
defaultsYugenDefaultsInputDefault: —Default props by component, such as { Button: { shape: "circular" } }. A prop on the instance wins. Nested providers merge per component. See Default props & density below.
density"compact" | "default" | "comfortable"Default: "default"compact: controls default to size small and spacing scales by 0.9375. comfortable: spacing scales by 1.125. Written to data-density (and to <html> with applyToDocument). Nested providers inherit it.
scrollLock"hide-scrollbar" | "keep-scrollbar"Default: "hide-scrollbar"Keeps or hides the page scrollbar while Menu, ContextMenu or Dropdown is open. Works with applyToDocument only (writes data-yugen-scroll-lock to <html>). 1.0 switches the default to keep-scrollbar. See Scroll lock.
className, …div propsHTMLAttributes<HTMLDivElement>Default: —Passed to the wrapping div (the ref is forwarded).
// Root provider, used once
<YugenProvider applyToDocument theme="light" brand="sakura" dir="ltr">
  <Toaster />
  <App />
</YugenProvider>

Default props & density

defaults sets props for every instance of a component in one part of the UI. density makes that part tighter or roomier. A prop on the instance always wins.

<YugenProvider
  applyToDocument
  density="compact"
  defaults={{
    Button: { shape: "circular" },
    Input: { appearance: "filled-darker" },
  }}
>
  <App />
</YugenProvider>

// An explicit prop still wins
<Button size="medium">Export</Button>
  • Order: the prop on the instance, then defaults of the nearest provider (nested providers merge per component), then density, then the component's own default.
  • density="compact" gives buttons, fields, pickers, tabs, toolbars, tables, Tree and Nav size small by default and scales spacing by 0.9375. "comfortable" scales spacing by 1.125 and keeps the sizes. Spacing comes from data-density in CSS (--spacing and the spacing tokens, the same factors as the Theme Designer), so prerendered HTML is right before hydration.
  • At compact density, size small controls are 22.5px high. Leave room between controls placed close together (WCAG 2.5.8).
  • Components with style props (size, appearance, shape, material) read their defaults. Parts such as TabsTrigger or ToolbarButton follow their parent. Defaults also reach components used inside others, such as the close button of a Dialog, so prefer size, shape and material over appearance.
  • defaults is typed per component. An object written inline in JSX is compared by value, so re-rendering the provider with the same defaults does not re-render the tree.
  • Yugen Labs: defaults={glassNavigationDefaults} gives every navigation component with glass material="glass". See Glass.
  • Source mode (shadcn registry, yugen add): you can also change defaultVariants in the copied file.

The useYugen() hook

Returns { theme, brand, dir, locale, density } from the nearest YugenProvider. Use it when a component needs to change content (not colors) with the theme or text direction.

import * as React from "react";
import { WeatherMoon20Regular, WeatherSunny20Regular } from "@fluentui/react-icons";
import { Button, Toaster, YugenProvider, useYugen, type YugenTheme } from "@/components/ui";

export default function App() {
  const [theme, setTheme] = React.useState<YugenTheme>("light");
  return (
    <YugenProvider applyToDocument theme={theme}>
      <Toaster />
      <ThemeToggle onToggle={() => setTheme((t) => (t === "light" ? "dark" : "light"))} />
    </YugenProvider>
  );
}

function ThemeToggle({ onToggle }: { onToggle: () => void }) {
  const { theme } = useYugen();
  const dark = theme === "dark";
  return (
    <Button
      appearance="subtle"
      icon={dark ? <WeatherSunny20Regular /> : <WeatherMoon20Regular />}
      aria-label={dark ? "Switch to light theme" : "Switch to dark theme"}
      onClick={onToggle}
    />
  );
}

Don't use useYugen() to pick colors; they already change through the tokens. Use it for content: a sun or moon icon, light and dark illustrations, arrow direction. Surfaces in portals already follow the nested provider (see Positioning).

import { useYugenLocale, useYugenTheme } from "@/components/ui";

const { theme, brand, dir } = useYugenTheme(); // re-renders when the theme, brand or dir change
const locale = useYugenLocale(); // re-renders when the locale changes

useYugen() re-renders when either the theme or the locale settings change. When a component needs only one of them, use useYugenTheme() ({ theme, brand, dir }) or useYugenLocale() (the locale string). Yugen's components do the same, so a theme switch re-renders only the parts that read the theme, such as charts and CodeBlock.

Nested themes

Set data-theme / data-brand on any element to change the theme of everything inside it. A YugenProvider without applyToDocument does exactly that.

useYugen() → theme: light · brand: sakura · dir: ltr

3

useYugen() → theme: dark · brand: web · dir: ltr

3

useYugen() → theme: light · brand: teams · dir: ltr

3
import { YugenProvider } from "@/components/ui";

<div className="grid gap-4 md:grid-cols-3">
  <YugenProvider theme="light" brand="sakura" className="rounded-xlarge border border-stroke-2 p-4">
    <Sample />
  </YugenProvider>
  <YugenProvider theme="dark" brand="web" className="rounded-xlarge border border-stroke-2 p-4">
    <Sample />
  </YugenProvider>
  <YugenProvider theme="light" brand="teams" className="rounded-xlarge border border-stroke-2 p-4">
    <Sample />
  </YugenProvider>
</div>
  • When you nest a brand inside a theme that differs from <html>, put both attributes on the same element: brand aliases are recomputed per [data-brand][data-theme] pair. YugenProvider always writes both.
  • The element with the attributes should set its own background (bg-background-1, bg-background-2…). Otherwise it is transparent and the parent theme's background shows through.
  • Dialog, Menu, Popover and Tooltip render through a portal into <body> but still pick up the theme, brand and text direction of the nearest nested YugenProvider (through usePortalThemeProps()). Details on Positioning.

Three token layers

Global → Alias → Utility. Each layer is read only by the layer right after it.

LayerExamplesUsed by
Global--grey-14, --brand-80, --red-shade-10tokens.css only (plus decorative illustrations and the palette page).
Alias--colorNeutralForeground1, --colorBrandBackground, --spacingM, --shadow16index.css (mapping to Tailwind) and custom CSS when you really need it.
Utilitytext-foreground-1, bg-brand-background, p-3, shadow-16Every .tsx file.
/* 1. GLOBAL (tokens.css): raw values, no role */
:root { --grey-14: #242424; --white: #ffffff; }
[data-brand="web"] { --brand-70: #115ea3; --brand-80: #0f6cbd; /* … 10 → 160 */ }

/* 2. ALIAS (tokens.css): roles, same names as Fluent UI v9, change with the theme */
[data-theme="light"] { --colorNeutralForeground1: var(--grey-14); }
[data-theme="light"], [data-brand] { --colorBrandBackground: var(--brand-80); }
[data-theme="dark"] { --colorBrandBackground: var(--brand-70); }

/* 3. UTILITY (index.css): the bridge to Tailwind */
@theme inline {
  --color-foreground-1: var(--colorNeutralForeground1);   /* text-foreground-1 */
  --color-brand-background: var(--colorBrandBackground);  /* bg-brand-background */
}

Alias names match Fluent UI v9 tokens one to one, so you can look them up in the Fluent docs: tokens.colorNeutralStroke1 is var(--colorNeutralStroke1) and the border-stroke-1 utility.

Create your own brand

A brand is a 16-step ramp, --brand-10 … --brand-160. Aliases (button background, link, focus, selection tint) take the right step for light and dark.

  1. Open the Theme Designer and enter a key color and a brand name. It builds the ramp by keeping the OKLCH lightness / chroma curve of Communication Blue and changing the hue, previews it on real components and checks contrast.
  2. In the export section at the end of the page, copy the CSS (or download the file) and paste the [data-brand="name"] block into your app's main CSS file, right after @import "yugen-ui/theme.css". If you copied the source into your project, paste it into src/styles/tokens.css after the existing brands.
  3. Use brand="name" on YugenProvider (the YugenBrand type accepts any string) or data-brand="name" on <html>.
/* src/styles/tokens.css: paste after the existing brand blocks (illustrative values; use the Theme Designer to generate an exact ramp) */
[data-brand="matcha"] {
  --brand-10: #0a1a10;
  --brand-20: #0f2818;
  --brand-30: #13351f;
  --brand-40: #174327;
  --brand-50: #1b522f;
  --brand-60: #1f6137;
  --brand-70: #236f3f;
  --brand-80: #277d47; /* key color, must be ≥ 4.5:1 on white */
  --brand-90: #2f9455;
  --brand-100: #3fac66;
  --brand-110: #5bbb7d;
  --brand-120: #74c690;
  --brand-130: #93d4a8;
  --brand-140: #b1e1c0;
  --brand-150: #cdeed6;
  --brand-160: #e9f7ed;
}
// The whole app
<YugenProvider applyToDocument theme="light" brand="matcha">…</YugenProvider>

// Or one region only (for example the marketing page of a sub-product)
<YugenProvider theme="light" brand="matcha" className="rounded-xlarge p-5">…</YugenProvider>
Do
  • Keep Brand80 at 4.5:1 or more with white text. It is the primary button background.
  • Pick a hue far from danger red, so a delete button is not mistaken for the primary one.
  • Check both light and dark under Color › Accessibility.
Don't
  • Don't adjust steps by hand. An uneven lightness curve gives wrong hover / pressed colors.
  • Don't mix two brands in the same part of the UI.
  • Don't spread the brand over large areas (header, sidebar, page background).