Yugen UI

Customizing with slots & composition

Fluent v9 lets you replace parts of a component with slots. Yugen does the same with asChild, ReactNode props, compound components and render-props.

Slots in Yugen

Each Fluent v9 slot concept has a Yugen counterpart. All of them are plain React with no extra API.

Fluent v9Yugen UIExamples
root slot, as propasChildButton, Link, BreadcrumbButton, NavItem; every Radix *Trigger
icon sloticonButton, ToggleButton, Badge, MenuItem, TabsTrigger, AccordionHeader
contentBefore / contentAfter slotsSame nameInput, SearchBox
media slotmediaListItem, DropdownOption, TableCellLayout, TagPicker option
action / actions slotsSame nameCardHeader action, MessageBar actions, ListItem actions, TreeItemLayout actions
Sub-components (CardHeader, DialogBody…)Compound componentsCard*, Dialog*, Drawer*, Table*, Menu*
Slot that takes a render functionRender-prop childrenField: (props) => <Control {...props} />
className of a child slotrootClassName, listboxClassName…Input, Slider, InfoLabel, Dropdown, Combobox, Select

asChild: change the root element

asChild uses the Radix Slot. The component does not render its own element; it merges className, ref, events and aria-* into the single child element you pass.

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

{/* Button renders as <a> but keeps its style, ref and events */}
<Button asChild appearance="primary">
  <a href="#/concepts/theming">
    Read about Theming
    <ArrowRight16Regular className="rtl:-scale-x-100" />
  </a>
</Button>

{/* The Radix trigger merges open behavior and aria-* into your Button */}
<Popover>
  <PopoverTrigger asChild>
    <Button icon={<Share20Regular />}>Share</Button>
  </PopoverTrigger>
  <PopoverSurface>Link copied.</PopoverSurface>
</Popover>
  • The child must be one element that forwards its ref and spreads props onto the DOM.
  • With <Button asChild>, the icon prop is ignored (Button only renders its children). Put the icon inside the child element, as in the example above.
  • Use asChild to wrap router links: <BreadcrumbButton asChild><RouterLink to="/">…</RouterLink></BreadcrumbButton>.

ReactNode props: icon, contentBefore, media, actions

Instead of slot objects, secondary areas take a ReactNode directly. The component handles alignment, spacing and icon size.

/
12
<Button icon={<Add20Regular />}>New</Button>
<Button icon={<ArrowRight16Regular className="rtl:-scale-x-100" />} iconPosition="after">Continue</Button>
<Button appearance="subtle" icon={<MoreHorizontal20Regular />} aria-label="More" />

<Input
  aria-label="Search files"
  placeholder="Search files"
  contentBefore={<Search20Regular />}
  contentAfter={<Kbd>/</Kbd>}
  rootClassName="w-64"
/>

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

<Menu>
  <MenuTrigger asChild>
    <Button>Actions</Button>
  </MenuTrigger>
  <MenuPopover>
    <MenuItem icon={<Edit20Regular />} secondaryContent="Ctrl+E">Edit</MenuItem>
    <MenuItem icon={<Copy20Regular />} secondaryContent="Ctrl+D">Duplicate</MenuItem>
    <MenuDivider />
    <MenuItem icon={<Delete20Regular />}>Delete</MenuItem>
  </MenuPopover>
</Menu>
  • Olivia BennettEdited Q3 plan
<List className="w-full max-w-md">
  <ListItem
    media={<Avatar name="Olivia Bennett" color="colorful" />}
    title="Olivia Bennett"
    description="Edited Q3 plan"
    actions={<Button size="small" appearance="subtle" icon={<MoreHorizontal20Regular />} aria-label="More actions" />}
  />
</List>

<Dropdown placeholder="Choose a folder" aria-label="Folder">
  <DropdownOption value="docs" media={<Folder20Regular />}>Documents</DropdownOption>
  <DropdownOption value="design" media={<Folder20Regular />}>Design</DropdownOption>
</Dropdown>

