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 v9 | Yugen UI | |
|---|---|---|
| Phân phối | @fluentui/react-components | Gói npm yugen-ui, hoặc chép mã nguồn vào dự án (src/components/ui) |
| Style | Griffel: makeStyles + tokens (CSS-in-JS lúc chạy / build) | Tailwind CSS v4 utility, map 1-1 với cùng tên token |
| Theme | Object theme (webLightTheme) qua FluentProvider | CSS custom property theo data-theme / data-brand qua YugenProvider |
| Tuỳ biến phần con | Slot (root, icon, content…) | asChild, prop ReactNode, component ghép, render-prop (xem trang Slot) |
| Hành vi & a11y | Cài đặt riêng + Tabster | Radix UI, cmdk, react-day-picker |
| Callback | (event, data) => … | (value) => … |
| Icon | @fluentui/react-icons | Giố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 v9 | Yugen UI |
|---|---|
webLightTheme | theme="light" brand="web" |
webDarkTheme | theme="dark" brand="web" |
teamsLightTheme / teamsDarkTheme | brand="teams" |
teamsHighContrastTheme | theme="high-contrast" (có sẵn trong tokens.css) |
createLightTheme(brandVariants) | [data-brand="tên"] với --brand-10 … --brand-160 |
useFluent().dir | useYugen() → { 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 token | Yugen utility | Ghi chú |
|---|---|---|
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 | Đã áp sẵn cho :focus-visible |
tokens.colorStatusDangerForeground1 | text-danger-foreground-1 | Tương tự success / warning |
tokens.colorPaletteTealBackground2 | bg-teal-bg | cranberry, marigold, teal, lavender, forest, steel |
tokens.spacingHorizontalM / spacingVerticalM | px-3 / py-3, gap-3 | Một thang chung cho hai trục |
tokens.spacingHorizontalSNudge | px-1.5 | Ramp Fluent trùng thang số của Tailwind |
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 tách riêng |
tokens.fontWeightSemibold | font-semibold | Chỉ normal / semibold |
tokens.durationNormal, curveDecelerateMid | duration-normal ease-decelerate-mid | |
tokens.fontFamilyMonospace | font-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 v9 | Yugen UI | Ghi chú |
|---|---|---|
FluentProvider | YugenProvider | Xem phần Provider. |
Button | Button | Thêm appearance="danger"; as="a" → asChild. |
CompoundButton | — | Ghép Button + hai dòng chữ, hoặc Card interactive. |
MenuButton | MenuMenuTrigger asChildButton | Button với icon={<ChevronDown20Regular />} iconPosition="after". |
SplitButton | SplitButton | Menu truyền qua prop menu. |
ToggleButton | ToggleButton | checked + onCheckedChange, thêm checkedIcon. |
LinkToolbar | LinkToolbarToolbarButtonToolbarToggleButtonToolbarDividerToolbarGroup | Link thêm external. |
FieldLabelInfoLabel | FieldLabelInfoLabel | Giữ nguyên prop; Field nhận thêm render-prop. |
InputTextareaSearchBoxSpinButton | InputTextareaSearchBoxSpinButton | Cùng appearance, contentBefore / After; onChange(ev, data) → onChange(ev) gốc của React; SearchBox và SpinButton có thêm onValueChange(value). |
Select | Select | Thẻ <select> gốc như Fluent. |
DropdownOptionOptionGroup | DropdownDropdownOptionDropdownGroup | selectedOptions / onOptionSelect → value / onValueChange. |
ComboboxOption | Combobox | Option con → mảng options; có multiselect, freeform. |
TagPickerTagPickerControlTagPickerInputTagPickerListTagPickerOption | TagPicker | Một component: options + value / onValueChange. |
CheckboxRadioGroupRadioSwitch | CheckboxRadioGroupRadioSwitch | onChange(ev, data) → onCheckedChange(checked) / onValueChange(value). |
Slider | Slider | value là mảng số ([40]), hỗ trợ khoảng hai đầu. |
RatingRatingDisplay | RatingRatingDisplay | |
CalendarDatePicker (compat) | CalendarDatePickerDateRangePicker | Dựa trên react-day-picker v10. |
TabListTab | TabsTabsListTabsTriggerTabsContent | selectedValue / onTabSelect → value / onValueChange; có sẵn panel. |
BreadcrumbBreadcrumbItemBreadcrumbButtonBreadcrumbDivider | BreadcrumbBreadcrumbItemBreadcrumbButtonBreadcrumbDivider | Thêm prop items tự chèn divider và gom mục thừa vào “…”. |
NavNavDrawerNavItemNavCategoryNavSubItem | NavNavItemNavCategoryNavCategoryItemNavSubItemNavSectionHeader | Không có NavDrawer — đặt Nav trong InlineDrawer / OverlayDrawer. |
MenuMenuTriggerMenuPopoverMenuListMenuItem… | MenuMenuTriggerMenuPopoverMenuItemMenuItemCheckboxMenuItemRadio | Không có MenuList — item đặt thẳng trong MenuPopover; checkedValues → MenuRadioGroup / checked. |
AvatarAvatarGroupPersona | AvatarAvatarGroupPersona | Avatar có presence thay cho PresenceBadge. |
BadgeCounterBadgePresenceBadge | Badge | CounterBadge → Badge với số; PresenceBadge → Avatar presence / Persona presenceOnly. |
TagInteractionTagTagGroup | TagInteractionTagTagGroup | |
TextTitle1…Caption2 | TextTitle1…Caption2 | Cùng tên preset. |
Image | ImageAspectRatio | |
ListListItem | ListListItem | ListItem có slot media / title / description / actions. |
Table…useTableFeatures | Table…TableCellLayoutTableSelectionCell | Tự quản state sắp xếp / chọn (xem ví dụ DataTable ở trang Table). |
TreeTreeItemTreeItemLayout | TreeTreeItemTreeItemLayout | |
Carousel… | CarouselCarouselViewportCarouselCardCarouselNavContainer | |
MessageBarMessageBarBodyMessageBarTitleMessageBarActions | MessageBar | Một component: title, children, actions, onDismiss, layout. |
ToasteruseToastControllerToast… | ToasteruseToasttoast() | Tuỳ chọn là object (xem bên dưới). |
ProgressBarSpinnerSkeletonSkeletonItem | ProgressBarSpinnerSkeletonSkeletonItem | |
CardCardHeaderCardPreviewCardFooter | CardCardHeaderCardPreviewCardContentCardFooter | |
DialogDialogSurfaceDialogBodyDialogTitleDialogContentDialogActions | DialogDialogSurfaceDialogTitleDialogContentDialogActions | Không có DialogBody; modalType="non-modal" → dùng Popover. |
OverlayDrawerInlineDrawerDrawerHeader… | OverlayDrawerInlineDrawerDrawerHeaderDrawerBodyDrawerFooter | |
PopoverPopoverSurface | PopoverPopoverSurface | positioning="above-start" → side="top" align="start". |
Tooltip | TooltipSimpleTooltip | relationship="label" → tự đặt aria-label trên trigger. |
AccordionAccordionItemAccordionHeaderAccordionPanel | AccordionAccordionItemAccordionHeaderAccordionPanel | openItems / onToggle → value / onValueChange, multiple → type="multiple". |
Divider | Divider |
// 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 v9 | Yugen UI |
|---|---|
appearance, size, shape, icon, iconPosition | Giữ nguyên (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 | Khô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 v9 | Thay thế |
|---|---|
DataGrid | Table + 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">. |
TeachingPopover | PopoverSurface appearance="brand" + nút điều hướng. |
Virtualizer | Thư 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, shorthands | cn() + utility Tailwind. |
Portal mountNode, FluentProvider lồng cho portal | Không cần — bề mặt nổi tự theo YugenProvider lồng (usePortalThemeProps()). |
- 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 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ố.