npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

handy-fluentui

v1.0.1

Published

Handy React components built on top of FluentUI v9 with responsive, form-friendly behaviours

Readme

Handy FluentUI

Opinionated React components built on top of FluentUI v9 with responsive, form-friendly behaviours out of the box.

  • Consistent label / hint / error / info layout via the withInputField HOC
  • Automatic mobile adaptation (breakpoint-driven theme switching, bottom-sheet drawers, stacking layouts)
  • Imperative useToast, useSpinner, useDialog APIs
  • i18n-ready label overrides through the provider

An equivalent library built on shadcn/ui instead of FluentUI, mirroring the same component API surface under a Hui* prefix, is available at kc2wong/handy-shadcnui.


Commands

yarn dev        # Start dev server (Vite)
yarn build      # Production build
yarn test       # Run tests in watch mode (Vitest)
yarn test:run   # Run tests once
yarn lint       # ESLint

Setup

Wrap your application in HandyFluentUiProvider once at the root. All components and hooks must be descendants of this provider.

import { webDarkTheme, webLightTheme } from '@fluentui/react-components';
import { HandyFluentUiProvider } from './providers/handy-fluent-ui-provider';

function App() {
  return (
    <HandyFluentUiProvider
      mobileBreakpoint={600}
      supportedTheme={{
        web: { light: webLightTheme, dark: webDarkTheme },
        default: 'light',
      }}
      component={{
        toast: { dismissTimeout: 3000 },
      }}
    >
      {/* your app */}
    </HandyFluentUiProvider>
  );
}

Provider props

| Prop | Type | Default | Description | |------|------|---------|-------------| | mobileBreakpoint | number | 600 | Viewport width (px) at which mobile layout and theme activate | | supportedTheme | SupportedTheme | built-in themes | Theme objects for web/mobile platforms | | component | Component | — | Spinner and toast configuration | | loggerConfig | { logMessage? } | console.log | Custom logger | | children | ReactNode | — | Required |

supportedTheme

type SupportedTheme = {
  web?:    { light?: Theme; dark?: Theme; custom?: Theme };
  mobile?: { light?: Theme; dark?: Theme; custom?: Theme };
  default?: 'light' | 'dark' | 'custom'; // defaults to system preference
};

A custom theme object must be supplied to make 'custom' selectable via useTheme().switchTheme('custom'). A single custom theme passed under either web or mobile is automatically shared with the other platform.

component config

type Component = {
  spinner?: SpinnerContextConfig;
  toast?: ToastContextConfig;
};

Component-specific labels (FuiTable pagination text, FuiImageCarousel tooltips, FuiInputMultiLangText language names) are passed directly as props on each component — see each component's section below.


Hooks

| Hook | Returns | Description | |------|---------|-------------| | useTheme() | { currentTheme, switchTheme } | Read and change the active theme | | useIsMobile() | boolean | True when viewport ≤ mobileBreakpoint | | useBreadcrumb() | { items, isCollapsed, toggleCollapsed, start, append, peek, popTill } | Read and update the trail rendered by FuiBreadcrumb | | useToast() | { success, error, info, warning } | Show toast notifications | | useSpinner() | { show, hide } | Show/hide the global overlay spinner | | useDialog() | { openDialog } | Show an imperative confirmation dialog | | useLogger() | (message, level?) => void | Log via the configured logger | | useTimeZone() | { timeZone, setTimeZone, zonedDate2LocalDate } | Read, update, and decompose dates in the active time zone |

All hooks throw if called outside HandyFluentUiProvider.

useTimeZone

