Yugen UI

Tuỳ biến bằng slot & composition

Fluent v9 cho thay từng phần của component bằng slot; Yugen đạt cùng mục tiêu bằng asChild, prop nhận ReactNode, component ghép và render-prop.

Slot trong Yugen

Mỗi khái niệm slot của Fluent v9 có một cách tương ứng trong Yugen — đều là React thuần, không cần API riêng.

Fluent v9Yugen UIVí dụ
Slot root, prop asasChildButton, Link, BreadcrumbButton, NavItem; mọi *Trigger của Radix
Slot iconiconButton, ToggleButton, Badge, MenuItem, TabsTrigger, AccordionHeader
Slot contentBefore / contentAfterCùng tênInput, SearchBox
Slot mediamediaListItem, DropdownOption, TableCellLayout, TagPicker option
Slot action / actionsCùng tênCardHeader action, MessageBar actions, ListItem actions, TreeItemLayout actions
Sub-component (CardHeader, DialogBody…)Component ghépCard*, Dialog*, Drawer*, Table*, Menu*
Slot nhận hàm renderRender-prop childrenField: (props) => <Control {...props} />
className của slot conrootClassName, listboxClassName…Input, Slider, InfoLabel, Dropdown, Combobox, Select

asChild — đổi phần tử gốc

asChild dùng Slot của Radix: component không render thẻ của nó mà gộp className, ref, sự kiện và aria-* vào đúng một phần tử con bạn truyền vào.

import { ArrowRight16Regular, Share20Regular } from "@fluentui/react-icons";
import { Button, Popover, PopoverSurface, PopoverTrigger } from "@/components/ui";

{/* Button render thành <a> nhưng giữ nguyên style, ref, sự kiện */}
<Button asChild appearance="primary">
  <a href="#/concepts/theming">
    Đọc về Theming
    <ArrowRight16Regular className="rtl:-scale-x-100" />
  </a>
</Button>

{/* Trigger của Radix gộp hành vi mở / aria-* vào Button của bạn */}
<Popover>
  <PopoverTrigger asChild>
    <Button icon={<Share20Regular />}>Chia sẻ</Button>
  </PopoverTrigger>
  <PopoverSurface>Liên kết đã được sao chép.</PopoverSurface>
</Popover>
  • Con phải là một phần tử duy nhất, forward ref và trải (spread) props xuống DOM.
  • Với <Button asChild>, prop icon bị bỏ qua (Button chỉ render children) — đặt icon bên trong phần tử con như ví dụ trên.
  • Dùng asChild để bọc link của router: <BreadcrumbButton asChild><RouterLink to="/">…</RouterLink></BreadcrumbButton>.

Prop nhận ReactNode: icon, contentBefore, media, actions

Thay vì slot object, các vùng phụ nhận thẳng ReactNode. Component lo căn chỉnh, khoảng cách và cỡ icon.

/
12
<Button icon={<Add20Regular />}>Tạo mới</Button>
<Button icon={<ArrowRight16Regular className="rtl:-scale-x-100" />} iconPosition="after">Tiếp tục</Button>
<Button appearance="subtle" icon={<MoreHorizontal20Regular />} aria-label="Thêm" />

<Input
  aria-label="Tìm tệp"
  placeholder="Tìm tệp"
  contentBefore={<Search20Regular />}
  contentAfter={<Kbd>/</Kbd>}
  rootClassName="w-64"
/>

<Badge icon={<Document20Regular />}>12</Badge>

<Menu>
  <MenuTrigger asChild>
    <Button>Thao tác</Button>
  </MenuTrigger>
  <MenuPopover>
    <MenuItem icon={<Edit20Regular />} secondaryContent="Ctrl+E">Sửa</MenuItem>
    <MenuItem icon={<Copy20Regular />} secondaryContent="Ctrl+D">Nhân bản</MenuItem>
    <MenuDivider />
    <MenuItem icon={<Delete20Regular />}>Xoá</MenuItem>
  </MenuPopover>
</Menu>
  • Phạm Thu HàĐã chỉnh sửa Kế hoạch quý 3
<List className="w-full max-w-md">
  <ListItem
    media={<Avatar name="Phạm Thu Hà" color="colorful" />}
    title="Phạm Thu Hà"
    description="Đã chỉnh sửa Kế hoạch quý 3"
    actions={<Button size="small" appearance="subtle" icon={<MoreHorizontal20Regular />} aria-label="Thêm thao tác" />}
  />
</List>

<Dropdown placeholder="Chọn thư mục" aria-label="Thư mục">
  <DropdownOption value="docs" media={<Folder20Regular />}>Tài liệu</DropdownOption>
  <DropdownOption value="design" media={<Folder20Regular />}>Thiết kế</DropdownOption>
</Dropdown>

Cỡ icon do component đặt bằng [&_svg]:size-* (small 16, medium 20, large 24), nên chỉ cần truyền icon Fluent cỡ 20 và để component co giãn. Icon chỉ có nghĩa trang trí không cần aria-label; nút chỉ có icon thì bắt buộc có.

Component ghép (compound)

Card, Dialog, Drawer, Table, Menu được chia thành nhiều phần nhỏ. Bỏ phần không cần, đổi thứ tự, hoặc chèn phần tử của bạn vào giữa.

