Upgrade to v4

Move a v3 app to v4: update the package, replace removed APIs, and check the result in your app.

Start from a clean working tree so you can review the migration. If you are starting a new app, use the installation guide instead.

  1. Install the new major

    npm install @usefragments/ui@4
  2. Check your root size

    The library no longer sets the document root font size. Its rem lengths follow the host page; a browser with default settings uses 16px.

    If your v3 app used a 14px root, review your own rem values as you remove that override. Multiplying those app-authored values by 0.875 preserves their size at a 16px root. Do not apply this conversion to library tokens or apps that already used a 16px root.

  3. Move the theme to seeds

    Theme colors derive from brand, neutral, and status seeds. Start with your brand seed, then review the resulting colors in light and dark modes.

    :root {
      --fui-seed-brand: #f56138;
    }

    ThemeProvider is now Theme. The root theme places its mode in data-theme on <html>. A Theme inside a Theme scopes its own subtree.

Find a replacement

Type errors point at most removed props and parts. Paste the name from the error into the filter to find its row.

644 changes

Removed components

Removed components: before and now
Before
Now
What to change
BentoGrid, BentoGrid.Item, BentoGridProps, BentoGridItemProps, ResponsiveSpanGrid with Grid.Item colSpan / rowSpan around a CardRewrite the mosaic as a Grid with spanning items
ThemeToggle, ThemeButton, Theme.Toggle, Theme.Button and their prop typesremoved: a ToggleGroup wired to useTheme (System, Light, Dark; System first), or an IconButton calling toggleModeReplace with the colour-mode composition from the ToggleGroup and Theme docs
Textarea resize="horizontal" / resize="both"Removed: none or vertical only; width never resizesReplace with vertical
ConversationList.TypingIndicator, TypingIndicator, TypingIndicatorPropsThinkingIndicator as the last childReplace with <ThinkingIndicator label="…" />
Prompt.Tabs, Prompt.Tab, PromptTabsProps, PromptTabPropsRemoved: put Tabs above the composer, or a Prompt.PickerMove modes into a Prompt.Picker or page-level Tabs
Prompt.ModeButton, PromptModeButtonPropsPrompt.Picker (one of a few options) or a ToggleReplace a mode toggle with Prompt.Picker options or a Toggle
Prompt.SelectPrompt.Picker (aria-label, options, value, onValueChange)Map the select options to Prompt.Picker options
Prompt.Usage, PromptUsagePropsPrompt.Info with your own wordsRender usage text in Prompt.Info
Pagination.Item, Pagination.Ellipsis, PaginationItemPropsPagination.Items (with renderLink for link pages)Replace hand-placed items with <Pagination.Items />
NavigationMenu, NavigationMenu.List, NavigationMenu.Item, NavigationMenu.LinkHeader.Nav with one Header.NavItem per link (href, active, render)NavigationMenu to Header.Nav inside a Header; drop .List (the nav renders the list); merge each .Item + .Link into one Header.NavItem, keeping href and active
NavigationMenu.Trigger + NavigationMenu.Content, NavigationMenu.Viewport, NavigationMenu.IndicatorHeader.NavMenu (label, active) holding Header.NavMenuItem rows; it opens a floating menu, with no viewport or indicatorTurn each trigger and content pair into <Header.NavMenu label="…"> with one Header.NavMenuItem href per link; a rich panel (columns, descriptions) becomes a plain list or its own page
NavigationMenu.MobileBrand, NavigationMenu.MobileContent, NavigationMenu.MobileSectionHeader.Trigger opening a Drawer of Sidebar.Item rows, or the Sidebar mobile panel inside AppShellInside AppShell delete the mobile parts (Header.Trigger opens the sidebar panel); standalone, open a Drawer side="left" from Header.Trigger onClick and list the links as Sidebar.Item
DashboardLayout block and DashboardLayoutPropsAppShell layout="sidebar" with Main (and Grid for the cards); the AppShell Dashboard exampleRebuild the page as AppShell > AppShell.Header + AppShell.Sidebar + AppShell.Main > Main > Main.Header + Main.Content; put stat cards in a Grid
Header.MobileNav, Header.MobileNavLink, Header.MobileNavActions and HeaderMobileNavProps / HeaderMobileNavActionsPropsHeader.Trigger opening a Drawer of Sidebar.Item rows (standalone) or the Sidebar mobile panel (inside AppShell)Move the mobile links into a Drawer opened from Header.Trigger onClick, one Sidebar.Item per link and actions in its footer; inside AppShell delete them
Progress.Circular / CircularProgress, CircularProgressProps, strokeWidthRemoved: Progress for a value, Loading for a waitReplace a ring with <Progress> or <Loading>
<Editor size="sm" | "md" | "lg"><Editor rows={n}> (lines of text)Replace with a line count
FragmentsLogoRemoved: use your own logoRemove the library import and provide your own logo in the existing slot
FragmentsWordmarkRemoved: use your own logoRemove the library import and provide your own logo in the existing slot
FragmentsBrandRemoved: use your own logoRemove the library import and provide your own logo in the existing slot
NavGlyphRemoved: use your own logo or generic iconRemove the library import and provide your own logo or generic icon in the existing slot
NAV_GLYPH_NAMESRemoved: use your own glyph namesRemove the library import and provide your own glyph names in the existing slot
NavGlyphNameRemoved: use your own glyph namesRemove the library import and provide your own glyph names in the existing slot

Props