The provider initialises timeZone from Intl.DateTimeFormat().resolvedOptions().timeZone (the browser's local time zone). useTimeZone lets you read or override it and decompose a Date object into its constituent parts within that zone.

const { timeZone, setTimeZone, zonedDate2LocalDate } = useTimeZone();

// Read the active time zone
console.log(timeZone); // e.g. 'Asia/Tokyo'

// Switch to a different time zone (validated; invalid values are ignored with a warning)
setTimeZone('America/New_York');

// Extract date parts in the active time zone
const parts = zonedDate2LocalDate(new Date());
// { year, month, day, hour, minute, second }

// Extract date parts in an explicit time zone (overrides the active one for this call)
const tokyoParts = zonedDate2LocalDate(new Date(), 'Asia/Tokyo');

LocalDate

type LocalDate = {
  year: number;
  month: number;   // 1–12
  day: number;     // 1–31
  hour: number;    // 0–23
  minute: number;  // 0–59
  second: number;  // 0–59
};

| Return value | Type | Description | |---|---|---| | timeZone | string | Currently active IANA time zone identifier | | setTimeZone | (tz: string) => void | Update the active time zone. Invalid identifiers are ignored and logged as a warning. | | zonedDate2LocalDate | (date: Date, tz?: string) => LocalDate | Decompose a Date into year/month/day/hour/minute/second in the active (or an explicitly supplied) time zone. Falls back to local time and logs a warning if tz is invalid. |


Common field props (FieldLayoutProps)

Every form input component (FuiInputText, FuiInputDate, FuiRadioGroup, ...) inherits these props from the withInputField HOC:

| Prop | Type | Default | Description | |------|------|---------|-------------| | label | string \| null | — | Field label. null suppresses the label container entirely | | required | boolean | false | Shows a red asterisk next to the label | | hint | string | — | Supplemental info shown in a popover (info icon appears) | | errorMessage | string | — | Error text shown in red below the input | | infoMessage | string | — | Grey helper text below the input (hidden when errorMessage is present) | | noMessage | boolean | false | Suppresses the message area and its reserved space | | additionalMessage | ReactNode | — | Extra content on the right of the message row | | clearable | boolean | true | Shows an eraser icon that clears the value | | layout | 'vertical' \| 'horizontal' | 'vertical' | Label position: above or to the left of the input | | labelWidth | 'quarter' \| 'third' \| 'half' \| 'auto' | — | Fixed label width when layout='horizontal' |

Horizontal layout automatically collapses to vertical on mobile.


Components

FuiInputText

Text input with optional show/hide toggle for passwords.

<FuiInputText
  label="Full Name"
  value={name}
  onChange={setName}
  required
  hint="Enter your legal name."
/>

<FuiInputText
  label="Password"
  value={password}
  onChange={setPassword}
  type="password"
/>

| Prop | Type | Required | Description | |------|------|----------|-------------| | value | string \| null | Yes | Current value | | onChange | (value: string \| null) => void | Yes | Change callback | | type | 'text' \| 'email' \| 'password' | No | Defaults to 'text'. Password adds show/hide toggle; email blocks duplicate @. | | contentBefore | ReactElement | No | Content rendered inside the input, left-aligned (e.g. an icon) | | contentAfter | ReactElement | No | Content rendered inside the input, right-aligned (e.g. an icon button). Ignored when type is 'password'. |


FuiInputTextArea

Multi-line text area with optional character counter.

<FuiInputTextArea
  label="Biography"
  value={bio}
  onChange={setBio}
  maxLength={300}
  hint="Max 300 characters."
/>

| Prop | Type | Required | Description | |------|------|----------|-------------| | value | string \| null | Yes | Current value | | onChange | (value: string \| null) => void | Yes | Change callback | | maxLength | number | No | Automatically appends a counter (n / max) unless additionalMessage is set | | rows | number | No | Number of visible text lines. Defaults to 4. | | readOnly | boolean | No | Native HTML read-only attribute, forwarded as-is to the <textarea> |


FuiInputNumber

Number input with keystroke filtering and optional SpinButton mode.

{/* Plain number input */}
<FuiInputNumber
  label="Age"
  value={age}
  onChange={setAge}
  min={0}
  max={120}
  precision={0}
  allowNegative={false}
/>

{/* SpinButton mode (set step) */}
<FuiInputNumber
  label="Salary"
  value={salary}
  onChange={setSalary}
  step={1000}
  min={0}
  formatter={(v) => `$${v.toLocaleString()}`}
/>

| Prop | Type | Required | Description | |------|------|----------|-------------| | value | number \| null | Yes | Current value | | onChange | (value: number \| null) => void | Yes | Change callback | | step | number | No | Enables SpinButton mode; direct typing is disabled. On mobile, renders a plain Input with horizontally-arranged up/down arrow buttons instead of the native SpinButton. | | precision | number | No | Decimal places allowed. Defaults to 0. Fixed at 0 in SpinButton mode. | | min | number | No | Minimum value | | max | number | No | Maximum value | | allowNegative | boolean | No | When false, blocks the minus key. Defaults to true. | | formatter | (value: number) => string | No | Formats the display value when the field is unfocused | | appearance | 'outline' \| 'underline' \| 'filled-darker' \| 'filled-lighter' \| 'filled-darker-shadow' \| 'filled-lighter-shadow' | No | Visual appearance of the underlying Input/SpinButton | | size | 'small' \| 'medium' \| 'large' | No | Input size |


FuiInputDate

Date picker. Renders a FluentUI DatePicker on desktop and a bottom-sheet calendar drawer on mobile.

<FuiInputDate
  label="Date of Birth"
  value={date}
  onChange={(d) => setDate(d ?? null)}
  required
/>

| Prop | Type | Required | Description | |------|------|----------|-------------| | value | Date \| null | Yes | Selected date | | onChange | (date: Date \| null) => void | Yes | Change callback | | formatter | (date: Date \| null) => string | No | Custom date format function. Defaults to toLocaleDateString(). | | placeholder | string | No | Placeholder text shown when empty | | disabled | boolean | No | Disables interaction | | readOnly | boolean | No | Suppresses the calendar popup/drawer. Desktop renders a plain read-only Input; mobile hides the calendar icon and ignores clicks. |


FuiInputTime

Time picker with up/down arrow buttons. Clicking an arrow increments or decrements the time segment under the cursor. On mobile the arrows are laid out in a horizontal row with larger icons.

import { FuiInputTime, FuiTime } from './components/fui-input-time';

const [shiftStart, setShiftStart] = useState<FuiTime | null>(null);

{/* 12-hour format with seconds */}
<FuiInputTime
  label="Shift Start"
  value={shiftStart}
  onChange={setShiftStart}
  in24HourFormat={false}
  withSeconds
/>

{/* 24-hour format, cascade carry enabled */}
<FuiInputTime
  label="Shift End"
  value={shiftEnd}
  onChange={setShiftEnd}
  cascadeCarry
/>

FuiTime type:

type FuiTime = {
  hour: number;    // 0–23
  minute: number;  // 0–59
  second: number;  // 0–59
};

| Prop | Type | Required | Description | |------|------|----------|-------------| | value | FuiTime \| null | Yes | Current time value | | onChange | (time: FuiTime \| null) => void | Yes | Change callback | | in24HourFormat | boolean | No | When false, display uses 12-hour clock and shows an AM/PM toggle. Defaults to true. | | withSeconds | boolean | No | When true, shows the seconds segment. Defaults to false. | | cascadeCarry | boolean | No | When true, incrementing past a segment boundary (e.g. 59m → 0m) also advances the next segment. Defaults to false. | | readOnly | boolean | No | Hides the up/down arrows; the input becomes non-interactive. |


FuiInputDropdown

Dropdown with single or multi-select. Renders a bottom-sheet drawer on mobile with the field label (or placeholder) as the drawer title.

const options = [
  { value: 'hk', text: 'Hong Kong', group: 'Asia' },
  { value: 'gb', text: 'United Kingdom', group: 'Europe' },
];

{/* Single select */}
<FuiInputDropdown
  label="Country"
  value={country}
  onChange={(val) => setCountry(val as string | null)}
  options={options}
/>

{/* Multi-select */}
<FuiInputDropdown
  label="Tags"
  value={tags}
  onChange={(val) => setTags(val as string[])}
  options={options}
  multiselect
/>

{/* Constrain dropdown height on desktop */}
<FuiInputDropdown
  label="Country"
  value={country}
  onChange={(val) => setCountry(val as string | null)}
  options={options}
  listbox={{ style: { maxHeight: '200px', overflowY: 'auto' } }}
  positioning={{ autoSize: false }}
/>

| Prop | Type | Required | Description | |------|------|----------|-------------| | value | string \| string[] \| null | Yes | Selected value(s) | | onChange | (value: string \| string[] \| null) => void | Yes | Change callback | | options | FuiInputDropdownOption[] | Yes | Option list | | multiselect | boolean | No | Enable multi-select mode. Defaults to false. | | placeholder | string | No | Placeholder text shown when empty | | disabled | boolean | No | Disables interaction | | readOnly | boolean | No | Silently ignores selection changes | | className | string | No | Custom CSS class for the dropdown root | | style | CSSProperties | No | Custom CSS styles for the dropdown root | | listbox | { style?: CSSProperties } | No | Style passthrough for the inner Listbox. Use style.maxHeight to constrain dropdown height. Must pass positioning={{ autoSize: false }} alongside this, otherwise Floating UI overrides inline max-height. | | positioning | { autoSize?: boolean } | No | Positioning passthrough for the dropdown popup |

FuiInputDropdownOption

| Prop | Type | Required | Description | |------|------|----------|-------------| | value | string | Yes | Option value | | text | string | Yes | Display text (used for search/filtering) | | group | string | No | Group label | | render | () => ReactNode | No | Custom option content renderer |


FuiRadioGroup / FuiRadio

Radio group with a shared label. FuiRadio items are independent components (not raw Fluent <Radio> elements).

<FuiRadioGroup
  label="Gender"
  value={gender}
  onChange={setGender}
  layout="horizontal"
>
  <FuiRadio label="Male" value="male" />
  <FuiRadio label="Female" value="female" />
  <FuiRadio label="Other" value="other" />
</FuiRadioGroup>

FuiRadioGroup props

| Prop | Type | Required | Description | |------|------|----------|-------------| | value | string | No | Controlled selected value | | defaultValue | string | No | Uncontrolled initial value | | name | string | No | Shared name for the underlying radio inputs on form submission | | onChange | (value: string) => void | No | Change callback | | disabled | boolean | No | Disables the entire group | | required | boolean | No | Marks the group mandatory | | readOnly | boolean | No | Silently ignores changes | | layout | 'vertical' \| 'horizontal' | No | Radio button layout direction. horizontal-stacked is not supported. |

FuiRadio props

| Prop | Type | Required | Description | |------|------|----------|-------------| | value | string | Yes | Value submitted when this option is selected | | label | string | Yes | Label rendered next to the radio button | | disabled | boolean | No | Disables interaction | | id | string | No | Optional id override; auto-generated when omitted |


FuiCheckbox

Checkbox with optional read-only mode.

<FuiCheckbox
  label="I agree to the terms"
  checked={agreed}
  onChange={setAgreed}
/>

| Prop | Type | Required | Description | |------|------|----------|-------------| | label | string | Yes | Text label rendered next to the checkbox | | checked | boolean | No | Checked state. Defaults to false. | | onChange | (checked: boolean) => void | Yes | Change callback — receives boolean directly | | disabled | boolean | No | Disables interaction | | readOnly | boolean | No | Visually interactive but ignores changes |


FuiSwitch

Toggle switch. onChange delivers a boolean directly.

<FuiSwitch
  label="Receive notifications"
  checked={notifications}
  onChange={setNotifications}
/>

| Prop | Type | Required | Description | |------|------|----------|-------------| | label | string | Yes | Text displayed next to the switch | | checked | boolean | No | Checked state. Defaults to false. | | defaultChecked | boolean | No | Initial value for uncontrolled usage | | onChange | (value: boolean) => void | Yes | Change callback — receives boolean directly | | disabled | boolean | No | Disables interaction | | readOnly | boolean | No | Silently ignores changes |


FuiInputMultiLangText

Text input for multi-language values. A translate icon opens a drawer with one field per configured language (up to 3).

<FuiInputMultiLangText
  label="Job Title"
  value={jobTitle}
  onChange={setJobTitle}
/>

value / onChange use MultiLangText:

type MultiLangText = {
  valueInLangOne: string | null;
  valueInLangTwo: string | null;
  valueInLangThree: string | null;
};

| Prop | Type | Required | Description | |------|------|----------|-------------| | value | MultiLangText \| null | Yes | Multi-language text value | | onChange | (value: MultiLangText \| null) => void | Yes | Change callback | | label | string | Yes | Field label — also used as the drawer title | | langLabel | { languages: string[] } | No | Names of each language slot shown in the drawer (up to 3). When fewer than 2 are provided, the translate icon is hidden. | | textComponent | ComponentType<FuiInputTextProps> | No | Overrides the inner text component. Defaults to FuiInputText. |


FuiInputGroup

Groups multiple inputs under one shared label with weighted distribution. Items stack vertically on mobile.

<FuiInputGroup
  label="City / Zip"
  items={[
    { element: <FuiInputText value={city} onChange={setCity} placeholder="City" />, weight: 2 },
    { element: <FuiInputText value={zip} onChange={setZip} placeholder="Zip" />, weight: 1 },
  ]}
/>

| Prop | Type | Required | Description | |------|------|----------|-------------| | label | string | Yes | Shared label for the group | | items | { element: ReactElement; weight?: number }[] | Yes | Inputs with optional flex-grow weights (default 1) |

Each item's own label is hidden; use the group-level label instead.


FuiTable / FuiColumn

Data table driven by FuiColumn children. Supports sorting and pagination with horizontal scroll.

<FuiTable
  data={records}
  pagination={{
    offset: 0,
    pageSize: 10,
    pageSizeOption: [5, 10, 20],
    totalRecord: records.length,
    position: 'bottom',
  }}
  width={{ minWidth: '560px' }}
>
  <FuiColumn field="id" header="ID" style={{ width: '10%' }} />
  <FuiColumn field="name" header="Name" sortable style={{ width: '40%' }} />
  <FuiColumn
    field="status"
    header="Status"
    builder={(value) => <Badge>{String(value)}</Badge>}
    style={{ width: '20%' }}
  />
</FuiTable>

FuiTable props

| Prop | Type | Required | Description | |------|------|----------|-------------| | data | T[] | Yes | Array of row objects | | pagination | PaginationProps | No | Pagination configuration | | onPageOrSort | (page?, sort?) => void | No | Called on page change or sort click. Omit to use local sort state. | | width | Pick<CSSProperties, 'width' \| 'minWidth' \| 'maxWidth'> | No | Width constraints on the inner DataGrid. Set minWidth to enable horizontal scroll on mobile. | | label | FuiTableLabel | No | Pagination text overrides. pageRange and paginationBar.nextN / previousN support template tokens ({{from}}, {{to}}, {{total}}, {{n}}). |

FuiColumn props

| Prop | Type | Required | Description | |------|------|----------|-------------| | field | string | Yes | Dot-notation path into the row object (e.g. "address.city") | | header | string | Yes | Column header text | | sortable | boolean | No | Makes the column header clickable for sorting | | align | 'left' \| 'center' \| 'right' | No | Cell text alignment | | formatter | (value, row) => string | No | Format function for plain text cells | | builder | (value, row) => ReactNode | No | Render function for rich content (mutually exclusive with formatter) | | style | CSSProperties | No | Cell styles (use to set column width) | | headerStyle | CSSProperties | No | Header-cell-specific styles | | headerEllipsis | boolean | No | Truncate long header text with ellipsis |

PaginationProps

| Prop | Type | Required | Description | |------|------|----------|-------------| | offset | number | Yes | Zero-based row offset of the current page | | pageSize | number | Yes | Rows per page | | totalRecord | number | Yes | Total number of records | | pageSizeOption | number[] | Yes | Available page size choices | | position | 'top' \| 'bottom' | No | Defaults to 'bottom' | | fastForwardPage | number | No | Pages to jump on << / >>. Defaults to 5. |


FuiTabList / FuiTab

Tabbed panel. On mobile the tab bar becomes horizontally scrollable. Vertical layout is forced horizontal on mobile.

<FuiTabList<string>
  selectedValue={tab}
  onTabSelect={(data) => setTab(data.value)}
>
  <FuiTab name="Personal" value="personal">
    <PersonalForm />
  </FuiTab>
  <FuiTab name="Employment" value="employment">
    <EmploymentForm />
  </FuiTab>
</FuiTabList>

| Prop (FuiTabList) | Type | Required | Description | |---------------------|------|----------|-------------| | selectedValue | T | No | Currently active tab value | | onTabSelect | (data: { value: T }) => void | No | Selection change callback | | vertical | boolean | No | Side-by-side layout (collapsed on mobile) |

| Prop (FuiTab) | Type | Required | Description | |-----------------|------|----------|-------------| | name | string | Yes | Tab button label | | value | T | No | Tab identifier — defaults to name | | children | ReactNode | No | Content panel | | icon | ReactNode | No | Icon shown in the tab button |


FuiImageCarousel

Circular image carousel with autoplay and navigation controls.

<FuiImageCarousel
  images={[
    'https://example.com/photo1.jpg',
    'https://example.com/photo2.jpg',
  ]}
/>

| Prop | Type | Required | Description | |------|------|----------|-------------| | images | string[] | Yes | Image URLs | | label | { autoplay?: string; next?: string; previous?: string } | No | Tooltip label overrides for the navigation buttons |


FuiButtonPanel

Flex row of action buttons. Collapses to full-width stacked column on mobile.

<FuiButtonPanel alignItems="right">
  <Button appearance="secondary" onClick={onCancel}>Cancel</Button>
  <Button appearance="primary" onClick={onSave}>Save</Button>
</FuiButtonPanel>

| Prop | Type | Required | Description | |------|------|----------|-------------| | alignItems | 'left' \| 'right' | No | Horizontal alignment. Defaults to 'right'. | | children | ReactNode | Yes | Button elements |


FuiButton / FuiIconButton

Button with appearance/size/icon. FuiIconButton is a thin wrapper that always renders icon-only (no children) and defaults to a subtle appearance, for toolbar/close-button usage.

<FuiButton appearance="primary" onClick={onSave}>Save</FuiButton>

<FuiIconButton aria-label="Close" icon={<DismissRegular />} onClick={onClose} />

FuiButton props

| Prop | Type | Required | Description | |------|------|----------|-------------| | appearance | 'primary' \| 'outline' \| 'subtle' \| 'transparent' \| 'secondary' | No | Defaults to 'secondary' | | size | 'small' \| 'medium' \| 'large' | No | Defaults to 'medium' | | icon | ReactElement | No | Icon rendered alongside the label | | iconPosition | 'before' \| 'after' | No | Side of the label the icon is rendered on. Defaults to 'before'. | | children | ReactNode | No | Button label |

FuiIconButton takes the same props minus children/iconPosition, with icon and aria-label required.


FuiToggle

Pressable toggle button (ToggleButton).

<FuiToggle checked={bold} icon={<TextBoldRegular />} onClick={() => setBold((v) => !v)} />

| Prop | Type | Required | Description | |------|------|----------|-------------| | checked | boolean | No | Controlled pressed state | | defaultChecked | boolean | No | Initial pressed state for uncontrolled usage. Defaults to false. | | appearance | 'primary' \| 'outline' \| 'subtle' \| 'transparent' \| 'secondary' | No | Defaults to 'secondary' | | size | 'small' \| 'medium' \| 'large' | No | Defaults to 'medium' | | icon | ReactElement | No | Icon rendered alongside the label | | iconPosition | 'before' \| 'after' | No | Defaults to 'before' |


FuiDivider

Thin horizontal rule used to separate page sections, with an optional centered label.

<FuiDivider>Section Title</FuiDivider>

| Prop | Type | Required | Description | |------|------|----------|-------------| | children | ReactNode | No | Optional label rendered centered on the divider line |


FuiText and typography variants

Typography element covering FluentUI's Label/Title1/Title2/Subtitle1/Subtitle2/Body1/Body2/Caption1/Caption2. FuiLabel, FuiTitle1, FuiTitle2, FuiSubTitle1, FuiSubTitle2, FuiBody1, FuiBody2, FuiCaption1, FuiCaption2 are FuiText fixed to the matching type.

<FuiTitle1>Page heading</FuiTitle1>
<FuiText type="body2" text="Body copy" />

| Prop | Type | Required | Description | |------|------|----------|-------------| | text | string | Yes | Text content to display | | type | 'label' \| 'title1' \| 'title2' \| 'subTitle1' \| 'subTitle2' \| 'body1' \| 'body2' \| 'caption1' \| 'caption2' | No | Defaults to 'label'. Ignored on the fixed-type variants. | | italic | boolean | No | Defaults to false | | bold | boolean | No | Overrides the type's default weight. Defaults to false. | | block | boolean | No | Renders as a block-level element instead of inline. Defaults to false. |


FuiTooltip

Wraps arbitrary content with a small message shown on hover (a Tooltip) or on click (a Popover, since Fluent's Tooltip has no click-triggered mode). If the wrapped element is itself disabled (disabled or aria-disabled), no affordance is added and the message never shows.

<FuiTooltip text="Delete this record">
  <FuiIconButton aria-label="Delete" icon={<DeleteRegular />} onClick={onDelete} />
</FuiTooltip>

<FuiTooltip showOn="click" text="Copied!" position="top">
  <FuiButton onClick={copyToClipboard}>Copy</FuiButton>
</FuiTooltip>

| Prop | Type | Required | Description | |------|------|----------|-------------| | text | string | Yes | Text shown in the tooltip/popover | | children | ReactElement | Yes | A single element to wrap | | showOn | 'hover' \| 'click' | No | Defaults to 'hover' | | dismissMs | number | No | When showOn is 'click', ms of inactivity before auto-dismiss. Defaults to 2000. | | position | 'top' \| 'bottom' \| 'left' \| 'right' | No | Defaults to the underlying component's own placement |


FuiAccordion / FuiAccordionItem

Groups collapsible FuiAccordionItem panels, controlling which are expanded — single- or multi-expand.

<FuiAccordion value={openItem} onChange={setOpenItem} collapsible>
  <FuiAccordionItem header="Details" value="details">
    <DetailsForm />
  </FuiAccordionItem>
  <FuiAccordionItem header="Preferences" value="preferences">
    <PreferencesForm />
  </FuiAccordionItem>
</FuiAccordion>

FuiAccordion props

| Prop | Type | Required | Description | |------|------|----------|-------------| | value / onChange | string / (value: string) => void | Yes | Controlled expanded value (single mode, default) | | value / onChange (when multiple) | string[] / (value: string[]) => void | Yes | Controlled expanded values (multi mode) | | multiple | boolean | No | Allow more than one panel open at once. Defaults to false. | | collapsible | boolean | No | When the open panel can be collapsed to leave none open. Not applicable when multiple. Defaults to false. | | expandIcon | ReactElement | No | Overrides the default expand/collapse icon for every item | | expandIconPosition | 'start' \| 'end' | No | Defaults to 'start' | | withDivider | boolean | No | Renders a divider between panels. Defaults to true. |

FuiAccordionItem props

| Prop | Type | Required | Description | |------|------|----------|-------------| | value | string | Yes | Unique identifier for this panel | | header | ReactNode | Yes | Clickable header content | | disabled | boolean | No | Disables toggling this item | | expandIcon | ReactElement | No | Per-item icon override, falls back to the parent's expandIcon |


FuiCard / FuiCardHeader / FuiCardPreview / FuiCardFooter

Content-display container for a single topic's header, preview and footer.

<FuiCard>
  <FuiCardHeader header="Quarterly Report" description="Q2 2026" />
  <FuiCardPreview>
    <img alt="" src="/report-preview.png" />
  </FuiCardPreview>
  <FuiCardFooter>
    <FuiButton onClick={onView}>View</FuiButton>
  </FuiCardFooter>
</FuiCard>

| Prop (FuiCard) | Type | Required | Description | |-------------------|------|----------|-------------| | appearance | 'filled' \| 'filled-alternative' \| 'outline' \| 'subtle' | No | Defaults to 'filled' | | orientation | 'horizontal' \| 'vertical' | No | Defaults to 'vertical' | | size | 'small' \| 'medium' \| 'large' | No | Controls border radius and inner spacing. Defaults to 'medium'. |

| Prop (FuiCardHeader) | Type | Description | |-------------------------|------|-------------| | image | ReactElement \| string \| number | Image or avatar related to the card | | header | ReactElement \| string \| number | Main header title | | description | ReactElement \| string \| number | Short description related to the title | | action | ReactElement \| string \| number | Content at the far end, e.g. an overflow menu button |

| Prop (FuiCardPreview) | Type | Description | |---------------------------|------|-------------| | logo | ReactElement \| string \| number | Small badge overlaid on the preview content | | children | ReactNode | The preview image or content itself |

| Prop (FuiCardFooter) | Type | Description | |--------------------------|------|-------------| | action | ReactElement \| string \| number | Content at the far end, e.g. a single icon button | | children | ReactNode | Main footer content, e.g. action buttons |


FuiDrawer / FuiDrawerHeader / FuiDrawerBody

Panel that hosts supplementary content or a management experience, dismissible ('overlay') or stacked with the page ('inline'). Forced to a bottom position on mobile regardless of position.

<FuiDrawer open={open} onOpenChange={setOpen} position="end">
  <FuiDrawerHeader
    title="Filters"
    action={<FuiIconButton aria-label="Close" icon={<DismissRegular />} onClick={() => setOpen(false)} />}
  />
  <FuiDrawerBody>
    <FiltersForm />
  </FuiDrawerBody>
</FuiDrawer>

| Prop | Type | Required | Description | |------|------|----------|-------------| | open | boolean | Yes | Controlled open state | | onOpenChange | (open: boolean) => void | Yes | Fires on Escape, backdrop click, etc. | | position | 'start' \| 'end' \| 'bottom' | No | Defaults to 'start'. Ignored (always 'bottom') on mobile. | | size | 'small' \| 'medium' \| 'large' \| 'full' | No | Defaults to 'small' | | type | 'overlay' \| 'inline' | No | Defaults to 'overlay' | | modalType | 'modal' \| 'non-modal' \| 'alert' | No | Overlay-only. Defaults to 'modal'. | | separator | boolean | No | Inline-only. Defaults to false. |

FuiDrawerHeader takes title/action; FuiDrawerBody renders scrollable main content.


FuiMenuBar

A horizontal bar of dropdown menus, e.g. a desktop-app-style File/Edit/View menu.

<FuiMenuBar>
  <FuiMenuBarMenu label="File">
    <FuiMenuBarItem icon={<DocumentAddRegular />} shortcut="Ctrl+N" onClick={onNew}>
      New
    </FuiMenuBarItem>
    <FuiMenuBarSeparator />
    <FuiMenuBarCheckboxItem checked={autosave} onCheckedChange={setAutosave}>
      Autosave
    </FuiMenuBarCheckboxItem>
    <FuiMenuBarSub label="Export as">
      <FuiMenuBarRadioGroup value={format} onValueChange={setFormat}>
        <FuiMenuBarRadioItem value="pdf">PDF</FuiMenuBarRadioItem>
        <FuiMenuBarRadioItem value="csv">CSV</FuiMenuBarRadioItem>
      </FuiMenuBarRadioGroup>
    </FuiMenuBarSub>
  </FuiMenuBarMenu>
</FuiMenuBar>

| Component | Key props | |---|---| | FuiMenuBar | children: one or more FuiMenuBarMenu | | FuiMenuBarMenu | label, disabled | | FuiMenuBarItem | icon, shortcut, disabled, onClick | | FuiMenuBarCheckboxItem | checked, onCheckedChange, disabled, shortcut | | FuiMenuBarRadioGroup | value, onValueChange — wraps FuiMenuBarRadioItem children | | FuiMenuBarRadioItem | value, disabled | | FuiMenuBarSeparator | className | | FuiMenuBarLabel | Non-interactive group heading | | FuiMenuBarSub | label, disabled — nested dropdown, triggered from within a parent menu |


FuiBreadcrumb

Renders the current trail from useBreadcrumb(). The last item is shown as the current page; earlier items without an action are non-interactive labels. Once there are at least 3 items, clicking the last item toggles a collapsed first > … > last view.

const breadcrumb = useBreadcrumb();

breadcrumb.start({ label: () => 'Home', action: () => navigate('/') });
breadcrumb.append({ label: () => 'Settings', action: () => navigate('/settings') });

<FuiBreadcrumb />

FuiBreadcrumb takes no props — it's entirely driven by useBreadcrumb().


Toast notifications

Use the useToast() hook to show non-blocking feedback.

const toast = useToast();

toast.success('Saved!');
toast.error('Something went wrong.');
toast.info('Processing…');
toast.warning('Check your input.');

Error toasts do not auto-dismiss. All other intents dismiss automatically after toast.dismissTimeout ms (configurable in the provider component.toast config). A dismiss button appears on error toasts after the timeout.


Spinner

Use useSpinner() to show a full-screen overlay spinner during async operations.

const spinner = useSpinner();

spinner.show();
await saveData();
spinner.hide();

Confirmation dialog

Use useDialog() for imperative confirmation dialogs.

const dialog = useDialog();

dialog.openDialog({
  title: 'Confirm Delete',
  content: 'Are you sure?',
  primaryButton: { label: 'Yes', action: handleDelete },
});