Yugen UI

Từ Fluent UI React v9

Yugen UI giữ tên token, thang đo và phần lớn API của Fluent UI React v9 — chuyển đổi chủ yếu là đổi import và thay makeStyles bằng utility Tailwind.

Khác biệt cốt lõi

Ngôn ngữ thiết kế giống nhau; khác nhau ở cách phân phối, cách viết style và thư viện hành vi bên dưới.

Fluent UI React v9Yugen UI
Phân phối@fluentui/react-componentsGói npm yugen-ui, hoặc chép mã nguồn vào dự án (src/components/ui)
StyleGriffel: makeStyles + tokens (CSS-in-JS lúc chạy / build)Tailwind CSS v4 utility, map 1-1 với cùng tên token
ThemeObject theme (webLightTheme) qua FluentProviderCSS custom property theo data-theme / data-brand qua YugenProvider
Tuỳ biến phần conSlot (root, icon, content…)asChild, prop ReactNode, component ghép, render-prop (xem trang Slot)
Hành vi & a11yCài đặt riêng + TabsterRadix UI, cmdk, react-day-picker
Callback(event, data) => …(value) => …
Icon@fluentui/react-iconsGiống hệt

Provider & theme

FluentProvider → YugenProvider. Theme object trở thành hai attribute.

// 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" (có sẵn trong tokens.css)
createLightTheme(brandVariants)[data-brand="tên"] với --brand-10 … --brand-160
useFluent().diruseYugen() → { theme, brand, dir }, hoặc useDirection() của Radix
// Fluent: BrandVariants 10 → 160 truyền vào createLightTheme / createDarkTheme
const brand: BrandVariants = { 10: "#061724", 20: "#082338", /* … */ 160: "#ebf3fc" };
const theme = createLightTheme(brand);

/* Yugen: cùng 16 bậc, khai báo một lần trong tokens.css — light và dark tự lấy đúng bậc */
[data-brand="contoso"] {
  --brand-10: #061724;
  --brand-20: #082338;
  /* … */
  --brand-160: #ebf3fc;
}

Ramp brand của Fluent (BrandVariants 10 → 160) dùng lại nguyên vẹn. Brand mặc định của Yugen là Sakura; muốn giao diện giống Fluent gốc, đặt brand="web".

makeStyles → utility

Tên alias token trùng Fluent, nên mỗi tokens.* có một utility tương ứng. Bỏ hook useStyles, viết class trực tiếp.

// Fluent UI React v9 — 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}>Doanh thu</h3>
      <span className={styles.meta}>Cập nhật 5 phút trước</span>
    </div>
  );
}

// Yugen UI — cùng token, viết bằng utility
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">Doanh thu</h3>
      <span className="text-caption1 text-foreground-3">Cập nhật 5 phút trước</span>
    </div>
  );
}
Fluent tokenYugen utilityGhi chú
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-focusĐã áp sẵn cho :focus-visible
tokens.colorStatusDangerForeground1text-danger-foreground-1Tương tự success / warning
tokens.colorPaletteTealBackground2bg-teal-bgcranberry, marigold, teal, lavender, forest, steel
tokens.spacingHorizontalM / spacingVerticalMpx-3 / py-3, gap-3Một thang chung cho hai trục
tokens.spacingHorizontalSNudgepx-1.5Ramp Fluent trùng thang số của Tailwind
tokens.borderRadiusMedium / Circularrounded-medium / rounded-circular
tokens.shadow4 / 16 / 64shadow-4 / shadow-16 / shadow-64
typographyStyles.body1 / subtitle1text-body1 / text-subtitle1 font-semiboldWeight tách riêng
tokens.fontWeightSemiboldfont-semiboldChỉ normal / semibold
tokens.durationNormal, curveDecelerateMidduration-normal ease-decelerate-mid
tokens.fontFamilyMonospacefont-mono

Khi cần CSS thuần (selector phức tạp, thư viện bên thứ ba), alias màu, bo góc, shadow, motion vẫn là biến CSS cùng tên Fluent: var(--colorNeutralStroke1), var(--borderRadiusMedium), var(--shadow16). Riêng spacing gộp hai trục: spacingHorizontalM và spacingVerticalM đều là var(--spacingM).

Bảng tương ứng component