The component sets the icon size with [&_svg]:size-* (small 16, medium 20, large 24), so pass a size 20 Fluent icon and let the component scale it. A decorative icon needs no aria-label; an icon-only button must have one.

Compound components

Card, Dialog, Drawer, Table and Menu are split into small parts. Leave out the parts you don't need, reorder them or put your own elements between them.

Sakura team
Updated 5 minutes ago
The draft of the new home page is ready for feedback.
DesignDraft
import { Avatar, Badge, Button, Card, CardContent, CardFooter, CardHeader } from "@/components/ui";

<Card className="w-full max-w-sm">
  <CardHeader
    image={<Avatar name="Sakura team" color="brand" />}
    header="Sakura team"
    description="Updated 5 minutes ago"
    action={<Button appearance="subtle" icon={<MoreHorizontal20Regular />} aria-label="More actions" />}
  />
  <CardContent>The draft of the new home page is ready for feedback.</CardContent>
  {/* Put any element between the parts; you decide the order */}
  <div className="flex gap-1">
    <Badge appearance="tint" color="brand">Design</Badge>
    <Badge appearance="tint" color="informative">Draft</Badge>
  </div>
  <CardFooter>
    <Button appearance="primary">Open</Button>
    <Button>Dismiss</Button>
  </CardFooter>
</Card>
Component familyParts
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-props in Field

Field connects the label, hint and validation to the control. Pass an element when the control takes the id directly; pass a function when you need to decide where the props go.

Shown in the title bar.
You can change this later in Settings.
{/* 1. Element: Field clones it and sets id, aria-describedby, aria-invalid, required, disabled */}
<Field label="Project name" hint="Shown in the title bar." required>
  <Input placeholder="Yugen Docs" />
</Field>

{/* 2. Render-prop: you decide where the props go */}
<Field label="Access" hint="You can change this later in Settings.">
  {(props) => (
    <RadioGroup {...props} defaultValue="team" layout="horizontal">
      <Radio value="private" label="Private" />
      <Radio value="team" label="Team" />
    </RadioGroup>
  )}
</Field>
Prop passed by FieldValue
idThe Field's id prop or a generated id (useId). The label points to it with htmlFor.
aria-describedbyThe ids of validationMessage and hint (when present).
aria-invalidtrue when validationState="error".
required, disabledTaken from the Field props of the same name.

With an element, props set directly on the control win over the Field props (Field merges in the order { ...fieldProps, ...child.props }). Note that <label for> cannot name a role="radiogroup". For choice groups, see how to label a group on Labelling components.

rootClassName & className

Components with several DOM layers let you target the right one. The general rule: className goes on the main visible part, rootClassName on the outer wrapper.

ComponentclassNameExtra prop
Input, SearchBoxThe input frame (border, background, focus bar)rootClassName: the outer block with label and hint; set width / margin here
SliderRadix root (rail and thumb)rootClassName: the outer block with label and value
InfoLabelThe label elementrootClassName: the span around the label and the (i) button
SelectThe outer frame (set width here)selectClassName: the <select> element itself
TextareaThe outer frame (set width here)textareaClassName: the <textarea> element itself
ProgressBarThe progress barrootClassName: the outer block when there is a label / description
Dropdown, Combobox, TagPickerTrigger / inputlistboxClassName: the listbox surface in the portal
PopoverSurface, MenuPopover, TooltipContentThe floating surface in the portal—
<Input rootClassName="w-72" className="bg-background-2" placeholder="Gray frame, 288px wide block" />
<Slider rootClassName="max-w-xs" label="Volume" defaultValue={[40]} />
<Select className="w-48" selectClassName="font-semibold">…</Select>
<Dropdown className="w-56" listboxClassName="max-h-60">…</Dropdown>
Do
  • Use asChild to change the root element (a link, a router link) instead of adding a wrapper.
  • Pass a size 20 Fluent icon to the icon / media prop and let the component scale it.
  • Use the Field render-prop when the control does not take the id on its root element.
Don't
  • Passing more than one child to an asChild element.
  • Setting width in the className of an Input that has a label. Use rootClassName.
  • Copying a component's inner markup to add an icon. Use the existing prop.