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 v9 | Yugen UI | |
|---|---|---|
| Distribution | @fluentui/react-components | The yugen-ui npm package, or the source copied into your project (src/components/ui) |
| Styling | Griffel: makeStyles + tokens (CSS-in-JS at runtime / build time) | Tailwind CSS v4 utilities, mapped 1:1 to the same token names |
| Theme | Theme object (webLightTheme) through FluentProvider | CSS custom properties per data-theme / data-brand through YugenProvider |
| Customizing parts | Slots (root, icon, content…) | asChild, ReactNode props, compound components, render-props (see Slots) |
| Behavior & a11y | Own implementation + Tabster | Radix UI, cmdk, react-day-picker |
| Callbacks | (event, data) => … | (value) => … |
| Icons | @fluentui/react-icons | The 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 v9 | Yugen UI |
|---|---|
webLightTheme | theme="light" brand="web" |
webDarkTheme | theme="dark" brand="web" |
teamsLightTheme / teamsDarkTheme | brand="teams" |
teamsHighContrastTheme | theme="high-contrast" (included in tokens.css) |
createLightTheme(brandVariants) | [data-brand="name"] with --brand-10 … --brand-160 |
useFluent().dir | useYugen() → { 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 token | Yugen utility | Notes |
|---|---|---|
tokens.colorNeutralForeground1 … 4 | text-foreground-1 … 4 | |
tokens.colorNeutralForegroundOnBrand | text-foreground-on-brand | |
tokens.colorBrandForegroundLink | text-link | Hover: text-link-hover |
tokens.colorNeutralBackground1(Hover|Pressed) | bg-background-1, hover:bg-background-1-hover… | |
tokens.colorSubtleBackgroundHover | hover:bg-subtle-hover | |
tokens.colorBrandBackground | bg-brand-background | Background2: bg-brand-background-2 |
tokens.colorNeutralStroke1 / 2 / Accessible | border-stroke-1 / 2 / accessible | |
tokens.colorStrokeFocus2 | outline-stroke-focus | Already applied on :focus-visible |
tokens.colorStatusDangerForeground1 | text-danger-foreground-1 | Same for success / warning |
tokens.colorPaletteTealBackground2 | bg-teal-bg | cranberry, marigold, teal, lavender, forest, steel |
tokens.spacingHorizontalM / spacingVerticalM | px-3 / py-3, gap-3 | One scale for both axes |
tokens.spacingHorizontalSNudge | px-1.5 | The Fluent ramp matches Tailwind's numeric scale |
tokens.borderRadiusMedium / Circular | rounded-medium / rounded-circular | |
tokens.shadow4 / 16 / 64 | shadow-4 / shadow-16 / shadow-64 | |
typographyStyles.body1 / subtitle1 | text-body1 / text-subtitle1 font-semibold | Weight is a separate class |
tokens.fontWeightSemibold | font-semibold | Only normal / semibold |
tokens.durationNormal, curveDecelerateMid | duration-normal ease-decelerate-mid | |
tokens.fontFamilyMonospace | font-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 v9 | Yugen UI | Notes |
|---|---|---|
FluentProvider | YugenProvider | See Provider & theme. |
Button | Button | Adds appearance="danger"; as="a" → asChild. |
CompoundButton | — | A Button with two lines of text, or an interactive Card. |
MenuButton | MenuMenuTrigger asChildButton | Button with icon={<ChevronDown20Regular />} iconPosition="after". |
SplitButton | SplitButton | Pass the menu through the menu prop. |
ToggleButton | ToggleButton | checked + onCheckedChange, plus checkedIcon. |
LinkToolbar | LinkToolbarToolbarButtonToolbarToggleButtonToolbarDividerToolbarGroup | Link adds external. |
FieldLabelInfoLabel | FieldLabelInfoLabel | Same props; Field also accepts a render-prop. |
InputTextareaSearchBoxSpinButton | InputTextareaSearchBoxSpinButton | Same appearance, contentBefore / After; onChange(ev, data) → React's native onChange(ev); SearchBox and SpinButton also have onValueChange(value). |
Select | Select | A native <select>, as in Fluent. |
DropdownOptionOptionGroup | DropdownDropdownOptionDropdownGroup | selectedOptions / onOptionSelect → value / onValueChange. |
ComboboxOption | Combobox | Option children → an options array; supports multiselect, freeform. |
TagPickerTagPickerControlTagPickerInputTagPickerListTagPickerOption | TagPicker | One component: options + value / onValueChange. |
CheckboxRadioGroupRadioSwitch | CheckboxRadioGroupRadioSwitch | onChange(ev, data) → onCheckedChange(checked) / onValueChange(value). |
Slider | Slider | value is an array of numbers ([40]); ranges with two thumbs are supported. |
RatingRatingDisplay | RatingRatingDisplay | |
CalendarDatePicker (compat) | CalendarDatePickerDateRangePicker | Built on react-day-picker v10. |
TabListTab | TabsTabsListTabsTriggerTabsContent | selectedValue / onTabSelect → value / onValueChange; panels included. |
BreadcrumbBreadcrumbItemBreadcrumbButtonBreadcrumbDivider | BreadcrumbBreadcrumbItemBreadcrumbButtonBreadcrumbDivider | Adds an items prop that inserts dividers and collapses extra items into “…”. |
NavNavDrawerNavItemNavCategoryNavSubItem | NavNavItemNavCategoryNavCategoryItemNavSubItemNavSectionHeader | No NavDrawer: put Nav inside InlineDrawer / OverlayDrawer. |
MenuMenuTriggerMenuPopoverMenuListMenuItem… | MenuMenuTriggerMenuPopoverMenuItemMenuItemCheckboxMenuItemRadio | No MenuList: items go directly in MenuPopover; checkedValues → MenuRadioGroup / checked. |
AvatarAvatarGroupPersona | AvatarAvatarGroupPersona | Avatar has presence instead of PresenceBadge. |
BadgeCounterBadgePresenceBadge | Badge | CounterBadge → Badge with a number; PresenceBadge → Avatar presence / Persona presenceOnly. |
TagInteractionTagTagGroup | TagInteractionTagTagGroup | |
TextTitle1…Caption2 | TextTitle1…Caption2 | Same preset names. |
Image | ImageAspectRatio | |
ListListItem | ListListItem | ListItem has media / title / description / actions slots. |
Table…useTableFeatures | Table…TableCellLayoutTableSelectionCell | You manage sort / selection state (see the DataTable example on the Table page). |
TreeTreeItemTreeItemLayout | TreeTreeItemTreeItemLayout | |
Carousel… | CarouselCarouselViewportCarouselCardCarouselNavContainer | |
MessageBarMessageBarBodyMessageBarTitleMessageBarActions | MessageBar | One component: title, children, actions, onDismiss, layout. |
ToasteruseToastControllerToast… | ToasteruseToasttoast() | Options are an object (see below). |
ProgressBarSpinnerSkeletonSkeletonItem | ProgressBarSpinnerSkeletonSkeletonItem | |
CardCardHeaderCardPreviewCardFooter | CardCardHeaderCardPreviewCardContentCardFooter | |
DialogDialogSurfaceDialogBodyDialogTitleDialogContentDialogActions | DialogDialogSurfaceDialogTitleDialogContentDialogActions | No DialogBody; for modalType="non-modal", use Popover. |
OverlayDrawerInlineDrawerDrawerHeader… | OverlayDrawerInlineDrawerDrawerHeaderDrawerBodyDrawerFooter | |
PopoverPopoverSurface | PopoverPopoverSurface | positioning="above-start" → side="top" align="start". |
Tooltip | TooltipSimpleTooltip | relationship="label" → set aria-label on the trigger yourself. |
AccordionAccordionItemAccordionHeaderAccordionPanel | AccordionAccordionItemAccordionHeaderAccordionPanel | openItems / onToggle → value / onValueChange, multiple → type="multiple". |
Divider | Divider |
// 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 v9 | Yugen UI |
|---|---|
appearance, size, shape, icon, iconPosition | Unchanged (size: small / medium / large). |
as="a" | asChild |
onChange={(ev, data) => data.checked} | onCheckedChange={(checked) => …} |
onChange={(ev, data) => data.value} | onValueChange={(value) => …} |
onOptionSelect / selectedOptions | onValueChange / value |
onTabSelect / selectedValue | onValueChange / value |
onOpenChange={(ev, data) => data.open} | onOpenChange={(open) => …} |
positioning | side, align, sideOffset, collisionPadding |
Slider value: number | Slider value: number[] |
mountNode | None. 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 v9 | Replacement |
|---|---|
DataGrid | Table + the DataTable pattern (filter, sort, row selection, pagination) on the Table page. |
Overflow, useOverflowMenu | Hide 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">. |
TeachingPopover | PopoverSurface appearance="brand" + navigation buttons. |
Virtualizer | An 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, shorthands | cn() + Tailwind utilities. |
Portal mountNode, nested FluentProvider for portals | Not needed. Floating surfaces follow the nearest nested YugenProvider (usePortalThemeProps()). |
- 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 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.