Đa số giữ nguyên tên và prop (appearance, size, shape, icon, intent). Cột ghi chú nêu khác biệt cần sửa khi chuyển.

Fluent v9Yugen UIGhi chú
FluentProviderYugenProviderXem phần Provider.
ButtonButtonThêm appearance="danger"; as="a" → asChild.
CompoundButton—Ghép Button + hai dòng chữ, hoặc Card interactive.
MenuButtonMenuMenuTrigger asChildButtonButton với icon={<ChevronDown20Regular />} iconPosition="after".
SplitButtonSplitButtonMenu truyền qua prop menu.
ToggleButtonToggleButtonchecked + onCheckedChange, thêm checkedIcon.
LinkToolbarLinkToolbarToolbarButtonToolbarToggleButtonToolbarDividerToolbarGroupLink thêm external.
FieldLabelInfoLabelFieldLabelInfoLabelGiữ nguyên prop; Field nhận thêm render-prop.
InputTextareaSearchBoxSpinButtonInputTextareaSearchBoxSpinButtonCùng appearance, contentBefore / After; onChange(ev, data) → onChange(ev) gốc của React; SearchBox và SpinButton có thêm onValueChange(value).
SelectSelectThẻ <select> gốc như Fluent.
DropdownOptionOptionGroupDropdownDropdownOptionDropdownGroupselectedOptions / onOptionSelect → value / onValueChange.
ComboboxOptionComboboxOption con → mảng options; có multiselect, freeform.
TagPickerTagPickerControlTagPickerInputTagPickerListTagPickerOptionTagPickerMột component: options + value / onValueChange.
CheckboxRadioGroupRadioSwitchCheckboxRadioGroupRadioSwitchonChange(ev, data) → onCheckedChange(checked) / onValueChange(value).
SliderSlidervalue là mảng số ([40]), hỗ trợ khoảng hai đầu.
RatingRatingDisplayRatingRatingDisplay
CalendarDatePicker (compat)CalendarDatePickerDateRangePickerDựa trên react-day-picker v10.
TabListTabTabsTabsListTabsTriggerTabsContentselectedValue / onTabSelect → value / onValueChange; có sẵn panel.
BreadcrumbBreadcrumbItemBreadcrumbButtonBreadcrumbDividerBreadcrumbBreadcrumbItemBreadcrumbButtonBreadcrumbDividerThêm prop items tự chèn divider và gom mục thừa vào “…”.
NavNavDrawerNavItemNavCategoryNavSubItemNavNavItemNavCategoryNavCategoryItemNavSubItemNavSectionHeaderKhông có NavDrawer — đặt Nav trong InlineDrawer / OverlayDrawer.
MenuMenuTriggerMenuPopoverMenuListMenuItem…MenuMenuTriggerMenuPopoverMenuItemMenuItemCheckboxMenuItemRadioKhông có MenuList — item đặt thẳng trong MenuPopover; checkedValues → MenuRadioGroup / checked.
AvatarAvatarGroupPersonaAvatarAvatarGroupPersonaAvatar có presence thay cho PresenceBadge.
BadgeCounterBadgePresenceBadgeBadgeCounterBadge → Badge với số; PresenceBadge → Avatar presence / Persona presenceOnly.
TagInteractionTagTagGroupTagInteractionTagTagGroup
TextTitle1…Caption2TextTitle1…Caption2Cùng tên preset.
ImageImageAspectRatio
ListListItemListListItemListItem có slot media / title / description / actions.
Table…useTableFeaturesTable…TableCellLayoutTableSelectionCellTự quản state sắp xếp / chọn (xem ví dụ DataTable ở trang Table).
TreeTreeItemTreeItemLayoutTreeTreeItemTreeItemLayout
Carousel…CarouselCarouselViewportCarouselCardCarouselNavContainer
MessageBarMessageBarBodyMessageBarTitleMessageBarActionsMessageBarMột component: title, children, actions, onDismiss, layout.
ToasteruseToastControllerToast…ToasteruseToasttoast()Tuỳ chọn là object (xem bên dưới).
ProgressBarSpinnerSkeletonSkeletonItemProgressBarSpinnerSkeletonSkeletonItem
CardCardHeaderCardPreviewCardFooterCardCardHeaderCardPreviewCardContentCardFooter
DialogDialogSurfaceDialogBodyDialogTitleDialogContentDialogActionsDialogDialogSurfaceDialogTitleDialogContentDialogActionsKhông có DialogBody; modalType="non-modal" → dùng Popover.
OverlayDrawerInlineDrawerDrawerHeader…OverlayDrawerInlineDrawerDrawerHeaderDrawerBodyDrawerFooter
PopoverPopoverSurfacePopoverPopoverSurfacepositioning="above-start" → side="top" align="start".
TooltipTooltipSimpleTooltiprelationship="label" → tự đặt aria-label trên trigger.
AccordionAccordionItemAccordionHeaderAccordionPanelAccordionAccordionItemAccordionHeaderAccordionPanelopenItems / onToggle → value / onValueChange, multiple → type="multiple".
DividerDivider
// Fluent UI React v9
<Combobox placeholder="Chọn thành phố" onOptionSelect={(_, data) => setCity(data.optionValue)}>
  <Option value="hn">Hà Nội</Option>
  <Option value="dn">Đà Nẵng</Option>