Props: before and now
Before
Now
What to change
Card variant solid / soft / outlineRemoved: one card, the surface plane with the hairlineDelete the prop
Card tone accent / warningRemoved: tone is neutral or danger; put a warning in an Alert inside the cardDrop accent; move a warning card's message into <Alert tone="warning">
Box background primary / secondary / tertiary / elevatedBox plane surface / band / canvas / raisedRename the prop and map the value
Box shadow sm / md / lgRemoved: static boxes cast no shadow; floating surfaces own shadowDelete the prop
Text weightText strong (the semibold weight)weight="semibold", "medium" or "bold" becomes strong; "normal" is deleted
Separator softRemoved: no rule takes opacity; a quieter break is more gapDelete soft. Raise gap where the break should read quieter
Text color mutedText color tertiaryRename the value
Badge size sm / md / lgRemoved: one 20px badgeDelete the prop
Badge dotPulse, dotColor, activeRemoved: the dot takes the tone ink and never animatesDelete the props
Avatar shape circle / squareRemoved: every avatar is the rounded squareDelete the prop
Avatar color, customSize, imageStyleRemoved: initials paint the accent tint; use className for one-off sizingDelete the props
Alert emphasis tint / surfaceRemoved: the tint is the only treatmentDelete the prop
EmptyState variant ghost / outlineRemoved: the state never draws its own frameDelete the prop. Frame a reserved region with its Card
Button variant="outline"variant="soft": the neutral secondary, a quiet tint with no borderRename variant="outline" to variant="soft" on Button
IconButton variant="outline"variant="soft"Rename variant="outline" to variant="soft" on IconButton
Link underline (always, hover, none, dotted)removed: a link is always underlined with a hairline at the 3px offsetDrop underline; text that must not underline is not a Link (use Button variant="link" for an action)
Chip variant (soft, outline), tone, size and avatarremoved: one look (24 high, band fill, hairline, control radius); a leading avatar goes in iconDrop variant, tone and size; rename avatar to icon
Chip.Group onChangeonValueChange(values: string[]); the group is role="group" and needs aria-label or aria-labelledbyRename onChange to onValueChange on Chip.Group; add a name
ToggleGroup variant (soft, ghost, outline) and gapremoved: one look, the segmented track with the lifted thumbDrop variant and gap on ToggleGroup
ToggleGroup onChange and selectionModeonValueChange(value: string); multiple makes the value a string[]Rename onChange to onValueChange on ToggleGroup; drop selectionMode
Input onChange(value: string)onValueChange(value); onChange is the native change eventRename onChange to onValueChange where the handler takes a string
Input inputStylestyle on the InputRename inputStyle to style
Input inputClassNameclassName on the Input (the field box)Rename inputClassName to className
Textarea rowsminRows; the field grows with its text to maxRowsRename rows to minRows
Textarea showCharCountRemoved: the counter shows whenever maxLength is setDelete the prop. Drop maxLength where no counter is wanted
Textarea errorinvalid, plus a Field.Error that says whyRename error to invalid
Textarea onChange(value: string)onValueChange(value); onChange is the native change eventRename onChange to onValueChange where the handler takes a string
Select onChangeonValueChangeRename onChange to onValueChange
Combobox onChangeonValueChangeRename onChange to onValueChange
Form onFormSubmitonSubmitRename onFormSubmit to onSubmit
Dialog modal propRemoved: Dialog is always modal, with a scrimDelete modal; a non-blocking panel moves to Popover or Drawer modal="trap-focus"
Drawer.Content width (sm · md · lg · xl · full)size (sm · md · lg): the width on start and end, the height on bottomRename width to size; xl and full become lg
Drawer.Content backdropDrawer modal: true draws the scrim, "trap-focus" traps focus without one, false leaves the page usablebackdrop={false} becomes modal="trap-focus" on the root; drop backdrop
Drawer swipeDirection / Drawer.SwipeArea swipeDirectionDerived from side (dismiss toward the drawer's own edge)Delete the prop
Popover.Content sideOffsetRemoved: every popup sits the library's popup offset from its anchorDelete the prop
Tooltip arrowRemoved: a tooltip has no arrowDelete the prop
Tooltip sideOffsetRemoved: the library's popup offsetDelete the prop
Tooltip.RootTooltipRename Tooltip.Root to Tooltip
HTML attributes passed to Tooltip itself (forwarded to the popup)contentProps={{ … }}Move className, style, id and data-* from Tooltip into contentProps
Menu.Content sideOffsetRemoved: the library's popup offsetDelete the prop
useToast().error()useToast().danger()Rename error( to danger( on the useToast() result
Command hostedRemoved: Command is frameless; Command.Dialog is the floating paletteDelete hosted. A Command inside Dialog moves to Command.Dialog
Command.Item onItemSelectonSelect (the Menu verb)Rename the prop
Loading centeredfill (centres the spinner in its parent)Rename centered to fill
Loading.Screen kind, size, color and the fragments symbolRemoved: one spinner on the canvas, with label and showLabelDelete kind, size and color from Loading.Screen
StepStatus "streaming""pending"Rename status="streaming" to status="pending" on steps
Default user and assistant avatars on MessageNo avatar unless avatar is passed (usually Message.Avatar)Pass avatar={<Message.Avatar …/>} where avatars are wanted; delete avatar={null}
ConversationList showAvatarsRemoved: avatars are opt-in per MessageDelete showAvatars
ConversationList scrollTopThreshold, scrollBottomThresholdRemoved: one follow distanceDelete both
Prompt loadingpending (sending) or working with onStop (responding)Rename loading to pending. Use working + onStop while the reply streams
Prompt autoResizeRemoved: the text area always grows between minRows and maxRowsDelete autoResize. autoResize={false} becomes maxRows equal to minRows
Prompt onChange(value)onValueChange(value)Rename onChange to onValueChange on Prompt
CodeBlock bgRemoved: the block is the bandDelete bg
CodeBlock filenametitle (in the header inside the block)Rename filename to title
CodeBlock collapseAction (lines, expand)Removed: one fold bar (Show N more lines / Show less)Delete collapseAction
TabbedCodeBlock theme, copyPlacement, tabsVariant, collapseActionRemoved; size forwards to every panelDelete the four props
Checkbox onChangeonCheckedChangeRename onChange to onCheckedChange on Checkbox
Checkbox size (sm, md, lg)Removed: one 16px boxDelete size from Checkbox
Checkbox descriptionhelperTextRename description to helperText on Checkbox
Checkbox controlClassName, contentClassNameRemoved: style the row with classNameMove the styles onto className on the row, or drop them
RadioGroup onChangeonValueChangeRename onChange to onValueChange on RadioGroup
RadioGroup size (sm, md, lg)Removed: one 16px circleDelete size from RadioGroup
RadioGroup groupClassNameRemoved: style the group through classNameTarget the radiogroup inside className, or drop it
RadioGroup.Item descriptionhelperTextRename description to helperText on RadioGroup.Item
RadioGroup.Item controlClassName, contentClassNameRemoved: style the row with classNameMove the styles onto className on the item, or drop them
Switch onChangeonCheckedChangeRename onChange to onCheckedChange on Switch
Switch size (sm, md, lg)Removed: one 36×18 trackDelete size from Switch
Switch descriptionhelperTextRename description to helperText on Switch
Slider onChangeonValueChangeRename onChange to onValueChange on Slider
Slider errorinvalid plus errorMessageRename error to invalid. Add the errorMessage
Tabs.Panel flushRemoved: panels have no padding; the caller pads the contentDrop flush. Add padding inside non-flush panels
Collapsible.Content forceMountkeepMountedRename forceMount to keepMounted
Accordion collapsibleRemoved: a single accordion always closes the open item when it is pressed againDrop collapsible
Breadcrumbs separator and Breadcrumbs.SeparatorOne caret separator drawn by the componentDrop separator and every <Breadcrumbs.Separator>
Breadcrumbs.Item currentThe last item is the current page (never a link, aria-current="page")Drop current. Make the current page the last item
Breadcrumbs.Item iconRemoved: crumbs are textDrop icon
Breadcrumbs maxItems inline … expandermaxItems folds the items after the first into a menu of linksSame prop
Pagination edgeCountRemoved: the first and last page always showDrop edgeCount
TableOfContents.Item idtargetIdRename id to targetId on TableOfContents.Item
navigation.link-states, navigation.link-active, navigation.collapsed-shellremoved: Header, Sidebar and Main own their row statesDrop the include
KEYBOARD_SHORTCUTS.NAV_TOGGLE, KEYBOARD_SHORTCUTS.NAV_CLOSEremoved: Header.NavMenu uses the menu keys (Enter, Space, arrows, Escape)Drop references to the two entries
Header.Spacerremoved: Header.Actions takes the free spaceDelete <Header.Spacer />
Sidebar position="right"removed: the rail sits at the inline startDrop the prop; a right-hand panel is AppShell.Aside
Sidebar.Item multilineremoved: rows hold one line and truncateDrop the prop; move secondary text to a tooltip or the page
useSidebarContextuseSidebarRename
AppShell.Sidebar collapsible, defaultCollapsedthe same props on AppShell, with the rest of the sidebar stateMove the props from AppShell.Sidebar to AppShell
The shell scrolled the pageAppShell is 100dvh and AppShell.Main scrolls, so the header row stays put and Header elevatedOnScroll does nothing inside itDrop elevatedOnScroll and position on a Header inside AppShell.Header; scroll listeners on window move to the pane
Table size (sm/md/lg) and density (compact/regular/relaxed/condensed)Removed: one 32px row track (44px under a coarse pointer)Delete size and density from <Table>
Table.Row bandRemoved: no row fill except hover, press and the selection washDelete band from <Table.Row>
Table.Caption hiddenTable.Caption visuallyHiddenRename hidden to visuallyHidden on <Table.Caption>
Skeleton radiusRemoved: each shape carries its radius (indicator, or control for control)Delete radius
Skeleton staticRemoved: reduced motion holds every bar stillDelete static
Skeleton shape="button" and shape="input"shape="control" with size sm/md/lg (28/32/40)Rename both shapes to control
Skeleton.Text gapRemoved: lines sit at the 18px body pitchDelete gap from <Skeleton.Text>
List marker="decimal"as="ol" (an ol numbers its items by default)marker="decimal" to as="ol", dropping the marker prop
List gap="md"gap="sm"Rename the value
Progress size (sm/md/lg)Removed: one 4px trackDelete size from <Progress>
Progress tone="success"Removed: say done in words; tone="neutral" for a meterDelete tone="success"
Icon color="primary"removed: the default inherits currentColorDelete color="primary" on Icon
Icon color="secondary" / color="tertiary"tone="secondary" / tone="tertiary" (one ink prop)Rename color to tone on Icon
Image rounded="none"radius="none" (the default: the container clips the corner)Delete rounded="none"
Image rounded="sm" / rounded="md"radius="control"Rename rounded to radius and map the value
Image fallback shown while loadingthe frame pulses (skeleton band) while loading; fallback replaces only the built-in error fallbackDrop loading placeholders passed as fallback
VisuallyHidden skip link with its own inline reveal styles<VisuallyHidden focusable> (raised chip while focused)Delete the hand-rolled reveal styles
Theme defaultThemedefaultModeRename the prop
ChartTooltip/ChartTooltipContent indicator="dot | line | dashed"removed; the swatch follows the series (dashed stroke, line, filled)Delete the prop
ChartTooltip/ChartTooltipContent hideIndicatorremoved; hidden automatically for one seriesDelete the prop
ColorPicker onChangeonValueChangeRename the prop
ColorPicker descriptionhelperTextRename the prop
<DatePicker selected={d} onSelect={fn}> / onChange={fn}<DatePicker value={d} onValueChange={fn}>Rename props
<DatePicker mode="range" selectedRange={r} onRangeSelect={fn}><DatePicker mode="range" value={r} onValueChange={fn}>Rename props
<DatePicker fixedWeeks><DatePicker> (six weeks always)Delete prop
<DatePicker error="Message"> / error<DatePicker invalid errorMessage="Message">Split prop
<DatePicker size="lg"><DatePicker size="md">Replace value
formats={["heading1", "heading2", "heading3"]}formats={["heading"]} (one heading step)Replace values
<LoginForm loading><LoginForm pending>Rename prop
<StatsCard changeTone="warning">changeTone="danger" or "neutral"Replace value
KEYBOARD_SHORTCUTS.EDITOR_HEADING1, KEYBOARD_SHORTCUTS.EDITOR_HEADING3removed: Editor offers only the level-2 heading commandDrop references to the two entries

Composition

Composition: before and now
Before
Now
What to change
A ThemeProvider inside another ThemeProvider (wrote data-theme and storage on <html>)A scope: one <div data-fui-theme> with its own mode and inputs; it persists only with its own storageKeyA nested provider meant to switch the page moves to the root, or calls useTheme().setMode
A disabled control inside a disabled container (Fieldset): dimmed twice, 0.45 × 0.45Dimmed once at the outermost disabled element (0.45)None needed; a custom disabled wrapper that sets opacity should include mixins.disabled-state instead
Card with onClick (a clickable div)Card render={<a href />} or render={<button />}: a real link or button; selected marks the held cardMove the handler onto the element passed to render
Box margin / marginX / marginYRemoved: the parent owns spacing (Stack gap)Move the space to the parent Stack or Grid gap
Box rounded sm / md / lgBox radius control / nested / surfacesm becomes control, md and lg become surface. A box inside a surface takes nested
Box color, display, overflowRemoved: Text owns ink, Stack and Grid own layout, ScrollArea owns scrollingMove each to the component that owns it
Box width / minWidth / maxWidth / height / minHeight / maxHeightRemoved: size the box with className or the parent layoutMove the size into a class
Hand-drawn keycaps (<kbd> styled per component)Kbd and Kbd.Group: the press-tint keycapRender shortcuts as <Kbd>; glyph keys pass label
Button asChild and as="a"render, e.g. render={<a href="/docs" />} or a router link; a rendered anchor keeps link semantics, and nativeButton overrides the inference<Button asChild><a href=x>Y</a></Button> and <Button as="a" href=x>Y</Button> become <Button render={<a href={x} />}>Y</Button>
Button and IconButton size: sm, md, lgxs (24), sm (28), md (32), lg (40); a ComponentDefaultsProvider controlSize="xs" region reaches xsNone needed; a dense row can drop to size="xs"
IconButton without renderrender, as on Button: render={<a href="/settings" />} for an icon linkAdditive
ButtonGroup gap, align, wrapremoved: a ButtonGroup is one joined cluster (no gap, one row, hairline dividers); spaced buttons are a Stack direction="row"A ButtonGroup with gap other than none becomes a Stack direction="row" gap=…; drop align and wrap
Link asChildrender, e.g. render={<RouterLink to="/" />}<Link asChild><X …>Y</X></Link> becomes <Link render={<X … />}>Y</Link>
Theme componentDefaults prop<ComponentDefaultsProvider controlSize=…> inside the Theme<Theme componentDefaults={{ controlSize: x }}> becomes <Theme><ComponentDefaultsProvider controlSize={x}>
ComponentDefaultsProvider value={{ controlSize }}ComponentDefaultsProvider controlSizeHoist value.controlSize to the controlSize prop
useResolvedControlSize(explicit, fallback)useResolvedControlSize(explicit, steps?): the provider size resolves to the nearest of the control's steps (sm, md, lg unless the control passes CONTROL_SIZES)A custom control passes the steps it draws instead of a fallback size
Field sizeRemoved: the control inside owns its height (size on Input, Select, Textarea, Combobox, NumberField)Move size from <Field> to the control inside it
Input label, helperText, required chrome (the built-in label row and asterisk)<Field> with Field.Label, Field.Description and Field.Required; required stays as the native attributeWrap in <Field>, move label to <Field.Label> and helperText to <Field.Description>
Input error (boolean)invalid on the Input, or invalid on its Field, plus a Field.Error that says whyRename error to invalid. Move the reason from helperText into <Field.Error match>
Input withFieldWrapperRemoved: Input never renders a wrapper; it is bare by defaultDelete the prop
Input rootPropsRemoved: there is no wrapper elementMove the attributes to the Input or to an element around it
Input className / style on the wrapper divOn the field box itselfReview layout classes that targeted the wrapper
Textarea label, helperText<Field> with Field.Label and Field.DescriptionWrap in <Field> and move both
Textarea rootPropsRemoved: the wrapper only appears with a counter and takes no propsMove the attributes to the Textarea or an element around it
Select variant="outline"Removed: the one field look is the defaultDelete the prop
Select label, helperText, error<Field> with Field.Label, Field.Description and Field.Error; invalid on the SelectWrap in <Field>, move the text into its parts, rename a boolean error to invalid
Select root className and the wrapper divRemoved: the root renders no element; style Select.TriggerMove className to <Select.Trigger>
Combobox label, helperText, error<Field> with Field.Label, Field.Description and Field.Error; invalid on the ComboboxWrap in <Field>, move the text into its parts, rename a boolean error to invalid
Combobox root className and the wrapper divRemoved: the root renders no element; style Combobox.InputMove className to <Combobox.Input>
Form onClearErrorsRemoved (it was never called): a field's server error clears when the field is editedDelete the prop
asChild on Dialog.Trigger / Dialog.Closerender={<Button … />} with the label as the part's children<Dialog.Trigger asChild><Button …>Label</Button></Dialog.Trigger> becomes <Dialog.Trigger render={<Button … />}>Label</Dialog.Trigger>
asChild on Drawer.Trigger / Drawer.Closerender={<Button … />}As for Dialog
asChild on Popover.Trigger / Popover.Closerender={<Button … />}As for Dialog
Tooltip.Provider delayDuration / skipDelayDurationdelay / timeoutRename the props
Tooltip delay with no provider: 600ms every time500ms cold, then 0ms for the next tooltip within 300ms (warm); a disabled control still shows its tooltip; shortcut adds a keyBehaviour
asChild on Menu.Triggerrender={<Button … />}As for Dialog
ToastProvider position top-left · top-right · bottom-left · bottom-rightbottom-end (default) · bottom-center · top-centerbottom-right becomes bottom-end; the other corners become bottom-end or top-center
ToastProvider duration default 8000, any valueDefault 5000; anything under 5000 is raised to 5000; 0 keeps a toast until dismissedDrop short durations; pass 0 for a toast that must stay
ToastProvider max (default 5, drops the oldest)Removed: three on screen, the rest queued in orderDelete the prop
A toast with an action stays until dismissedIt keeps the provider duration (five seconds, paused on hover, focus and window blur); the action closes itPass duration: 0 where an action toast must stay
Loading color (accent, current, muted)Removed: the spinner follows currentColorDelete color. Set color on a parent where it must differ
Message.Content markdownRemoved: an assistant string always renders as markdown; a user string keeps its line breaksDelete markdown. Pass a node to opt out of markdown
RadioGroup wrapperClassNameclassName (on the wrapper)Rename wrapperClassName to className
RadioGroup groupIdid (now on the radiogroup element)Rename groupId to id. A wrapper selector that read id now reads className
RadioGroup ref and id on the outer wrapperOn the radiogroup elementA ref that measured the wrapper now gets the radiogroup
Collapsible.Trigger asChildrender<Collapsible.Trigger asChild><X/></…> → <Collapsible.Trigger render={<X/>} />
Collapsible.Trigger showChevron and chevronPositionOne leading caret on the default trigger; a render trigger draws its ownDrop both props. Use render for a trigger without the caret
Accordion value / defaultValue as a stringstring[] (the open items)Wrap a string value in an array; onValueChange receives an array
Pagination with totalPages 0: an empty <nav>Nothing rendersReview layouts that reserved the space
TableOfContents.Item indentNesting: put the items in a TableOfContents.GroupWrap indented items in a Group (collapsible={false} for a plain label)
TableOfContents.Group active and TableOfContents hideSubItemsRemoved: the active item carries the marking; render fewer items for a compact indexDrop both props
asChild on Header.Brand, Header.NavItem, Header.NavMenuItemrender, e.g. render={<RouterLink to="/" />}<Header.NavItem asChild><Link …>x</Link></Header.NavItem> to <Header.NavItem render={<Link … />}>x</Header.NavItem>
Header inside AppShell.Header rendered a second <header> bannerHeader renders a div there; AppShell.Header is the bannerMarkup only
asChild on Sidebar.Item, Sidebar.SubItemrender, e.g. render={<RouterLink to="/" />}As for the Header parts
State props on a Sidebar inside a Sidebar.Provider or AppShell were merged with the provider'sthe provider (or AppShell) owns the state; props on the inner Sidebar are ignored with a development warningMove collapsed, defaultCollapsed, onCollapsedChange, open, defaultOpen, onOpenChange, collapsible and enableKeyboardShortcut to the provider or AppShell
AppShell layout="sidebar-floating", layout="floating", slot variant="floating" and AppShellSlotVariantlayout="default" or layout="sidebar": canvas planes split by hairlines, no floating panelssidebar-floating to sidebar, floating to default; delete variant
AppShell.Main padding and its <main id="main-content"> elementAppShell.Main is the scroll pane (a div); put Main inside it, which owns <main id="main-content"> and the 16 gutterDelete padding; wrap the children of AppShell.Main in <Main>
Table stripedRemoved: rows are transparent on the table's plane; the hairline divides themDelete striped from <Table>
DataTable emptyMessage (string)emptyState: EmptyState parts, rendered in a row under the real headeremptyMessage="X" to emptyState={<EmptyState.Title as="p">X</EmptyState.Title>}
List gap="lg" and gap="xl"Removed: space larger groups with the parent (Stack gap)Wrap the items in a Stack or move the spacing to the parent
List.Item icon ignored unless marker="icon"icon renders whenever it is passed, in the marker columnRemove icons you passed but did not want shown
Icon iconPropsremovedSize and weight come from Icon's size and weight; label the wrapper with aria-label
Image rounded="lg"radius="surface" (or radius="nested" inside a padded surface)Rename rounded to radius="surface"
Separator gap="…"removed; the parent owns the space (Stack gap)Move the space to the parent Stack gap
Stack separator / separator={node}divided (renders Separator; list-safe)separator → divided; by hand for custom nodes
resolveLayoutGap (internal util)removedNothing to change (never exported)
ThemeProvider, Theme.ProviderTheme (or Theme.Root)Rename the element and import
ColorPicker showInput={false}removed; the hex field always showsDelete the prop
LoginForm form error in a custom boxAlert tone="danger"; per-field emailError / passwordErrorNothing to change (render change)
ActivityFeed renders every itemfirst 5, then "Show more"; pass limitPass limit={items.length} to show all
Table head position: sticky and opaque at every size (it could never stick: the wrapper is its scrollport)maxHeight on Table and DataTable bounds the wrapper; the head sticks inside it and fills its plane only while rows pass under it. Unbounded heads scroll with the page and draw no bandSet maxHeight where the head must stay in view

Types and imports

Types and imports: before and now
Before
Now
What to change
Typed text under a coarse pointer: the field's type role (12px at md), and font set per component16px in every text-entry element (field.size(), field.coarse-typed-text), each with font: inherit; field.size($size, $typed: false) opts a trigger outA value change; a custom text field built on field.size() gets 16px too
CardRoot, CardHeader, CardTitle, CardDescription, CardBody, CardFooter (flat exports)Card, Card.Header, Card.Title, Card.Description, Card.Body, Card.FooterRewrite the import and the JSX tag
Text role ui-compact / ui-standard / body-compact / body-relaxedText type control (ui-compact) or body (the rest)Map the role value
Text role title-sm / title-md / title-lgText type title (15) or display (24)title-sm and title-md become title; title-lg becomes display
Text role eyebrowText type section-labelRename the value
Text scale, font, letterSpacingRemoved: pick a typeChoose the role whose size matches the old scale step
Text role (the type step) and the TextRole typeText type and the TextType type; role is the ARIA attribute again (variant stays chrome only)Rename role= to type= on Text; rename TextRole to TextType
Badge variant soft / outline / ghost, BadgeVariantRemoved: one badge, the tone tintDelete the prop and the type import
Button tone="info", tone="success", tone="warning"removed: the tone matrix is solid accent or danger, soft and ghost neutral or danger, link accent or neutral; a tone outside the matrix falls back to the variant defaultDrop the status tone (status sits in a Badge or Alert beside the action); a type error flags each pair outside the matrix
SelectVariant typeRemovedDelete the import
Flat part exports FieldRoot, FieldLabel, FieldControl, FieldDescription, FieldError, FieldValidity, FieldRequiredField, Field.Label, Field.Control, Field.Description, Field.Error, Field.Validity, Field.RequiredFieldX to Field.X
Flat part exports FieldsetRoot, FieldsetLegend, FieldsetDescriptionFieldset, Fieldset.Legend, Fieldset.DescriptionFieldsetX to Fieldset.X
Flat part exports ComboboxRoot, ComboboxInput, ComboboxTrigger, ComboboxContent, ComboboxItem, ComboboxEmpty, ComboboxGroup, ComboboxGroupLabelCombobox and its dot parts (Combobox.Input …)ComboboxX to Combobox.X
Input type="search": Escape always stopped at the fieldEscape clears a filled search and stops there; on an empty search it blurs and propagates, so a surrounding Dialog or Popover closesBehaviour change. A test that expects the first Escape to stay in an empty search now sees the overlay close
Toast props id, duration, onPause, onResume; the ToastData typeRemoved from the static Toast; ToastInput (id, duration) is what useToast().toast() takesPass those fields to toast({ … }), not to <Toast>
Flat part exports DialogRoot, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter, DialogCloseDialog and its dot parts (Dialog.Trigger …)DialogX to Dialog.X
Flat part exports AlertDialogRoot, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogBody, AlertDialogFooter, AlertDialogCancel, AlertDialogActionAlertDialog and its dot parts (AlertDialog.Cancel …)AlertDialogX to AlertDialog.X
Flat part exports DrawerRoot, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, DrawerBody, DrawerFooter, DrawerClose, DrawerSwipeAreaDrawer and its dot parts (Drawer.SwipeArea …)DrawerX to Drawer.X
Flat part exports CommandRoot, CommandInput, CommandList, CommandItem, CommandGroup, CommandEmpty, CommandError, CommandSeparator, CommandDialogCommand and its dot parts (Command.Dialog …)CommandX to Command.X
Loading kind (spinner, dots, pulse, fragments) and the LoadingKind typeRemoved: one spinnerDelete kind
Loading size (sm, md, lg, xl) and the LoadingSize typeRemoved: one size, the icon md (16px); inline sizes it to the text (1em)Delete size. Inside a line of text add inline
Loading.Inline, LoadingInline, LoadingInlineProps<Loading inline />, LoadingProps<Loading.Inline …/> to <Loading inline …/>; drop its size
ThinkingIndicator steps (data array) and the ThinkingStep data type (ThinkingStepType in the barrel)ThinkingIndicator.Steps with ThinkingIndicator.Step label status childrenMap steps={[{ id, label, status }]} to <ThinkingIndicator.Steps>{steps.map((s) => <ThinkingIndicator.Step key={s.id} label={s.label} status={s.status} />)}</ThinkingIndicator.Steps>
Message role (user, assistant, system) and the MessageRole typefrom (user, assistant) and MessageFrom; a system line is ConversationList.EventRename role= to from= on Message. Replace role="system" messages with <ConversationList.Event>…</ConversationList.Event>
ConversationList.DateSeparator, DateSeparator, DateSeparatorPropsConversationList.Event (date, format, or children for an event), ConversationListEventPropsRename ConversationList.DateSeparator and DateSeparator to ConversationList.Event
Prompt variant (outline, ghost) and the PromptVariant typeRemoved: one field, toolbar on the same planeDelete variant
Prompt placement (inline, fixed, sticky) and the PromptPlacement typeRemoved: the page places the composerDelete placement. Position the wrapper in page CSS
Prompt.ActionButton, PromptActionButtonPropsAn IconButton (ghost, sm) in Prompt.Toolbar<Prompt.ActionButton …> to <IconButton variant="ghost" size="sm" …>
CodeBlock persistentCopy, copyPlacement and the CodeBlockCopyPlacement typeRemoved: copy is always visible, in the header when titled, over the first line otherwiseDelete both
Barrel exports MessageRoot, MessageContent, MessageActions, MessageTimestamp, MessageAvatar, MessageErrorMessage and its dot parts (Message.Content …)MessageX to Message.X
Barrel exports ConversationListRoot, ConversationListEventConversationList, ConversationList.EventConversationListX to ConversationList.X
Barrel exports LoadingRoot, LoadingScreenLoading, Loading.ScreenLoadingScreen to Loading.Screen, LoadingRoot to Loading
TabbedCodeBlock (flat export)CodeBlock.Tabbed<TabbedCodeBlock to <CodeBlock.Tabbed
Flat TabsRoot, TabsList, Tab, TabsPanel exports from the moduleTabs, Tabs.List, Tabs.Tab, Tabs.Panel (the barrel only exported the compound)TabsList → Tabs.List, Tab → Tabs.Tab, TabsPanel → Tabs.Panel
CollapsibleRoot, CollapsibleTrigger, CollapsibleContent, CollapsibleRootPropsCollapsible, Collapsible.Trigger, Collapsible.Content, CollapsiblePropsRename the flat parts to the compound parts
Accordion type="single" / type="multiple"multiple boolean (default single)type="multiple" → multiple; drop type="single"
Flat AccordionRoot, AccordionItem, AccordionTrigger, AccordionContent exports from the moduleAccordion, Accordion.Item, Accordion.Trigger, Accordion.ContentRename the flat parts to the compound parts
BreadcrumbsRoot, BreadcrumbsItem, BreadcrumbsSeparator, BreadcrumbsSeparatorPropsBreadcrumbs, Breadcrumbs.ItemRename to the compound parts; drop the separator
Flat PaginationRoot, PaginationPrevious, PaginationNext, PaginationItems exports from the modulePagination, Pagination.Previous, Pagination.Next, Pagination.ItemsRename the flat parts to the compound parts
TableOfContentsRoot, TableOfContentsItem, TableOfContentsGroupTableOfContents, TableOfContents.Item, TableOfContents.GroupRename the flat parts to the compound parts
The flat NavigationMenuRoot, …List, …Item, …Trigger, …Content, …Link, …Indicator, …Viewport, the mobile flat exports and the NavigationMenu*Props typesremoved with the component: the Header parts and their Header*Props typesMap each flat import to the Header part above; NavigationMenuLinkProps to HeaderNavItemProps
Header navAlign, container and the HeaderNavAlign / HeaderContainer typesremoved: the nav follows the brand and the bar spans its container with the 16 gutterDrop the props; centre a nav with your own wrapper if a page needs it
Header icons and the HeaderIcons / HeaderIconSlot / HeaderIconRenderState typesHeader.Trigger children for the glyph; the menu chevron is fixedPass a custom trigger glyph as Header.Trigger children; drop the other slots
Sidebar.Trigger and SidebarTriggerPropsHeader.Trigger (it opens the mobile panel and brings back a hidden rail) and HeaderTriggerPropsSidebar.Trigger to Header.Trigger
Sidebar.Overlay and SidebarOverlayPropsremoved: the mobile panel draws its own scrimDelete <Sidebar.Overlay />
Sidebar.Rail, SidebarRailProps and the floating collapse toggle on the rail edgeSidebar.CollapseToggle in Sidebar.Footer, or Cmd/Ctrl+BSidebar.Rail to Sidebar.CollapseToggle inside Sidebar.Footer
SidebarProvider, SidebarRoot and the other flat Sidebar* component exportsSidebar.Provider and the other compound partsSidebarProvider to Sidebar.Provider; flat part imports to Sidebar.<Part>
Sidebar.MenuSkeleton rendered a divan li, so it sits inside the nav list; SidebarMenuSkeletonProps extends HTMLLIElement attributesPlace it inside Sidebar.Nav as before
useAppShell, AppShellRoot, AppShellHeader, AppShellSidebar, AppShellMain, AppShellAsideuseSidebar for the state; the compound AppShell partsuseAppShell to useSidebar; flat part imports to AppShell.<Part>
Main as values ul, ol, nav, aside, header, footer, articleas is main (default), section or div; the MainElement typeKeep Main as the page region and put the list or landmark inside Main.Content
DataTable size, density and striped; the DataTableDensity typeRemoved: DataTable composes Table and takes its one row trackDelete the props and the type import
List type and markers at the reading size (16px)Body size (12px), markers in ink 3A value change; review screens
useListContext (module export, not in the barrel)RemovedNothing reads it
IconColor typeremoved (use IconTone)Rename the type import
ResponsiveColumns, ResponsiveDirection, ResponsiveGap typesremovedSee the Grid and Stack rows above
ThemeProviderPropsThemePropsRename the type
NeutralPalette, RadiusStyle type exportsremovedType neutral as ThemeNeutral, radius as number | string
<Editor toolbarIcons={…}>, EditorToolbarIcons, EditorToolbarIconSlot, EditorToolbarIconRenderState(none; glyphs come from Icon)Delete prop and types
<Editor.Separator />, EditorSeparator, EditorSeparatorProps<Separator orientation="vertical" length="control" />Replace component
EditorSize(none)Delete type
Flat part exports ListRoot, ListItem, ListRowList, List.Root, List.Item, List.RowRename to the compound parts
Flat part exports ListboxRoot, ListboxItem, ListboxGroup, ListboxEmptyListbox, Listbox.Root, Listbox.Item, Listbox.Group, Listbox.EmptyRename to the compound parts
Flat part exports DatePickerRoot, DatePickerTrigger, DatePickerContent, DatePickerCalendar, DatePickerPreset (root and ./datepicker)DatePicker, DatePicker.Root, .Trigger, .Content, .Calendar, .PresetRename to the compound parts
Flat part exports EditorRoot, EditorToolbar, EditorToolbarGroup, EditorToolbarButton, EditorStatusIndicator, EditorContentArea, EditorStatusBar (root and ./editor)Editor, Editor.Root, .Toolbar, .ToolbarGroup, .ToolbarButton, .StatusIndicator, .Content, .StatusBarRename to the compound parts
Flat part exports ToggleGroupRoot, ToggleGroupItemToggleGroup, ToggleGroup.Root, ToggleGroup.ItemRename to the compound parts
Flat part exports ThinkingIndicatorRoot, ThinkingSteps, ThinkingStepThinkingIndicator, ThinkingIndicator.Root, .Steps, .StepRename to the compound parts
FragmentsLogoPropsRemoved: use your own logo propsRemove the library import and provide your own logo props in the existing slot
FragmentsWordmarkPropsRemoved: use your own logo propsRemove the library import and provide your own logo props in the existing slot
FragmentsBrandPropsRemoved: use your own logo propsRemove the library import and provide your own logo props in the existing slot
NavGlyphPropsRemoved: use your own glyph propsRemove the library import and provide your own glyph props in the existing slot
*.contract.json*.meta.jsonRename metadata files and update relative imports. Review custom metadata loaders

Theme and CSS

Theme and CSS: before and now
Before
Now
What to change
$fui-brand / --fui-color-accent default #f56138#3d5ae8, set as --fui-seed-brand on :rootTo keep the old brand, set --fui-seed-brand: #f56138 on :root (or $fui-brand in @use … with)
$fui-brand-is-default (ink primary buttons while the brand was the default)Removed: the primary button is the accent for every brandDelete $fui-brand-is-default from @use … with ()
$fui-danger / --fui-color-danger #c44732#d13d1f, set as --fui-seed-dangerTo keep it, set --fui-seed-danger: #c44732
$fui-success / --fui-color-success #2c8c5f#2fbf8f, set as --fui-seed-successTo keep it, set --fui-seed-success: #2c8c5f
$fui-warning / --fui-color-warning #c4922a#f2a100, set as --fui-seed-warningTo keep it, set --fui-seed-warning: #c4922a
$fui-info / --fui-color-info #3d7aa8oklch(0.58 0.13 245), set as --fui-seed-infoTo keep it, set --fui-seed-info: #3d7aa8
configureTheme() writing derived roles (--fui-color-accent, -hover, -active, --fui-focus-ring-color, --fui-color-danger-hover, status fills)configureTheme() writes only --fui-seed-*; the stylesheet derives every role from themDrop hand-set hover, press and ring overrides next to configureTheme(); read the computed token instead of the inline value
Sass colour engine: _palettes.scss, $palettes, $palette-paper, get-palette(), palette-shade(), contrast-color(), wcag-contrast(), ensure-contrast(), is-dark-color(), every derive-*() function, computed.$computed-* coloursDeleted: colour derives in the browser from --fui-seed-* (_derive.scss writes the expressions)Replace each call with the token it computed (var(--fui-color-accent-text, $fui-color-accent-text)); set seeds instead of computing colours in Sass
Sass twins $fui-* and $fui-dark-* following $fui-brand and $fui-neutralFixed Glass literals from _defaults.generated.scss; only the custom properties follow the seedsRead var(--fui-x) where a value must follow the theme; the twin is a fallback only
$fui-neutral ("paper" only)A colour or "paper"; also the runtime input --fui-seed-neutralAdditive
--fui-bg-primary #faf8f5 / #171614#fffefd / #1a1918, a lightness step of --fui-seed-neutralA value change; review screens
--fui-bg-elevated #ffffff / #262421#fffefd / #272624A value change; review screens
--fui-app-canvas-bgThe canvas role, reading --fui-body-bg (UIR-D145)Keep reading it
--fui-border translucent #3c2d1e1a / #343029Opaque #e2dfda / #2c2a28, a lightness step of the neutralA value change; review screens
--fui-border-strong translucent #3c2d1e2e / #4a4641Opaque #d4d0ca / #373532A value change; review screens
--fui-text-primary #1d1c1a / #f0ede9#1b1916 / #eee9e0A value change; review screens
--fui-text-secondary #59544f / #b6b0a8#4f4b45 / #b8b3aaA value change; review screens
--fui-text-tertiary #58534c / #8d8780#67625a / #99948bA value change; review screens
Field edges (the field recipe: Input, Textarea, Select, Combobox and the rest), the Checkbox box and the Switch off track, on --fui-border or --fui-border-strong--fui-field-border, a boundary of at least 3:1 on every planeA value change; review screens
--fui-scrollbar-track, --fui-scrollbar-thumb, --fui-scrollbar-thumb-hover (computed from the palette)transparent, var(--fui-border-strong), var(--fui-field-border)A value change; review screens
--fui-bg-hover ink 1 at 7% / 9%Ink 1 at 6% / 8%A value change; review screens
--fui-bg-active ink 1 at 14% / 16%Ink 1 at 11% / 14%A value change; review screens
--fui-opacity-disabled 0.50.45A value change; review screens
--fui-control-selected-bg ink 1 at 12% / 14%The selection colour (--fui-control-checked-bg) at 12%A value change; review screens
--fui-control-selected-border transparentvar(--fui-control-checked-bg), the selection ringA value change; review screens
--fui-control-checked-bg var(--fui-color-accent)The brand clamped and solved to 3:1 on every plane (#3d5ae8 / #587cff at the default)A value change; review screens
--fui-focus-ring-color (fixed per mode)The brand clamped and solved to 3:1 (#3d5ae8 / #6890ff)A value change; review screens
--fui-link-ink #9a3412 / #f56138var(--fui-color-accent-text)A value change; review screens
--fui-color-on-accent (#000000 on the old brand)Black or white, picked at relative luminance 0.1791 (#ffffff on the default)A value change; review screens
--fui-color-accent-hover / -active #df4d22 / #ca3800The accent mixed away from its label at 86% / 76% (#344dc8 / #2e44b0)A value change; review screens
--fui-color-{accent,danger,success,warning,info}-tint (seed at 18% over transparent) and -washThe seed mixed into the surface at the Glass percentages; opaqueA value change; review screens
--fui-color-{accent,danger,success,warning,info}-text (a 72% mix with black)The seed clamped in lightness and solved to 4.5:1 on every plane and its own tint; -border follows itA value change; review screens
--fui-color-{danger,success,warning,info}-on-fill, -fill-hover, -fill-active, --fui-color-danger-hoverBlack or white picked on the seed; hover and press move away from that labelA value change; review screens
--fui-control-height-{xs,sm,md,lg} literal 24 / 28 / 32 / 40pxcalc(var(--fui-scale, 1) * 1.5rem) to 2.5rem: rem against the host, scaled with --fui-scale, and the one height family (same values at a 16px host and scale 1)None at the default scale and host
--fui-shadow-sm / -md / -lg: a second value under :root[data-theme="dark"] and :root.darkOne value with light-dark() colour stops: it follows color-scheme, so dark scopes and an OS-dark page with no data-theme get the dark shadowAn override on :root now applies in both modes; to keep a light-only override, write its colour stops as light-dark(<yours>, <dark default>)
--fui-button-primary-base, -hover, -active, -color reading the accent ramp directlyThe same colours through the primary chrome switch (color-mix() on --_fui-chrome); data-chrome="ink" paints them in inkThe same colour under the default chrome; read the computed value, not the declared one
--fui-seed-neutral chroma used in full by planes, inks and linesChroma capped at 0.04 in the plane expressionA value change only for a neutral above chroma 0.04; review screens
configureTheme({ neutral }): a palette name, never written"paper" or any CSS colour, written as --fui-seed-neutral; stone, ice, sand, earth, fire and fragments warn and do nothingDelete a retired palette name. Pass a colour to tint the planes
Root ThemeProvider leaving color-scheme on <html> to the stylesheetWrites color-scheme inline on <html> with the resolved modeDelete app code that sets document.documentElement.style.colorScheme beside the provider
html { font-size: 14px } (the library set the root)No root font size: the host's size is 1rem (16px by default); type, space and control heights are rem against itRem the app writes itself was 14px per rem; multiply it by 0.875 to keep its size, and set font-size: var(--fui-type-ui-standard-size) on body to keep 14px body text. Never set a 14px root: every library length shrinks by an eighth
$fui-base-font-size / --fui-base-font-size 14px16px, the host the rem values are written against (spacing.baseFontSize)A value change; review screens
Component padding and gaps on the 7px grid (7 / 14 / 21 / 28px and the rest)The nearest raw-space step (6 / 12 / 20 / 24px), the same table as the --fui-space-* aliasesA value change; review screens
--fui-popup-padding, --fui-popup-item-padding-block 7px6px (--fui-raw-space-6)A value change; review screens
--fui-popup-item-padding-inline 14px12px (--fui-raw-space-12)A value change; review screens
--fui-kbd-padding-x 3px4px (--fui-raw-space-4)A value change; review screens
--fui-kbd-font-size 10px11px (--fui-type-caption-size)A value change; review screens
--fui-type-*-size and -line in px; --fui-type-*-weight a numberrem against the host (the same px at 16px); the weight reads var(--fui-font-weight-normal, 400) or var(--fui-font-weight-semibold, 600)None at a 16px host
--fui-type-caption-* 12 / 16px, tracking 011 / 16px, tracking 0.005emA value change; review screens
--fui-type-ui-compact-* 12 / 16px, weight 500, tracking -0.01em11 / 16px, weight 400, tracking 0.005emA value change; review screens
--fui-type-ui-standard-* weight 500, tracking -0.01emweight 600, tracking 0.005emA value change; review screens
--fui-type-body-compact-* 14 / 20px, tracking 012 / 18px, tracking 0.005emA value change; review screens
--fui-type-body-relaxed-* tracking 0tracking 0.005emA value change; review screens
--fui-type-title-sm-* 16 / 24px, tracking -0.01em15 / 20px, tracking -0.005emA value change; review screens
--fui-type-title-md-* tracking -0.02emtracking -0.005emA value change; review screens
--fui-type-title-lg-* tracking -0.02emtracking -0.005emA value change; review screens
$fui-font-sans / --fui-font-sans "Onest Variable", Onest, -apple-system, … (the face never shipped)The system stack: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serifTo keep a brand face, load it and set --fui-font-sans on :root
--fui-icon-lg 20px18pxA value change; review screens
--fui-field-inline-inset-md 12px10px; new --fui-field-inline-inset-xs (8px), so the track is 8 / 10 / 10 / 16A value change; review screens
$fui-duration-spin / --fui-duration-spin 800ms700msA value change; review screens
$fui-duration-shimmer / --fui-duration-shimmer 2000ms1600msA value change; review screens
--fui-radius-{sm,md,lg,xl} under data-fui-radius-style="subtle": rem on the 14px root (1.75 / 3.5 / 5.25 / 7px)Whole px: 2 / 4 / 6 / 8pxA value change; review screens
--fui-radius-{sm,md,lg,xl} under data-fui-radius-style="default": rem on the 14px root (3.5 / 6 / 8 / 12px)Whole px: 4 / 6 / 8 / 12pxA value change; review screens
--fui-radius-{sm,md,lg,xl} under data-fui-radius-style="rounded": rem on the 14px root (5.25 / 7 / 10.5 / 14px)Whole px: 6 / 8 / 12 / 16pxA value change; review screens
--fui-radius-{sm,md,lg,xl} under data-fui-radius-style="pill": rem on the 14px root (7 / 10.5 / 14 / 21px)Whole px: 8 / 12 / 16 / 24pxA value change; review screens
@property registrations for --fui-space-*, --fui-font-size-*, --fui-font-weight-medium / -bold and the control heightsNot registered: a rem initial value depends on the root, and deprecated aliases are never registeredA transition on one of these now jumps instead of interpolating
tokens.css spacing and type twins --fui-space-* / --fui-font-size-*--fui-raw-space-* / --fui-type-*-sizeThe engine suggests the new names
Sass internals: computed.$computed-space-* and the never-emitted icon key of measurements.$legacyDeleted: $fui-space-* reads the raw-space scaleRead measurements.raw-space() or $fui-control-height-*
Header inner measure calc(var(--fui-space-12) * 31) 2604px; inline padding clamp up to 84px1984px (31 × --fui-raw-space-64); the clamp tops out at 64pxA value change; review screens wider than 1984px
Sass internals: computed.$computed-shadow-md / -lg and computed.$computed-dark-shadow-md / -lgDeleted: $fui-shadow-popup and $fui-dark-shadow-popupRead var(--fui-shadow-popup, $fui-shadow-popup)
--fui-card-shadow (inset top highlight), --fui-card-elevated-shadow (highlight plus two drop layers), --fui-card-active-shadow (inset press), --fui-card-panel-shadow, --fui-card-stat-shadownone: a surface casts no shadowA value change; review screens
Popups, menus, dialogs, drawers, the floating Chart tooltip, Toast, the NavigationMenu viewport, the ColorPicker popup, the mobile Sidebar, Prompt and Box shadow="md" / "lg" on --fui-shadow-md or -lg--fui-shadow-popupA value change; review screens
The pressed Switch and Slider thumbs on --fui-shadow-md--fui-shadow-sm, the thumb shadow at restA value change; review screens
data-fui-radius-style writing --fui-radius-sm / -md / -lg / -xlWrites only --fui-radius (5px under default, the profile's md step otherwise); every radius role derives from itRead a radius role
Floating surfaces (overlay.surface: Popover, Menu, Select, Combobox, Command, DatePicker, ColorPicker, Dialog, Drawer, Toast): raised fill, hairline border, --fui-radius-l1, the popup shadowShadow only: --fui-bg-elevated, --fui-shadow-popup, --fui-radius-popup (-overlay for a modal or side panel), no border; forced colours draw an outlineA value change; review screens
Static surfaces (Card, BentoGrid item, the in-flow pick list): raised fill, translucent edge, row radiussurface.plane: --fui-bg-primary, the opaque --fui-border hairline, --fui-radius-surface (the in-flow pick list keeps --fui-radius-popup), no shadowA value change; review screens
Card soft and outline, the tone capsulesoft sits on the band, outline is transparent; the capsule paints only its hairline in --fui-color-*-text, no radial washA value change; review screens
Field shell fill --fui-bg-primary at --fui-radius-md--fui-field-bg (the sheet in light, the band in dark) with --fui-field-border, at --fui-radius-controlA value change; review screens
Pick-list rows (Menu, Select, Combobox, Command, Listbox) at --fui-radius-l2--fui-radius-rowA value change; review screens
Inline code (prose, Markdown) on --fui-bg-tertiary at --fui-radius-sm--fui-bg-secondary at --fui-radius-indicatorA value change; review screens
Button outline rest fill (the raised plane at 42%) and the neutral soft fill --fui-bg-secondaryoutline rests on --fui-bg-primary; neutral soft rests on --fui-bg-hover; soft has no edge until more contrast gives it --fui-border-strong (--fui-button-soft-border)A value change; review screens
Table rows painted the canvas (--fui-app-canvas-bg)Transparent over the plane the table sits on; the stripe and row states paint over itA value change; review screens
--fui-sidebar-item-active-bg var(--fui-bg-active), --fui-sidebar-item-active-border transparentvar(--fui-control-selected-bg) and var(--fui-control-selected-border): the current item is the selectionTo keep the press tint, set both on Sidebar.Root
Input key cap: raised fill, hairline edge, --fui-kbd-shadowThe press tint --fui-bg-active, no edge, no shadowA value change; review screens
--fui-popup-offset set on the popup container as var(--fui-raw-space-4)A root token from the popup measurement target, 4px × --fui-scale; POPUP_OFFSET_PX reads the same targetThe value is unchanged at scale 1
EmptyState inline measure 240 / 320 / 400px by size (the recipe fallbacks of --fui-feedback-empty-max-inline-*)40ch at every size, set as --fui-feedback-empty-max-inline-sm / -md / -lgA value change; review screens
Target mixin hit area (target.hit-area("micro" | "compact")): the role's own track, 24 / 32px × --fui-scale under every pointermax(track, --fui-hit-area): unchanged for a pointer at scale 1, never below 24px at a smaller scale, and 44px under a coarse pointerA value change; review touch screens
target.hit-size("touch") (48px, the coarse-pointer override in Switch and Slider)Removed: hit-size("micro" | "compact") already rises to 44px under a coarse pointerDelete any @media (pointer: coarse) block that sets a hit size from hit-size("touch"); the role covers it
Pick-list row (Menu, Select, Combobox, Command, Listbox) under a coarse pointer: 48pxmax(--fui-popup-row-pitch, --fui-hit-area): 44px under a coarse pointer, the pitch otherwiseA value change; review touch screens
--fui-focus-ring-color registered as a <color> (@property)An unregistered custom property, so it can hold Highlight under forced colours; it no longer interpolates in a transitionDo not transition the ring colour; a stylesheet that registered it again must use syntax: "*"
Under (forced-colors: active): --fui-focus-ring-color and the edge roles kept their light or dark valuesThe ring is Highlight; --fui-border (-default, -strong), --fui-field-border, --fui-control-selected-border, --fui-field-selection-border are CanvasTextA value change under forced colours only
Component, recipe and @property CSS shipped unlayered, so a library selector with more specificity beat a consumer classEvery library rule sits in a cascade layer (fui.tokens, fui.base, fui.components), so unlayered consumer CSS always winsRemove !important and doubled selectors written to beat library styles; a consumer stylesheet in its own layer must order that layer after fui.components
Button and IconButton press: scale: 0.96 while active--fui-press-scale (0.985), never on a popup trigger, a pending or disabled action, or under reduced motionNone needed; to keep a deeper press set --fui-press-scale on :root
Globals :focus-visible: a 1px ring at 24% of the ring colour, two halo shadowsThe one focus ring: 2px solid, 2px outside, in --fui-focus-ring-colorA consumer element that relied on the faint ring now shows the full ring; remove a local outline: none only through focus.delegated
Field focus: the edge turned --fui-color-accent, the ring the state colour at 34%The edge keeps its state colour; the ring is the focus ring in every stateNone needed
Invalid field edge: --fui-color-danger--fui-color-danger-textNone needed
Current nav item (Header, NavigationMenu, Pagination, TableOfContents, Sidebar): the selection wash; NavigationMenu also turned the text accent--fui-bg-active, ink 1, the semibold weight, no ringNone needed
--fui-table-row-selected-border: var(--fui-border-strong)var(--fui-control-selected-border)To keep the old edge, set --fui-table-row-selected-border: var(--fui-border-strong)
--fui-sidebar-item-active-bg: var(--fui-control-selected-bg)var(--fui-bg-active)To keep the old fill, set --fui-sidebar-item-active-bg: var(--fui-control-selected-bg)
Nav panel rows, table rows and Sidebar items: their own track under every pointermax(own track, --fui-hit-area): unchanged for a pointer, 44px under a coarse pointerNone needed; a fixed-height layout of rows on touch devices grows
IconButton hit area: 32px on sm only, none on md and lgEvery size draws max(own track, --fui-hit-area): sm is its own 28px track for a pointer, every size 44px under a coarse pointerNone needed; icon buttons packed closer than 44px apart on touch screens now overlap their hit areas, so space them with a Stack gap
Select and Combobox: a highlighted selected row filled with --fui-field-selection-bg-hoverThe wash and ring stay, with the hover tint painted over them; Select and Combobox no longer read --fui-field-selection-bg-hover (Chip and DatePicker still do)A theme that set --fui-field-selection-bg-hover for pick-list rows should set --fui-field-selection-bg or --fui-bg-hover instead
Small popups (Menu, Select, Combobox, Popover, Tooltip, DatePicker, ColorPicker, Header nav menu): fade plus scale(0.95) (and a few pixels of travel on Popover and Tooltip) over --fui-transition-fast (160ms)Still: appear and leave in 0ms (--fui-duration-enter / -exit), no scale, no travel, through popup.motionNone needed; a consumer popup that wants the old grow should own its motion rather than restyle a library popup
Dialog popup: fade plus scale(0.95) and --fui-anim-offset-md travel, 200ms in and outOpacity only: 200ms in (--fui-duration-enter-lg), 100ms out (--fui-duration-exit-lg), on --fui-ease-standard (overlay.motion)None needed
Drawer panel: slides in and out from its side over --fui-transition-normalFades like the dialog (200ms in, 100ms out); transform moves only with a swipe, and under reduced motion only opacity transitionsNone needed; a test that waited for the slide transform should wait for opacity
Dialog and Drawer backdrop exit: 200ms100ms (--fui-duration-exit-lg), opacity onlyNone needed
Toast entry: toastEnter keyframes (rise plus scale(0.95))Removed: the toast appears in placeNone needed; remove any override of the toastEnter animation
Skeleton keyframes skeleton-shimmer, painted in each modulefui-skeleton-shimmer from recipes/_skeleton.scss (skeleton.band); under reduced motion one still bandRename skeleton-shimmer to fui-skeleton-shimmer in consumer overrides
Loading spinner keyframes spin, stopped under reduced motionfui-loading-spin from recipes/_loading.scss; keeps turning under reduced motion (UIR-D102)Rename spin to fui-loading-spin in consumer overrides
No working recipeworking.area, working.text, working.keyframes; tokens --fui-working-sweep-duration (1800ms) and --fui-working-sheen-duration (2400ms)Additive
Globals under reduced motion: every animation and transition cut to 0.01msOnly smooth scrolling is turned off; each component holds its own motion stillA consumer animation that relied on the blanket must add its own @media (prefers-reduced-motion: reduce) block
Action controls (Button, IconButton, segments, menu triggers) forced to the semibold weight by action.baseLabels take the size step's regular weight; a component that needs the strong weight includes action.weight("strong")A consumer override that expected semibold labels sets font-weight: var(--fui-font-weight-semibold, $fui-font-weight-semibold) itself
action.motion and interactive-base on --fui-transition-fast (160ms) or --fui-transition-normal (200ms)--fui-duration-micro (100ms) on --fui-ease-standard, for background, border, colour, shadow, opacity and scaleNone needed
Hover styles drawn on every pointer, including touchHover drawn only under @media (hover: hover) (action.hover, Tabs ghost and default tabs)A consumer hover override wraps itself in @media (hover: hover)
layout.page-gutter() 24px16pxA page that wants the wider gutter sets its own inline padding with --fui-raw-space-24
boolean.card mixin (recipes/_boolean-range.scss)removed: choice-card.root and choice-card.size($size) from recipes/_choice-card.scssReplace @include boolean.card($size) with @include choice-card.root; @include choice-card.size($size)
ToggleGroup soft: colour-mixed pill track and --fui-form-group-* hooksThe segmented track (band, hairline, control radius, pad 2) with the lifted thumb at the strong weight; the hooks are not readDrop overrides of --fui-form-group-* on soft ToggleGroups
Tabs soft: tinted list and --fui-tabs-pills-* hooksThe same segmented track and thumb as ToggleGroup soft; Tabs.List carries data-sizeDrop overrides of --fui-tabs-pills-*
Box borderColor default / strong / accent / dangerRemoved: border draws the one hairline, --fui-borderDelete the prop. A state edge is an Alert
Segmented selection (soft Tabs, ToggleGroup, Prompt modes; ThemeToggle is cut): the selection wash plus an inset selection ringThe thumb: --fui-bg-primary, a hairline --fui-border ring and --fui-shadow-smNone needed
Link color (primary, secondary, tertiary)tone="neutral": ink 1 at rest, no hover colour change (the underline is the cue); tone="accent" (default) reads the link inkDrop color; color="secondary" or "tertiary" becomes tone="neutral"
--fui-link-underline-offset: 0.2em3px, shared by Link, Button link and prose linksNone needed
ThinkingIndicator kind (dots, pulse, spinner) and the ThinkingKind typeRemoved: one row with the work wash and a selection-colour dotDelete kind
CodeBlock theme and the CodeBlockTheme type (12 bundled highlighter themes)Removed: one css-variables theme that follows the mode through --fui-code-token-*Delete theme. Set --fui-code-token-* to restyle syntax colours
Slider fill on --fui-color-accent--fui-control-checked-bg, the selection fillA value change; review screens
Header heightthe --fui-appshell-header-height tokenMove a custom height to a --fui-appshell-header-height override on a theme scope
Sidebar / Sidebar.Provider width, collapsedWidththe --fui-appshell-sidebar-width and --fui-navigation-sidebar-collapsed-width tokensMove custom widths to token overrides on a theme scope
--fui-sidebar-section-label-size var(--fui-type-body-compact-size)var(--fui-type-caption-size)Value change
bg on AppShell and every slotremoved: the shell is canvas; AppShell.Aside is the surface planeDrop the prop; recolour through theme tokens
AppShell.Header heightthe --fui-appshell-header-height tokenAs for Header height
AppShell.Aside widththe --fui-appshell-sidebar-width-wide tokenMove a custom width to a token override
table-chrome.tree-cell mixintable-chrome.tree-indent (indent from --fui-table-tree-depth)Rename the include
Icon tone="accent" painting the accent filltone="accent" paints --fui-color-accent-textNone needed
Theme / configureTheme neutral="stone" | "ice" | "sand" | "earth" | "fire" | "fragments"neutral="paper" or any CSS colour (retired names are plain strings now)Delete the retired name or set paper
Chart config { label, color: "…" }{ label, series: 1..6 }; draw with var(--fui-chart-N)Map each key to a series index; replace status-hue strokes/fills with series tokens
Chart frame custom properties --chart-<key>removedRead var(--fui-chart-N) for the key's series
:root.dark / :root.light class selectors set the colour schemeremoved: data-theme="dark" / data-theme="light" on <html> (or Theme)Set data-theme instead of the class
DataTable sub-rows with no fill, expand button size="sm" drawn down to 24 by a module overrideSub-rows sit on the band (--fui-bg-secondary, yielding to the selection wash); the expand button is IconButton size="xs"A visual change

Tokens

Tokens: before and now
Before
Now
What to change
--fui-bg-tertiary #f2ede7 / #1e1c19Removed: use --fui-bg-secondary for a well (the band) or --fui-body-bg for the canvasRename to --fui-bg-secondary. A page background reads --fui-body-bg
--fui-main-bgRemoved: use --fui-app-canvas-bg (the canvas, --fui-body-bg)Rename to --fui-app-canvas-bg
--fui-bg-subtle #fdfcfa / #1b1a17Removed: use --fui-bg-secondaryRename to --fui-bg-secondary
--fui-code-bg (its own value)Removed: use --fui-bg-secondary; a code well is the bandRename to --fui-bg-secondary
--fui-border-defaultRemoved: use --fui-borderRename to --fui-border
--fui-border-subtle #3c2d1e12 / #343029adRemoved: use --fui-borderRename to --fui-border
fui-unit(), $fui-base-unit / --fui-base-unit (the 7px grid)Removed: use --fui-raw-space-* (measurements.raw-space() in Sass)Replace fui-unit(N) with the nearest var(--fui-raw-space-M, #{measurements.raw-space(M)})
$fui-space-px / --fui-space-px 1pxRemoved: use --fui-stroke-hairline (1px)Rename to --fui-stroke-hairline
$fui-space-0-5 / --fui-space-0-5 2.1px (on the 14px root)Removed: use --fui-raw-space-2 (2px)Rename to --fui-raw-space-2
$fui-space-0-75 / --fui-space-0-75 3.01px (on the 14px root)Removed: use --fui-raw-space-4 (4px)Rename to --fui-raw-space-4
$fui-space-1 / --fui-space-1 7px (on the 14px root)Removed: use --fui-raw-space-6 (6px)Rename to --fui-raw-space-6
$fui-space-2 / --fui-space-2 14px (on the 14px root)Removed: use --fui-raw-space-12 (12px)Rename to --fui-raw-space-12
$fui-space-3 / --fui-space-3 21px (on the 14px root)Removed: use --fui-raw-space-20 (20px)Rename to --fui-raw-space-20
$fui-space-4 / --fui-space-4 28px (on the 14px root)Removed: use --fui-raw-space-24 (24px)Rename to --fui-raw-space-24
$fui-space-5 / --fui-space-5 35px (on the 14px root)Removed: use --fui-raw-space-32 (32px)Rename to --fui-raw-space-32
$fui-space-6 / --fui-space-6 42px (on the 14px root)Removed: use --fui-raw-space-40 (40px)Rename to --fui-raw-space-40
$fui-space-8 / --fui-space-8 56px (on the 14px root)Removed: use --fui-raw-space-48 (48px)Rename to --fui-raw-space-48
$fui-space-10 / --fui-space-10 70px (on the 14px root)Removed: use --fui-raw-space-64 (64px)Rename to --fui-raw-space-64
$fui-space-12 / --fui-space-12 84px (on the 14px root)Removed: use --fui-raw-space-64 (64px)Rename to --fui-raw-space-64
$fui-font-size-2xs / --fui-font-size-2xs 10pxRemoved: use --fui-type-caption-size (11px)Rename to --fui-type-caption-size
$fui-font-size-xs / --fui-font-size-xs 12pxRemoved: use --fui-type-body-compact-size (12px)Rename to --fui-type-body-compact-size
$fui-font-size-sm / --fui-font-size-sm 14pxRemoved: use --fui-type-ui-standard-size (14px)Rename to --fui-type-ui-standard-size
$fui-font-size-base / --fui-font-size-base 14pxRemoved: use --fui-type-ui-standard-size (14px)Rename to --fui-type-ui-standard-size
$fui-font-size-md / --fui-font-size-md 15pxRemoved: use --fui-type-title-sm-size (15px)Rename to --fui-type-title-sm-size
$fui-font-size-lg / --fui-font-size-lg 20pxRemoved: use --fui-type-title-md-size (20px)Rename to --fui-type-title-md-size
$fui-font-size-xl / --fui-font-size-xl 24pxRemoved: use --fui-type-title-lg-size (24px)Rename to --fui-type-title-lg-size
$fui-font-size-2xl / --fui-font-size-2xl 30pxRemoved: use --fui-type-display-size (32px)Rename to --fui-type-display-size
$fui-font-size-3xl / --fui-font-size-3xl 36pxRemoved: use --fui-type-display-size (32px)Rename to --fui-type-display-size
$fui-font-size-4xl / --fui-font-size-4xl 48pxRemoved: use --fui-type-display-size (32px)Rename to --fui-type-display-size
$fui-font-weight-medium / --fui-font-weight-medium 500Removed: use --fui-font-weight-semibold (600)Rename to --fui-font-weight-semibold
$fui-font-weight-bold / --fui-font-weight-bold 700Removed: use --fui-font-weight-semibold (600)Rename to --fui-font-weight-semibold
$fui-button-height-xs / --fui-button-height-xsRemoved: use --fui-control-height-xs (24px, no value change)Rename to --fui-control-height-xs
$fui-button-height-sm / --fui-button-height-smRemoved: use --fui-control-height-sm (28px, no value change)Rename to --fui-control-height-sm
$fui-button-height-md / --fui-button-height-mdRemoved: use --fui-control-height-md (32px, no value change)Rename to --fui-control-height-md
$fui-button-height-lg / --fui-button-height-lgRemoved: use --fui-control-height-lg (40px, no value change)Rename to --fui-control-height-lg
$fui-input-height-sm / --fui-input-height-smRemoved: use --fui-control-height-sm (28px, no value change)Rename to --fui-control-height-sm
$fui-input-height / --fui-input-heightRemoved: use --fui-control-height-md (32px, no value change)Rename to --fui-control-height-md
$fui-input-height-lg / --fui-input-height-lgRemoved: use --fui-control-height-lg (40px, no value change)Rename to --fui-control-height-lg
$fui-control-track-micro / --fui-control-track-microRemoved: use --fui-control-height-xs (24px, no value change)Rename to --fui-control-height-xs
$fui-control-track-sm / --fui-control-track-smRemoved: use --fui-control-height-sm (28px, no value change)Rename to --fui-control-height-sm
$fui-control-track-md / --fui-control-track-mdRemoved: use --fui-control-height-md (32px, no value change)Rename to --fui-control-height-md
$fui-control-track-lg / --fui-control-track-lgRemoved: use --fui-control-height-lg (40px, no value change)Rename to --fui-control-height-lg
--fui-field-track-smRemoved: use --fui-control-height-sm (28px, no value change)Rename to --fui-control-height-sm
--fui-field-track-mdRemoved: use --fui-control-height-md (32px, no value change)Rename to --fui-control-height-md
--fui-field-track-lgRemoved: use --fui-control-height-lg (40px, no value change)Rename to --fui-control-height-lg
--fui-badge-track-md / -lg override hooks (read with 24 / 28px fallbacks)Removed: the md and lg badges sit on --fui-control-height-xs / -sm; the badge keeps --fui-badge-track-sm (20px), now a real tokenDelete the override, or set --fui-control-height-xs / -sm in the badge's scope
$fui-radius-sm / --fui-radius-sm 4pxRemoved: use --fui-radius-indicator, --fui-radius × 0.6 (3px at the default 5px)Rename to --fui-radius-indicator
$fui-radius-md / --fui-radius-md 6pxRemoved: use --fui-radius-control, min(--fui-radius, control height md ÷ 2) (5px)Rename to --fui-radius-control
$fui-radius-lg / --fui-radius-lg 8pxRemoved: use --fui-radius-row (8px)Rename to --fui-radius-row. A card or panel corner reads --fui-radius-surface, a popup --fui-radius-popup
$fui-radius-xl / --fui-radius-xl 12pxRemoved: use --fui-radius-surface, min(--fui-radius × 2.4, 24px) (12px)Rename to --fui-radius-surface
$fui-radius-l1 / --fui-radius-l1 8px (--fui-radius-lg)Removed: use --fui-radius-surface (12px)Rename to --fui-radius-surface. A popup or overlay corner reads --fui-radius-popup
$fui-radius-l2 / --fui-radius-l2 6px (--fui-radius-md)Removed: use --fui-radius-control (5px)Rename to --fui-radius-control
$fui-radius-l3 / --fui-radius-l3 4px (--fui-radius-sm)Removed: use --fui-radius-indicator (3px)Rename to --fui-radius-indicator
tokens.css radius twins --fui-radius-sm / -md / -lg / -xl--fui-radius-control, -indicator, -surface, -row, -tooltip, -segment (a value an earlier twin already names is left out, so popup and overlay read as surface)Read the role the corner serves; the old names are removed
$fui-shadow-md / --fui-shadow-md (two layers: 0 2px 4px -1px, 0 1px 3px -2px)Removed: use --fui-shadow-popup (three layers: a 0.5px ring, 0 2px 5px, 0 10px 24px)Rename to --fui-shadow-popup on a floating layer. Delete it from a surface, which casts no shadow
$fui-shadow-lg / --fui-shadow-lg (two layers: 0 8px 12px -3px, 0 3px 5px -4px)Removed: use --fui-shadow-popupRename to --fui-shadow-popup on a floating layer. Delete it from a surface
$fui-transition-fast / --fui-transition-fast 160ms cubic-bezier(0.25, 0.46, 0.45, 0.94)Removed: use var(--fui-duration-micro) var(--fui-ease-standard): 100ms cubic-bezier(0.2, 0, 0, 1)Rename to var(--fui-duration-micro) var(--fui-ease-standard)
$fui-transition-normal / --fui-transition-normal 200ms cubic-bezier(0.4, 0, 0.2, 1)Removed: use var(--fui-duration-enter-lg) var(--fui-ease-standard): 200ms cubic-bezier(0.2, 0, 0, 1)Rename to var(--fui-duration-enter-lg) var(--fui-ease-standard)
$fui-anim-offset-sm / --fui-anim-offset-sm, -mdRemoved: use --fui-popup-travelRename to --fui-popup-travel
--fui-app-main-bg, --fui-app-sidebar-bg, --fui-sidebar-bgRemoved: use --fui-app-canvas-bgRename to --fui-app-canvas-bg
--fui-card-* and --fui-panel-* (-bg, -stat-bg, -border, -hover-border, -header-bg, -radius, -accent-*, -shadow, -elevated-shadow, -active-shadow, -panel-shadow, -stat-shadow)Removed: a card is surface.plane (--fui-bg-primary, --fui-border, --fui-radius-surface, no shadow); a header band reads --fui-bg-secondaryReplace each read with the plane, line or radius role it served; delete shadow overrides
--fui-form-group-* (-bg, -border and the rest)Removed: form-group-surface paints --fui-bg-secondary with --fui-border and --fui-radius-surface-bg → --fui-bg-secondary, -border → --fui-border
--fui-table-bg, --fui-table-border, --fui-table-header-bg, --fui-table-header-border, --fui-table-subrow-bgRemoved: rows are transparent over their plane; header and subrow fills read --fui-bg-secondary; lines read --fui-border / --fui-border-strongSet the plane on the table's container instead of the table
--fui-tabs-pill-* / --fui-tabs-pills-*, --fui-header-search-bgRemoved: the track reads --fui-bg-secondary; Header search reads --fui-bg-hoverDelete the override
--fui-kbd-bg, --fui-kbd-border, --fui-kbd-shadow, --fui-kbd-radiusRemoved: the key cap is the press tint --fui-bg-active with no edge and no shadow, at --fui-radius-indicatorDelete the override
--fui-skeleton, --fui-skeleton-hiRemoved: the bar reads --fui-bg-secondary with --fui-bg-active as its highlight--fui-skeleton → --fui-bg-secondary, -hi → --fui-bg-active
--fui-button-radius, --fui-button-neutral-* (-bg, -bg-hover, -bg-active, -color, -border, -border-hover, -shadow, -shadow-active), --fui-button-outlined-shadow, --fui-button-primary-shadow, -shadow-activeRemoved: no button shadow; neutral reads the neutral channels (--fui-bg-hover rest, --fui-bg-active hover and press); corner --fui-radius-controlDelete neutral and shadow overrides; restyle the primary action through --fui-button-primary-*
--fui-badge-radius, --fui-sidebar-item-radiusRemoved: --fui-radius-indicator (Badge), --fui-radius-control (Sidebar rows)Rename to the role
--fui-sidebar-border, --fui-sidebar-footer-borderRemoved: use --fui-borderRename to --fui-border
--fui-field-selection-bg-hover, --fui-field-selection-borderRemoved: hover paints --fui-bg-hover over the selection (selection.hover-tint); the edge is --fui-control-selected-borderDelete the override
--fui-color-{accent,info,success,warning,danger}-borderRemoved: a tone line reads --fui-color-*-textRename -border to -text
Skeleton keyframes fui-skeleton-shimmer (a travelling highlight) and the --fui-skeleton / --fui-skeleton-hi hooksfui-skeleton-pulse: the press tint pulses over the band on the bar's ::after; the hooks are not readRename fui-skeleton-shimmer to fui-skeleton-pulse in consumer overrides; a consumer ::after on a skeleton bar must move to a wrapper
--fui-badge-dot-colorRemoved: the dot paints the tone ink (currentColor)Delete the override
--fui-feedback-empty-max-inline-lg, the feedback.empty-state Sass recipeRemoved with size="lg": --fui-feedback-empty-max-inline-sm / -md remain, and EmptyState authors its own sizesRename the override to -md. Replace an @include feedback.empty-state(...) with the EmptyState component
navigation.active-indicator($placement), navigation.active-rail(), navigation.active-dot(), --fui-navigation-active-rail, --fui-navigation-active-dotremoved: the current row is the selection wash and ring, with no rail or dotDrop the include; mark the current row with aria-current
Sidebar activeIndicator and the --fui-sidebar-item-active-indicator tokenthe current item takes the selection wash and ringDelete the prop and token overrides
--fui-table-row-bg hook and the --_fui-table-* density tracksRemoved: --fui-table-row-track (one track) and the --fui-table-row-hover-bg / -active-bg / -selected-* hooksMove row fills to the hover, active and selected hooks
--fui-progress-diameter, --fui-progress-dash-full, --fui-progress-dash-quarterRemoved with the ringDelete overrides of these properties
--fui-stack-direction*, --fui-stack-gap*removed (private --_fui-stack-collapse)Set gap / collapseBelow instead of the property
--fui-grid-cols*, --fui-grid-min-child-widthremoved (private --_fui-grid-min, --_fui-grid-max, --_fui-grid-gap)Set columns / minChildWidth / gap props

Behaviour to review

Behaviour to review: before and now
Before
Now
What to change
Button labels at every size, the small Badge and the Tooltip at weight 500600, the strong weight (buttons, badges, tooltips and field labels)A value change; review screens
Text scale 2xl / 3xl / 4xl (30 / 36 / 48px) and weight medium / bold (500 / 700)32px (the display role) and 600 (semibold); the props stay until the v4 cut trims the unionsUse scale="2xl" for display text and weight="semibold" for strong text
surface-elevated mixin (tokens/_mixins.scss)Removed: surface.plane (recipes/surface) for a static surface, overlay.surface for a floating oneInclude the recipe the surface is
Dialog and Drawer footer: a hairline top edge on the bandNo band: the footer sits on the overlay plane with 12px below, and a hairline top edge shows only while the body above it overflowsA value change; review screens
Chip selected: the field-selection fill, a darker hover, a ring only on pressThe selection wash plus its ring at rest, the hover tint painted over bothA value change; review screens
Button pending: aria-busy="true" styled by the consumer, label and width free to changepending prop: nothing for 1s, then the label hides in place, a spinner shows, the width holds and presses are swallowed<Button aria-busy="true"> → <Button pending>
Disabled Button: always removed from the tab orderfocusableWhenDisabled keeps it focusable with aria-disabled="true" (default unchanged)Pass focusableWhenDisabled where an unavailable action should still be found by keyboard and announce why
Action md inline inset 12px, icon-side inset 10px10px and 8pxNone needed; a Button pinned to an exact width may lose 4px
Button and IconButton label at md and lg: the 14px ui-standard role11px (ui-compact) at micro, sm and md; 12px (body-compact) at lg; the strong weight at every sizeNone needed; set size="lg" where a larger label is wanted
Field hover: no change; read-only: same as editableHover darkens the edge toward ink 1; read-only is a dashed edge on the field fill (the sheet in light, the band in dark)None needed
Popover sideOffset default 8; Tooltip sideOffset default 64 (POPUP_OFFSET_PX) for bothPass sideOffset={8} or sideOffset={6} where the old gap mattered
Popup positioners: the headless library's collision padding (5px)8px (POPUP_COLLISION_PADDING_PX) on every popup positionerNone needed; a popup at a viewport edge now sits 8px in
Menu viewport: block-size transition when a submenu changes the list heightRemoved: the height changes in the same frameNone needed
Loading delays decided per calleruseLoadingPhase(loading) returns idle, quiet (under 1s), loading or slow (past 10s); LOADING_DELAY_MS, LOADING_SLOW_MSAdditive
Alert dismiss: the alert unmounts and focus falls to the pageFocus moves to the next focusable item (else the one before) in the same frame; useDismiss and nextFocusTarget exported for other dismissible surfacesNone needed
useAnnounce: two live regions per mounted hook, created with their first messageOne shared polite and one shared assertive region per document, mounted empty; repeats before they land are read onceNone needed; a test that queried the hook's own region should query [data-fui-announcer]
No errbox recipe: errors drawn per surfacefeedback.errbox, errbox-icon, errbox-words, errbox-actions mixinsAdditive
Tabs indicator and Prompt beam under reduced motion: still only through the globals blanketTheir own reduced-motion blocks: the indicator jumps, the beam holds stillNone needed
Current nav item (navigation.link-active, selection.current): press tint, ink 1, strong weight, no ringThe selection wash plus the 1px selection ring, ink 1, regular weight (Header, Pagination, TableOfContents, NavigationMenu)None needed; screenshots of nav change
Outline Checkbox and RadioGroup item: raised fill, outer accent ring when checkedThe surface fill and hairline; checked takes the selection wash and its hairline becomes the 1px selection ringNone needed
Card / Card.Body padding sm / lgpadding none or md (the compact inset)sm and lg become md
Box padding none-xlBox inset none / compact (12) / default (16)none stays; xs and sm become compact; md and up become default
Box paddingX / paddingYRemoved: use insetPick the inset role that matches the larger axis
Box rounded fullRemoved: pills belong to Badge, Chip and AvatarUse the component that owns the pill
Box borderTop / borderBottom / borderLeft / borderRightBox border block-start / block-end (logical sides)borderTop becomes border="block-start", borderBottom becomes border="block-end"; by hand for left and right
Avatar size xs / sm / md / lg / xl (24 / 32 / 40 / 48 / 64)Avatar size xs / sm / md / lg on the control tracks (24 / 28 / 32 / 40), default xsKeep the step whose pixel size is nearest; xl becomes lg
Avatar.Group overlap (one step for every size)Per size: none on xs (the tiles meet and the ring parts them), sm 2px, md 2px, lg 4px, so no stacked avatar covers a wide pair of initials beside itA value change; review screens
Alert tone successRemoved: a pass is a Badge verdict, a confirmation is a ToastMove a confirmation to Toast and a standing pass to a Badge
AlertRoot, AlertIcon, AlertBody, AlertTitle, AlertContent, AlertActions, AlertAction, AlertClose, useAlertContextAlert, Alert.Icon, Alert.Body, Alert.Title, Alert.Content, Alert.Actions, Alert.Action, Alert.CloseRewrite the import and the JSX tag; useAlertContext has no replacement
EmptyState size lgEmptyState size md: size changes padding onlylg becomes md
EmptyStateRoot, EmptyStateIcon, EmptyStateTitle, EmptyStateDescription, EmptyStateActions, useEmptyStateContextEmptyState, EmptyState.Icon, EmptyState.Title, EmptyState.Description, EmptyState.ActionsRewrite the import and the JSX tag
Button icon (icon-only square button)IconButton with aria-label<Button icon aria-label="X">{glyph}</Button> becomes <IconButton aria-label="X">{glyph}</IconButton>
IconButton pressed (a styling flag)A real toggle: pressed, defaultPressed or onPressedChange set aria-pressed, and pressed paints the selection wash and ringA call site that toggled pressed from its own onClick moves the update to onPressedChange
ButtonGroup without a namerole="group" with a required aria-label or aria-labelledbyName each ButtonGroup for what its buttons act on
Chip as a button alwaysA static <span> unless selected or onClick is set (or it sits in a Chip.Group); then a toggle button with aria-pressed. A removable chip's remove control is its own button, named by removeLabelA chip that must be focusable passes selected or onClick
ToggleGroup as a radiogroup of radios with aria-checkedA role="group" of toggle buttons with aria-pressed; a chosen segment stays chosen when pressed againTests that query radiogroup or radio query group and button with aria-pressed
ToggleGroup size: sm, md, lgxs, sm, md, lg; new fullWidthNone needed
Input shortcutBehavior="focus-input" (global focus hotkey)Removed: shortcut is a display-only hint; the app registers the keyRegister the hotkey in the app and focus the input through a ref
Input size sm | md | lgxs | sm | md | lg (24, 28, 32, 40)Additive
Select variant="ghost"Removed: style a quiet trigger in the consuming component, or use Menu with a Button triggerGive Select.Trigger a class that clears the edge and fill, or rebuild as Menu
Select size sm | md | lgxs | sm | md | lg (24, 28, 32, 40)Additive
Select list over the trigger (item-aligned)The list opens beside the triggerReview screens
Combobox built-in "No results found""No match for “query”", or "No options" with nothing to filter; Combobox.Empty still overridesReview copy assertions in tests
Input.RootInput (it has no parts)<Input.Root to <Input
Form.RootForm (it has no parts)<Form.Root to <Form
Field, ColorPicker and Slider labels each styled on their ownOne field.label recipe: body-compact, semibold, ink 1; a disabled field dims the label onlyA value change; review screens
ColorPicker error text as a styled helper lineField.Error (announced, in aria-describedby); helper text is Field.DescriptionMarkup change. Tests query the error by its text or aria-describedby, not a class
Dialog.Content width="xl" / width="full"width is sm · md · lgxl and full become lg
A confirm or delete flow built on DialogAlertDialog (role="alertdialog", no outside-press dismiss, focus on AlertDialog.Cancel, a 500ms settle window on AlertDialog.Action)Move the question to AlertDialog with AlertDialog.Cancel and AlertDialog.Action
Dialog sheet: bordered modal shell, title 20/28, 24px section insetsThe raised sheet on the popup shadow with no edge, title 15/20, 16px insets, a body that scrolls on its own and joins the tab order only when it overflowsA visual change; review screens
Drawer.Content side="left" / side="right"side="start" / side="end" (follow the writing direction)left becomes start, right becomes end
Drawer.Content side="top"Removed: start · end · bottomMove a top sheet to bottom or to Dialog
Drawer.Content viewportPropsRemovedStyle the drawer through Drawer.Content className
Popover.Content size="lg"size is sm · mdlg becomes md
Popover.Content positionerClassNameRemovedStyle the panel through Popover.Content className
Popover panel border and bordered arrow; title 14/600The raised panel on the popup shadow with no edge; the arrow is fill only; title 12/550A visual change
Tooltip closeOnClick default falseDefault true: a press on the trigger closes the tooltipPass closeOnClick={false} where the tooltip must stay through a press
Menu.Item dangertone="danger"danger becomes tone="danger"
Menu.Item checkedMenu.CheckboxItem / Menu.RadioItem (a check in the leading column)Move a checked row to Menu.CheckboxItem checked or a Menu.RadioGroup
Menu radio dot indicatorA check, like every selected menu rowA visual change
Toast tone="info" / useToast().info()tone="neutral" / useToast().toast()info becomes neutral; info(…) becomes toast({ … })
Toast: tone wash, tone border and tone ink; action as a link button; role="alert" for warningThe raised plane with no edge; the tone is the icon's ink only; the action is a small ghost button; only danger announces urgentlyA visual and announcement change
Command root role="search", Command.Empty role="option", no active row until an arrow keyNo landmark; Empty is not an option; the first match is active on mount and after every search change, sorted by scoreBehaviour; Return now runs the first match
Command.Separator line and group linesSpace between groups; group labels separate themA visual change
Command search as a 40px track with a hairline belowThe search is a field inside the palette (field shell, hover and focus ring), inset by the popup insetA value change; review screens
Menu.Content without a busy stateloading on Menu.Content: busy at once, and after a second a spinner row below the rows it hasAdditive
Loading overlayRemoved: dim the waiting content in place, or use Loading.Screen for a full-page waitReplace the overlay with the content's own busy state, or Loading.Screen
Loading shows at onceShows after delay (default 1000ms); the status region stays mounted and empty until thenPass delay={0} where the spinner must show at once (tests, fixtures)
useThinkingIndicatorContextRemovedRead active from your own state
Error state as styling on the messagestatus="error" adds Message.Error (an alert with Try again when onRetry is set)Pass onRetry; move custom error copy into <Message.Error>
ConversationList autoScroll={true} (always follow) and AutoScrollBehavior booleanautoScroll "smart" | false; "smart" is the defaultautoScroll={true} to autoScroll="smart"
ConversationList loadingHistoryhistory ("idle" | "loading" | "error") with onRetryHistoryloadingHistory={x} to history={x ? "loading" : "idle"}
ConversationList onScrollTop(event)onScrollTop() with no argumentRead scroll position from your own ref if the handler used the event
useOptionalConversationListRemoved: useConversationList inside the listCall useConversationList only inside ConversationList
CodeBlock title as a label above the blocktitle in the header inside the blockNone needed; screenshots change
CodeBlock captionRemoved: put the caption in the surrounding textMove the caption into a Text after the block
CodeBlock compactsize="sm"compact to size="sm"
Copy result as an aria-label swap (Copied!)A visible status: Copied, or Couldn't copy in the danger ink, for two seconds; the button stays Copy codeTests query role="status" instead of the button name
ConversationList history rows (history="loading", history="error") inside the scrolling logAbove the log, outside role="log", so they stay in view and are not announced as messagesMarkup change
RadioGroup error (string)invalid plus errorMessageerror="x" becomes invalid errorMessage="x"
Switch label and helper inside the switch elementBeside it, in a label row; the switch is named by aria-labelledbyReview tests that read the switch's text content
Slider valueSuffixformat (Intl.NumberFormatOptions) and localevalueSuffix="%" on a 0–100 scale becomes max={1} format={{ style: "percent" }}; units use style: "unit"
Slider fallback accessible name "Slider"Removed: a development warning asks for label, aria-label or aria-labelledbyGive every unnamed slider a label or aria-label
Tabs.List variant and sizeThe root's variant and size; the list follows themMove variant and size from <Tabs.List> to <Tabs>
Tabs size="lg"md (Tabs sizes are sm and md; lg resolves to md)size="lg" → size="md" on <Tabs>
Tabs orientation="vertical"Removed: tabs are horizontal; a vertical index is TableOfContents or SidebarReplace vertical Tabs with TableOfContents (in-page) or Sidebar (routes)
useCollapsibleContextRemoved: read open from your own state with open/onOpenChangeControl the Collapsible and read the state you own
Collapsible onOpenChange(open)onOpenChange(open, eventDetails)Additive
ScrollArea scrollbarVisibility="hover"auto: shows while scrolling, on hover and while focus is inside"hover" → "auto"
Pagination size="lg"md (Pagination sizes are sm and md)size="lg" → size="md"
TableOfContents hideTitletitle={null}hideTitle → title={null}
TableOfContents title default "On This Page""On this page"A copy change
TableOfContents.Group disabledcollapsible={false}disabled → collapsible={false}
Header position="fixed"position="sticky", or AppShell, where the header row never scrollsposition="fixed" to position="sticky"; remove the page's top padding that made room for it
AppShell.Sidebar width, collapsedWidth, position, activeIndicatorthe sidebar width tokens; position and activeIndicator are gone as on SidebarAs for the Sidebar rows
recipes/table-chrome.ts: TableDensity, TableDensityInput, LegacyTableSize, TABLE_ROW_TRACKS, resolveTableDensity, resolveTableRowTrackTABLE_ROW_TRACK (32)Read TABLE_ROW_TRACK where a row height is needed
DataTable default empty copy "No data available""No data"A copy change
Skeleton.Circle<Skeleton shape="avatar" size=…> (a square at the indicator radius)<Skeleton.Circle size="md"> to <Skeleton shape="avatar" size="md">
Skeleton avatar sizes 32/40/4824/32/40 (Avatar xs/sm/md)A size change; review screens
Progress fill by animated inline-size; indeterminate 50% ping-pong, parked under reduced motionscaleX from the start edge; a 30% travelling run, full width at half strength under reduced motionA motion change; review screens
Progress tone="neutral" filled in ink 3The inverse plane (the strongest neutral), so a meter reads against its trackA value change; review screens
Icon weight free string (thin, light, duotone)weight: "regular" | "bold" | "fill"thin/light → regular, duotone → fill
Image rounded="full"removed (use Avatar for a round picture)A person or a round thumbnail is an Avatar
Image aspectRatio="21:9"removed (use 16:9)21:9 → 16:9
Image objectFit="fill" / objectFit="none"removed (cover or contain)fill → cover, none → contain
Separator inner .line spanthe separator element is the ruleSelectors that styled the inner span target the separator itself
Separator label inside role="separator"label is a sibling of the rule; only the first rule has the roleQueries for the label inside the separator role look beside it
Stack gap={1…8}gap="xs" (1) · "sm" (2) · "md" (3) · "lg" (4) · "xl" (6); 5, 7 and 8 have no stepMap 1–4 and 6; by hand for 5, 7, 8
Stack gap={{ base, sm, md, lg, xl }}one gap tokenPick one gap; let collapseBelow change the flow
Stack direction={{ base: "column", md: "row" }}collapseBelow="<length>" (answers the Stack's own width, not the viewport)Choose the width below which the row folds
Stack data-direction-base…xl attributesremovedStyle hooks on these attributes target the direction class
Grid columns={5 | 7 | 8 | 9 | 10 | 11}columns={1 | 2 | 3 | 4 | 6 | 12} or "auto"Pick the nearest kept count or auto-fill
Grid columns={{ base, sm, md, lg, xl }}columns={N} minChildWidth="<length>" (a ceiling answering the grid's own width)Largest count becomes N; choose the narrowest track
Grid gap={1…8}gap="xs" | "sm" | "md" | "lg" | "xl"1→xs, 2→sm, 3→md, 4→lg, 6→xl; by hand for 5, 7, 8
Grid paddingremoved (the shell owns the gutter; Card or Box owns inset)Move the inset to the surrounding surface
Grid repeat(N, 1fr) tracksrepeat(N, minmax(0, 1fr))Nothing to change (content can no longer widen a track)
Theme / ThemeScript attribute="class"removed; the mode is always data-theme on <html>CSS keyed on .dark keys on [data-theme="dark"]
configureTheme({ radiusStyle })configureTheme({ radius })Pick the radius in px or a CSS length
useTheme().toggleMode light ⇄ darksteps system → light → dark → systemCall setMode for a two-state flip
useTheme() outside a Theme returns inert no-opsreads and writes data-theme on <html>Nothing to change (a hook outside a Theme now works)
Chart with an empty data array drew blank axesshows an EmptyState ("No data to show", empty sets the copy)Nothing to change
Listbox.Item selected + onClick per itemroot value / defaultValue / onValueChange (single) or multiple with string[]; Listbox.Item value (required)Lift the selected id to the root value; give each item a value
Listbox.Empty as role="option" aria-disabledrole="status" line beside the listbox (direct child of Listbox)Nothing to change
Listbox DOM: one role="listbox" element took className/stylean outer plane takes className/style; the inner role="listbox" takes the ARIA and handlersSelectors that targeted the listbox element by class move to [role="listbox"]
ColorPicker errorinvalid + errorMessageMove the error copy from helperText into errorMessage
ColorPicker size="lg"size="md"Lg → md
ColorPicker reverted a bad hex on blurkeeps it and flags it inline ("Enter a hex color like #3366ff")Nothing to change
<DatePicker formatDate={f} formatRange={g}><DatePicker format={f}> (or locale; ranges join with an en dash)Keep one formatter for a single date
DatePicker single pick closes after 150mscloses in the same eventNo change needed
Editor status bar on by defaulton only with maxLength or onAutoSave; pass statusBar to force itAdd statusBar where counts are wanted
Editor named by its placeholderlabel (visible) or aria-labelAdd a label
LoginForm "Sign In" title and button"Sign in"Update text queries in tests
StatsCard changeTone default successdefault neutral; pass changeTone="success" for a good changeAdd changeTone="success" where wanted
<StatsCard icon={<TrendUp />}> (node)<StatsCard icon={TrendUp}> (component, drawn neutral)Pass the component
<StatsCard value> requiredoptional; null shows "—"Nothing to change
ActivityFeed title "Recent Activity""Recent activity"Update text queries in tests
ActivityFeed ref on the inner list divref on the root div around the CardNothing to change
Table unbordered first and last columns flush with the surrounding surface (0 inline inset)The first column starts 12 in on every table, bordered or not; every other cell pads 8Remove padding added around a table to line its text up with a card's inset; a selected row's ring no longer touches the content
Sidebar.Header showing children while the rail is collapsedcollapsedContent alone while collapsed (nothing when unset)Pass the brand mark as collapsedContent
fragmentsLogoSvgRemoved: use your own logoRemove the library import and provide your own logo in the existing slot
fragmentsSymbolRemoved: use your own logoRemove the library import and provide your own logo in the existing slot
fragmentsWordmarkAspectRemoved: use your own logo measurementsRemove the library import and provide your own logo measurements in the existing slot
fragmentsWordmarkSvgRemoved: use your own logoRemove the library import and provide your own logo in the existing slot
fragmentsWordmarkSymbolRemoved: use your own logo measurementsRemove the library import and provide your own logo measurements in the existing slot
./assets/*Removed: use your own logoRemove the library import and provide your own logo in the existing slot

Verify the upgrade

Run your app's type check and build to find removed imports, props, and compound parts. Use the replacement table above for each reported name.

Open representative screens in light and dark modes. Check text size, spacing, forms, and overlays at narrow and wide widths. Follow a main task with the keyboard before treating the migration as complete.

Next steps