Yugen UI

From Fluent UI React v9

Yugen UI keeps the token names, scales and most of the API of Fluent UI React v9. Migrating is mostly a matter of changing imports and replacing makeStyles with Tailwind utilities.

Core differences

The design language is the same. Yugen differs in how it is distributed, how styles are written and which libraries handle behavior.

Fluent UI React v9Yugen UI
Distribution@fluentui/react-componentsThe yugen-ui npm package, or the source copied into your project (src/components/ui)
StylingGriffel: makeStyles + tokens (CSS-in-JS at runtime / build time)Tailwind CSS v4 utilities, mapped 1:1 to the same token names
ThemeTheme object (webLightTheme) through FluentProviderCSS custom properties per data-theme / data-brand through YugenProvider
Customizing partsSlots (root, icon, content…)asChild, ReactNode props, compound components, render-props (see Slots)
Behavior & a11yOwn implementation + TabsterRadix UI, cmdk, react-day-picker
Callbacks(event, data) => …(value) => …
Icons@fluentui/react-iconsThe same

Provider & theme

FluentProvider → YugenProvider. The theme object becomes two attributes.

// Fluent UI React v9
import { FluentProvider, webLightTheme } from "@fluentui/react-components";

<FluentProvider theme={webLightTheme} dir="ltr">
  <App />
</FluentProvider>

// Yugen UI
import { YugenProvider } from "@/components/ui";

<YugenProvider applyToDocument theme="light" brand="web" dir="ltr">
  <Toaster />
  <App />
</YugenProvider>
Fluent v9Yugen UI
webLightThemetheme="light" brand="web"
webDarkThemetheme="dark" brand="web"
teamsLightTheme / teamsDarkThemebrand="teams"
teamsHighContrastThemetheme="high-contrast" (included in tokens.css)
createLightTheme(brandVariants)[data-brand="name"] with --brand-10 … --brand-160
useFluent().diruseYugen() → { theme, brand, dir }, or Radix's useDirection()
// Fluent: BrandVariants 10 → 160 passed to createLightTheme / createDarkTheme
const brand: BrandVariants = { 10: "#061724", 20: "#082338", /* … */ 160: "#ebf3fc" };
const theme = createLightTheme(brand);

/* Yugen: the same 16 steps, declared once in tokens.css; light and dark pick the right step */
[data-brand="contoso"] {
  --brand-10: #061724;
  --brand-20: #082338;
  /* … */
  --brand-160: #ebf3fc;
}

Fluent brand ramps (BrandVariants 10 → 160) work unchanged. Yugen's default brand is Sakura; for the original Fluent look, set brand="web".

makeStyles → utilities

Alias token names match Fluent, so every tokens.* has a matching utility. Drop the useStyles hook and write classes directly.

// Fluent UI React v9 with Griffel
import { makeStyles, tokens, typographyStyles } from "@fluentui/react-components";

