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 v9 | Yugen UI | Examples |
|---|---|---|
root slot, as prop | asChild | Button, Link, BreadcrumbButton, NavItem; every Radix *Trigger |
icon slot | icon | Button, ToggleButton, Badge, MenuItem, TabsTrigger, AccordionHeader |
contentBefore / contentAfter slots | Same name | Input, SearchBox |
media slot | media | ListItem, DropdownOption, TableCellLayout, TagPicker option |
action / actions slots | Same name | CardHeader action, MessageBar actions, ListItem actions, TreeItemLayout actions |
| Sub-components (CardHeader, DialogBody…) | Compound components | Card*, Dialog*, Drawer*, Table*, Menu* |
| Slot that takes a render function | Render-prop children | Field: (props) => <Control {...props} /> |
| className of a child slot | rootClassName, 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>, theiconprop 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.
<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.
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 family | Parts |
|---|---|
| Card | Card · CardHeader (image, header, description, action) · CardPreview · CardContent · CardFooter |
| Dialog | Dialog · DialogTrigger · DialogSurface (modalType) · DialogTitle (action) · DialogDescription · DialogContent · DialogActions · DialogClose |
| Drawer | Drawer / OverlayDrawer / InlineDrawer · DrawerSurface · DrawerHeader · DrawerHeaderTitle (action) · DrawerBody · DrawerFooter |
| Table | Table · TableHeader · TableHeaderCell · TableBody · TableRow · TableCell · TableCellLayout · TableCellActions · TableSelectionCell |
| Menu | Menu · 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.
{/* 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 Field | Value |
|---|---|
id | The Field's id prop or a generated id (useId). The label points to it with htmlFor. |
aria-describedby | The ids of validationMessage and hint (when present). |
aria-invalid | true when validationState="error". |
required, disabled | Taken 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.
| Component | className | Extra prop |
|---|---|---|
| Input, SearchBox | The input frame (border, background, focus bar) | rootClassName: the outer block with label and hint; set width / margin here |
| Slider | Radix root (rail and thumb) | rootClassName: the outer block with label and value |
| InfoLabel | The label element | rootClassName: the span around the label and the (i) button |
| Select | The outer frame (set width here) | selectClassName: the <select> element itself |
| Textarea | The outer frame (set width here) | textareaClassName: the <textarea> element itself |
| ProgressBar | The progress bar | rootClassName: the outer block when there is a label / description |
| Dropdown, Combobox, TagPicker | Trigger / input | listboxClassName: the listbox surface in the portal |
| PopoverSurface, MenuPopover, TooltipContent | The 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>- 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.
- 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.