</Combobox>

// Yugen UI — options là mảng, callback nhận thẳng giá trị
<Combobox
  placeholder="Chọn thành phố"
  options={[
    { value: "hn", label: "Hà Nội" },
    { value: "dn", label: "Đà Nẵng" },
  ]}
  onValueChange={setCity}
/>
// Fluent UI React v9
const toasterId = useId("toaster");
const { dispatchToast } = useToastController(toasterId);
dispatchToast(
  <Toast>
    <ToastTitle>Đã lưu</ToastTitle>
    <ToastBody>Thay đổi có hiệu lực ngay.</ToastBody>
  </Toast>,
  { intent: "success" },
);
<Toaster toasterId={toasterId} />

// Yugen UI — tuỳ chọn là object, không cần JSX
const { dispatchToast } = useToast("settings");
dispatchToast({ intent: "success", title: "Đã lưu", body: "Thay đổi có hiệu lực ngay." });
<Toaster toasterId="settings" />

// Hoặc helper tĩnh tới Toaster mặc định (đặt <Toaster /> trong YugenProvider)
toast({ intent: "success", title: "Đã lưu" });

Prop đổi tên

Quy tắc chung: appearance, size, shape, icon, intent giữ nguyên; callback chuyển sang kiểu Radix (giá trị thay vì event + data).

Fluent v9Yugen UI
appearance, size, shape, icon, iconPositionGiữ nguyên (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[]
mountNodeKhông có — portal luôn vào <body> nhưng tự nhận theme / brand / dir của YugenProvider lồng.

Chưa có trong Yugen

Một số component và tiện ích của Fluent v9 chưa có bản tương đương. Đây là cách thay thế khuyến nghị.

Fluent v9Thay thế
DataGridTable + mẫu DataTable (lọc, sắp xếp, chọn hàng, phân trang) ở trang Table.
Overflow, useOverflowMenuẨn mục theo breakpoint và gom vào Menu “…”; Breadcrumb có sẵn maxDisplayedItems.
TimePicker (compat)Combobox với danh sách giờ, hoặc <Input type="time">.
TeachingPopoverPopoverSurface appearance="brand" + nút điều hướng.
VirtualizerThư viện ảo hoá bên ngoài (ví dụ @tanstack/react-virtual).
useArrowNavigationGroup, useFocusableGroup (Tabster)Toolbar / ToggleGroup / List có roving focus sẵn; tự viết thì đọc hướng bằng useDirection().
makeStyles, mergeClasses, shorthandscn() + utility Tailwind.
Portal mountNode, FluentProvider lồng cho portalKhông cần — bề mặt nổi tự theo YugenProvider lồng (usePortalThemeProps()).
Mẹo chuyển dần:hai thư viện chạy song song được — FluentProvider và YugenProvider không xung đột. Chuyển từng màn hình, bắt đầu từ màn hình ít component phức tạp nhất.
Nên
  • Tra token Fluent → utility theo bảng trên; tên alias giống hệt.
  • Chuyển callback (ev, data) sang dạng nhận giá trị.
  • Đặt brand="web" nếu muốn giữ màu xanh Fluent gốc.
Không nên
  • Không chép giá trị hex từ theme Fluent vào className.
  • Không giữ makeStyles song song cho component đã chuyển — trùng nguồn style.
  • Không quên Slider nhận mảng số.