const useStyles = makeStyles({
  root: {
    display: "flex",
    flexDirection: "column",
    gap: tokens.spacingVerticalS,
    padding: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXL}`,
    backgroundColor: tokens.colorNeutralBackground1,
    borderRadius: tokens.borderRadiusMedium,
    boxShadow: tokens.shadow4,
    ":hover": { backgroundColor: tokens.colorNeutralBackground1Hover },
  },
  title: { ...typographyStyles.subtitle1, color: tokens.colorNeutralForeground1 },
  meta: { ...typographyStyles.caption1, color: tokens.colorNeutralForeground3 },
});

function Tile() {
  const styles = useStyles();
  return (
    <div className={styles.root}>
      <h3 className={styles.title}>Revenue</h3>
      <span className={styles.meta}>Updated 5 minutes ago</span>
    </div>
  );
}

// Yugen UI: the same tokens, written as utilities
function Tile() {
  return (
    <div className="flex flex-col gap-2 rounded-medium bg-background-1 px-5 py-4 shadow-4 hover:bg-background-1-hover">
      <h3 className="text-subtitle1 font-semibold text-foreground-1">Revenue</h3>
      <span className="text-caption1 text-foreground-3">Updated 5 minutes ago</span>
    </div>
  );
}
Fluent tokenYugen utilityNotes
tokens.colorNeutralForeground1 … 4text-foreground-1 … 4
tokens.colorNeutralForegroundOnBrandtext-foreground-on-brand
tokens.colorBrandForegroundLinktext-linkHover: text-link-hover
tokens.colorNeutralBackground1(Hover|Pressed)bg-background-1, hover:bg-background-1-hover…
tokens.colorSubtleBackgroundHoverhover:bg-subtle-hover
tokens.colorBrandBackgroundbg-brand-backgroundBackground2: bg-brand-background-2
tokens.colorNeutralStroke1 / 2 / Accessibleborder-stroke-1 / 2 / accessible
tokens.colorStrokeFocus2outline-stroke-focusAlready applied on :focus-visible
tokens.colorStatusDangerForeground1text-danger-foreground-1Same for success / warning
tokens.colorPaletteTealBackground2bg-teal-bgcranberry, marigold, teal, lavender, forest, steel
tokens.spacingHorizontalM / spacingVerticalMpx-3 / py-3, gap-3One scale for both axes
tokens.spacingHorizontalSNudgepx-1.5The Fluent ramp matches Tailwind's numeric scale
tokens.borderRadiusMedium / Circularrounded-medium / rounded-circular
tokens.shadow4 / 16 / 64shadow-4 / shadow-16 / shadow-64
typographyStyles.body1 / subtitle1text-body1 / text-subtitle1 font-semiboldWeight is a separate class
tokens.fontWeightSemiboldfont-semiboldOnly normal / semibold
tokens.durationNormal, curveDecelerateMidduration-normal ease-decelerate-mid
tokens.fontFamilyMonospacefont-mono

When you need plain CSS (complex selectors, third-party libraries), the color, radius, shadow and motion aliases are CSS variables with the Fluent names: var(--colorNeutralStroke1), var(--borderRadiusMedium), var(--shadow16). Spacing is the exception and merges the two axes: spacingHorizontalM and spacingVerticalM are both var(--spacingM).

Component mapping

Most components keep their names and props (appearance, size, shape, icon, intent). The notes column lists what to change when you migrate.

Fluent v9Yugen UINotes
FluentProviderYugenProviderSee Provider & theme.
ButtonButtonAdds appearance="danger"; as="a" → asChild.
CompoundButton—A Button with two lines of text, or an interactive Card.
MenuButtonMenuMenuTrigger asChildButtonButton with icon={<ChevronDown20Regular />} iconPosition="after".
SplitButtonSplitButtonPass the menu through the menu prop.
ToggleButtonToggleButtonchecked + onCheckedChange, plus checkedIcon.
LinkToolbarLinkToolbarToolbarButtonToolbarToggleButtonToolbarDividerToolbarGroupLink adds external.
FieldLabelInfoLabelFieldLabelInfoLabelSame props; Field also accepts a render-prop.
InputTextareaSearchBoxSpinButtonInputTextareaSearchBoxSpinButtonSame appearance, contentBefore / After; onChange(ev, data) → React's native onChange(ev); SearchBox and SpinButton also have onValueChange(value).
SelectSelectA native <select>, as in Fluent.
DropdownOptionOptionGroupDropdownDropdownOptionDropdownGroupselectedOptions / onOptionSelect → value / onValueChange.
ComboboxOptionComboboxOption children → an options array; supports multiselect, freeform.
TagPickerTagPickerControlTagPickerInputTagPickerListTagPickerOptionTagPickerOne component: options + value / onValueChange.
CheckboxRadioGroupRadioSwitchCheckboxRadioGroupRadioSwitchonChange(ev, data) → onCheckedChange(checked) / onValueChange(value).
SliderSlidervalue is an array of numbers ([40]); ranges with two thumbs are supported.
RatingRatingDisplayRatingRatingDisplay
CalendarDatePicker (compat)CalendarDatePickerDateRangePickerBuilt on react-day-picker v10.
TabListTabTabsTabsListTabsTriggerTabsContentselectedValue / onTabSelect → value / onValueChange; panels included.
BreadcrumbBreadcrumbItemBreadcrumbButtonBreadcrumbDividerBreadcrumbBreadcrumbItemBreadcrumbButtonBreadcrumbDividerAdds an items prop that inserts dividers and collapses extra items into “…”.
NavNavDrawerNavItemNavCategoryNavSubItemNavNavItemNavCategoryNavCategoryItemNavSubItemNavSectionHeaderNo NavDrawer: put Nav inside InlineDrawer / OverlayDrawer.
MenuMenuTriggerMenuPopoverMenuListMenuItem…MenuMenuTriggerMenuPopoverMenuItemMenuItemCheckboxMenuItemRadioNo MenuList: items go directly in MenuPopover; checkedValues → MenuRadioGroup / checked.
AvatarAvatarGroupPersonaAvatarAvatarGroupPersonaAvatar has presence instead of PresenceBadge.
BadgeCounterBadgePresenceBadgeBadgeCounterBadge → Badge with a number; PresenceBadge → Avatar presence / Persona presenceOnly.
TagInteractionTagTagGroupTagInteractionTagTagGroup
TextTitle1…Caption2TextTitle1…Caption2Same preset names.
ImageImageAspectRatio
ListListItemListListItemListItem has media / title / description / actions slots.
Table…useTableFeaturesTable…TableCellLayoutTableSelectionCellYou manage sort / selection state (see the DataTable example on the Table page).
TreeTreeItemTreeItemLayoutTreeTreeItemTreeItemLayout
Carousel…CarouselCarouselViewportCarouselCardCarouselNavContainer
MessageBarMessageBarBodyMessageBarTitleMessageBarActionsMessageBarOne component: title, children, actions, onDismiss, layout.
ToasteruseToastControllerToast…ToasteruseToasttoast()Options are an object (see below).
ProgressBarSpinnerSkeletonSkeletonItemProgressBarSpinnerSkeletonSkeletonItem
CardCardHeaderCardPreviewCardFooterCardCardHeaderCardPreviewCardContentCardFooter
DialogDialogSurfaceDialogBodyDialogTitleDialogContentDialogActionsDialogDialogSurfaceDialogTitleDialogContentDialogActionsNo DialogBody; for modalType="non-modal", use Popover.
OverlayDrawerInlineDrawerDrawerHeader…OverlayDrawerInlineDrawerDrawerHeaderDrawerBodyDrawerFooter
PopoverPopoverSurfacePopoverPopoverSurfacepositioning="above-start" → side="top" align="start".
TooltipTooltipSimpleTooltiprelationship="label" → set aria-label on the trigger yourself.
AccordionAccordionItemAccordionHeaderAccordionPanelAccordionAccordionItemAccordionHeaderAccordionPanelopenItems / onToggle → value / onValueChange, multiple → type="multiple".
DividerDivider
// Fluent UI React v9
<Combobox placeholder="Choose a city" onOptionSelect={(_, data) => setCity(data.optionValue)}>
  <Option value="hn">Hanoi</Option>
  <Option value="dn">Da Nang</Option>
</Combobox>

// Yugen UI: options is an array, the callback gets the value
<Combobox
  placeholder="Choose a city"
  options={[
    { value: "hn", label: "Hanoi" },
    { value: "dn", label: "Da Nang" },
  ]}
  onValueChange={setCity}
/>
// Fluent UI React v9
const toasterId = useId("toaster");
const { dispatchToast } = useToastController(toasterId);
dispatchToast(
  <Toast>
    <ToastTitle>Saved</ToastTitle>
    <ToastBody>Changes take effect immediately.</ToastBody>
  </Toast>,
  { intent: "success" },
);
<Toaster toasterId={toasterId} />

// Yugen UI: options are an object, no JSX needed
const { dispatchToast } = useToast("settings");
dispatchToast({ intent: "success", title: "Saved", body: "Changes take effect immediately." });
<Toaster toasterId="settings" />

// Or the static helper for the default Toaster (place <Toaster /> in YugenProvider)
toast({ intent: "success", title: "Saved" });

Renamed props

General rule: appearance, size, shape, icon and intent stay the same; callbacks follow Radix and receive the value instead of event + data.

Fluent v9Yugen UI
appearance, size, shape, icon, iconPositionUnchanged (size: small / medium / large).
as="a"asChild
onChange={(ev, data) => data.checked}onCheckedChange={(checked) => …}
onChange={(ev, data) => data.value}onValueChange={(value) => …}
onOptionSelect / selectedOptionsonValueChange / value
onTabSelect / selectedValueonValueChange / value
onOpenChange={(ev, data) => data.open}onOpenChange={(open) => …}
positioningside, align, sideOffset, collisionPadding
Slider value: numberSlider value: number[]
mountNodeNone. Portals always render into <body> but take the theme / brand / dir of the nearest nested YugenProvider.

Not in Yugen yet

Some Fluent v9 components and utilities have no equivalent yet. These are the recommended replacements.

Fluent v9Replacement
DataGridTable + the DataTable pattern (filter, sort, row selection, pagination) on the Table page.
Overflow, useOverflowMenuHide items by breakpoint and collect them in a “…” Menu; Breadcrumb has maxDisplayedItems.
TimePicker (compat)A Combobox with a list of times, or <Input type="time">.
TeachingPopoverPopoverSurface appearance="brand" + navigation buttons.
VirtualizerAn external virtualization library (for example @tanstack/react-virtual).
useArrowNavigationGroup, useFocusableGroup (Tabster)Toolbar / ToggleGroup / List have roving focus built in; in your own components, read the direction with useDirection().
makeStyles, mergeClasses, shorthandscn() + Tailwind utilities.
Portal mountNode, nested FluentProvider for portalsNot needed. Floating surfaces follow the nearest nested YugenProvider (usePortalThemeProps()).
Migrating gradually:the two libraries can run side by side, FluentProvider and YugenProvider don't conflict. Move one screen at a time, starting with the one that has the fewest complex components.
Do
  • Look up Fluent token → utility in the table above; the alias names are identical.
  • Change (ev, data) callbacks to the form that receives the value.
  • Set brand="web" to keep the original Fluent blue.
Don't
  • Don't copy hex values from a Fluent theme into className.
  • Don't keep makeStyles next to a migrated component; its styles would come from two sources.
  • Don't forget that Slider takes an array of numbers.