Nhóm Sakura
Cập nhật 5 phút trước
Bản phác thảo trang chủ mới đã sẵn sàng để góp ý.
Thiết kếBản nháp
import { Avatar, Badge, Button, Card, CardContent, CardFooter, CardHeader } from "@/components/ui";

<Card className="w-full max-w-sm">
  <CardHeader
    image={<Avatar name="Nhóm Sakura" color="brand" />}
    header="Nhóm Sakura"
    description="Cập nhật 5 phút trước"
    action={<Button appearance="subtle" icon={<MoreHorizontal20Regular />} aria-label="Thêm thao tác" />}
  />
  <CardContent>Bản phác thảo trang chủ mới đã sẵn sàng để góp ý.</CardContent>
  {/* Chèn bất kỳ phần tử nào giữa các phần — thứ tự do bạn quyết định */}
  <div className="flex gap-1">
    <Badge appearance="tint" color="brand">Thiết kế</Badge>
    <Badge appearance="tint" color="informative">Bản nháp</Badge>
  </div>
  <CardFooter>
    <Button appearance="primary">Mở</Button>
    <Button>Bỏ qua</Button>
  </CardFooter>
</Card>
Họ componentCác phần
CardCard · CardHeader (image, header, description, action) · CardPreview · CardContent · CardFooter
DialogDialog · DialogTrigger · DialogSurface (modalType) · DialogTitle (action) · DialogDescription · DialogContent · DialogActions · DialogClose
DrawerDrawer / OverlayDrawer / InlineDrawer · DrawerSurface · DrawerHeader · DrawerHeaderTitle (action) · DrawerBody · DrawerFooter
TableTable · TableHeader · TableHeaderCell · TableBody · TableRow · TableCell · TableCellLayout · TableCellActions · TableSelectionCell
MenuMenu · MenuTrigger · MenuPopover · MenuItem · MenuItemCheckbox · MenuItemRadio · MenuGroupHeader · MenuDivider · MenuSub…

Render-prop trong Field

Field nối nhãn, gợi ý và validation với control. Truyền element khi control nhận id trực tiếp; truyền hàm khi bạn cần quyết định props đi vào đâu.

Hiển thị trên thanh tiêu đề.
Có thể đổi sau trong Cài đặt.
{/* 1. Element: Field clone và gắn id, aria-describedby, aria-invalid, required, disabled */}
<Field label="Tên dự án" hint="Hiển thị trên thanh tiêu đề." required>
  <Input placeholder="Yugen Docs" />
</Field>

{/* 2. Render-prop: tự quyết định props đi vào đâu */}
<Field label="Quyền truy cập" hint="Có thể đổi sau trong Cài đặt.">
  {(props) => (
    <RadioGroup {...props} defaultValue="team" layout="horizontal">
      <Radio value="private" label="Riêng tư" />
      <Radio value="team" label="Trong nhóm" />
    </RadioGroup>
  )}
</Field>
Prop Field truyền xuốngGiá trị
idProp id của Field hoặc id tự sinh (useId) — label trỏ tới bằng htmlFor.
aria-describedbyid của validationMessage và hint (nếu có).
aria-invalidtrue khi validationState="error".
required, disabledLấy từ prop cùng tên của Field.

Với element, props đặt trực tiếp trên control thắng props của Field (Field gộp theo thứ tự { ...fieldProps, ...child.props }). Lưu ý <label for> không đặt tên được cho role="radiogroup" — với nhóm lựa chọn, xem cách gắn nhãn nhóm ở trang Gắn nhãn component.

rootClassName & className

Component có nhiều lớp DOM cho bạn nhắm style vào đúng lớp. Quy tắc chung: className vào phần nhìn thấy chính, rootClassName vào khối bao ngoài.

ComponentclassNameProp phụ
Input, SearchBoxKhung ô nhập (viền, nền, thanh focus)rootClassName — khối ngoài gồm label + hint; đặt width / margin ở đây
SliderRadix root (rail + thumb)rootClassName — khối ngoài gồm label + giá trị
InfoLabelThẻ labelrootClassName — span bọc label + nút (i)
SelectKhung ngoài (đặt width ở đây)selectClassName — chính thẻ <select>
TextareaKhung ngoài (đặt width ở đây)textareaClassName — chính thẻ <textarea>
ProgressBarThanh tiến độrootClassName — khối ngoài khi có label / description
Dropdown, Combobox, TagPickerTrigger / ô nhậplistboxClassName — bề mặt listbox trong portal
PopoverSurface, MenuPopover, TooltipContentBề mặt nổi trong portal—
<Input rootClassName="w-72" className="bg-background-2" placeholder="Khung nền xám, khối rộng 288px" />
<Slider rootClassName="max-w-xs" label="Âm lượng" defaultValue={[40]} />
<Select className="w-48" selectClassName="font-semibold">…</Select>
<Dropdown className="w-56" listboxClassName="max-h-60">…</Dropdown>
Nên
  • Dùng asChild để đổi thẻ gốc (link, link của router) thay vì bọc thêm một lớp.
  • Truyền icon Fluent cỡ 20 vào prop icon / media và để component tự co giãn.
  • Dùng render-prop của Field khi control không nhận id ở phần tử gốc.
Không nên
  • Không truyền nhiều hơn một con cho phần tử asChild.
  • Không đặt width lên className của Input khi có label — dùng rootClassName.
  • Không chép lại markup bên trong component để thêm một icon — dùng prop có sẵn.