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-shadcnui

v1.0.2

Published

This repo is a port of [handy-fluentui](https://github.com/kc2wong/handy-fluentui), swapping FluentUI v9 for shadcn/ui as the underlying primitives while keeping the same component API surface (under a `Hui*` prefix instead of `Fui*`).

Downloads

509

Readme

Handy shadcn/ui

This repo is a port of handy-fluentui, swapping FluentUI v9 for shadcn/ui as the underlying primitives while keeping the same component API surface (under a Hui* prefix instead of Fui*).

  • Consistent label / hint / error / info layout via the withInputField HOC
  • Automatic mobile adaptation (breakpoint-driven layouts, bottom-sheet drawers, stacking layouts)
  • Imperative useToast, useSpinner, useDialog APIs
  • Light/dark/custom theming with a global toggle shortcut and cross-tab sync

A live demo of most of the components is available at kc2wong.github.io/handy-shadcnui.


Commands

yarn dev           # Start Vite dev server
yarn build         # Type-check (tsc -b) then production build
yarn build:lib     # Build a publishable library bundle (dist/)
yarn typecheck     # Type-check only, no emit
yarn lint          # ESLint over the whole repo
yarn format        # Prettier --write on **/*.{ts,tsx}
yarn shadcn:sync   # Regenerate src/components/shadcn/ui/** via the shadcn CLI

src/components/shadcn/ui/** is gitignored and CLI-managed — never hand-edit it; run shadcn:sync to (re)generate it, then restart the dev server.


Setup

1. Install

yarn add handy-shadcnui

2. Peer dependencies

react, react-dom, and @base-ui/react are declared as peerDependencies — install them explicitly alongside handy-shadcnui:

yarn add react react-dom @base-ui/react

They're peers rather than regular dependencies specifically so your app supplies a single shared copy of each — letting your package manager install its own nested copy risks two React (or two @base-ui/react) instances coexisting, which breaks context-based state (hooks throwing "context is missing" errors) since a provider from one copy isn't recognized by consumers using the other.

Everything else handy-shadcnui needs (lucide-react, date-fns, embla-carousel-react, react-day-picker, usehooks-ts, clsx, tailwind-merge, class-variance-authority) is a regular dependency and installs automatically with it — no extra step. You'll also need tailwindcss itself for the next step, if your app doesn't already have it.

3. Tailwind CSS v4

Every Hui* component is styled with Tailwind utility classes, not shipped CSS — the package contains no .css file. Your app needs its own Tailwind v4 setup that:

  • Defines the design tokens Hui components' classes resolve against (--background, --foreground, --primary, the @theme inline mapping, etc.). Copy the @theme inline block and the :root / .dark variable definitions from this repo's src/index.css as a starting point — Hui components assume the same shadcn/ui token set (base-nova style, neutral base color).

  • Scans handy-shadcnui for utility classes. Tailwind always excludes node_modules from its automatic class detection. Since handy-shadcnui ships compiled JS rather than source .tsx, any utility class referenced only inside a Hui component's compiled output — and never literally written in your own app's source — is silently skipped unless you add:

    @source "../node_modules/handy-shadcnui/dist/index.js";

    (path relative to wherever your CSS entrypoint lives). Skipping this is the single most common cause of a Hui prop that "does nothing": the class name lands in the DOM correctly, but no matching CSS rule exists to render it. labelWidth on HuiRadioGroup (w-1/4, w-1/3, w-1/2) is a concrete example that depends on this.

4. Provider

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

import { HandyShadcnUiProvider } from 'handy-shadcnui';

function App() {
  return (
    <HandyShadcnUiProvider
      mobileBreakpoint={600}
      theme={{ defaultTheme: 'light' }}
      component={{
        toast: { dismissTimeout: 3000 },
      }}
    >
      {/* your app */}
    </HandyShadcnUiProvider>
  );
}

Provider props

| Prop | Type | Default | Description | |------|------|---------|-------------| | mobileBreakpoint | number | 600 | Viewport width (px) at which mobile layout activates | | component | Component | — | Spinner and toast configuration | | theme | ThemeConfig | — | Theme configuration — see below | | loggerConfig | { logMessage? } | console.log | Custom logger | | children | ReactNode | — | Required |

theme

type ThemeConfig = {
  /** Initial theme when nothing is stored yet. Defaults to the OS color-scheme preference. */
  defaultTheme?: 'light' | 'dark' | 'custom';
  /** localStorage key the selected theme is persisted under. Defaults to 'theme'. */
  storageKey?: string;
  /** Briefly disables CSS transitions while the theme class swaps, to avoid a flash. Defaults to true. */
  disableTransitionOnChange?: boolean;
  /** Disables the global 'd' keydown shortcut that toggles light/dark. Defaults to false. */
  disableThemeToggleShortcut?: boolean;
};

The active theme is applied by toggling a light / dark / custom class on <html>. A custom theme needs its own token overrides defined in your app's CSS (the same way .dark overrides are defined) — none are provided by default. There is no persistent "system" value: defaultTheme (when unset and nothing is in localStorage) resolves the OS color-scheme preference once at mount, rather than staying live-synced to it. Pressing d anywhere outside an editable element toggles light/dark (disable via disableThemeToggleShortcut); the active theme also syncs across browser tabs.

component config

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

Component-specific labels (HuiTable pagination text, HuiImageCarousel tooltips, HuiInputMultiLangText 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 HuiBreadcrumb | | 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 HandyShadcnUiProvider.

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 (HuiInputText, HuiInputDate, HuiRadioGroup, ...) 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 click-triggered tooltip (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.

Most input components also accept an appearance?: 'outline' | 'underline' prop controlling the input box's visual style (a full box outline vs. just a bottom border), and a contentBefore prop for rendering an icon or other content left-aligned inside the input.


Components

HuiInputText

Text input with optional show/hide toggle for passwords.

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

<HuiInputText
  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 | ReactNode | No | Content rendered inside the input, left-aligned (e.g. an icon) | | contentAfter | ReactNode | No | Content rendered inside the input, right-aligned (e.g. an icon button). Ignored when type is 'password'. | | appearance | 'outline' \| 'underline' | No | Defaults to 'outline' |


HuiInputTextArea

Multi-line text area with optional character counter.

<HuiInputTextArea
  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. | | appearance | 'outline' \| 'underline' | No | Defaults to 'outline' |


HuiInputNumber

Number input with keystroke filtering and an optional spin-button mode.

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

{/* Spin-button mode (set step) */}
<HuiInputNumber
  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 spin-button mode (up/down arrows inside the input); direct typing is disabled. precision is fixed at 0 in this mode. | | precision | number | No | Decimal places allowed. Defaults to 0. | | 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 | | contentBefore | ReactNode | No | Content rendered inside the input, left-aligned (e.g. an icon) | | appearance | 'outline' \| 'underline' | No | Defaults to 'outline' |


HuiInputDate

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

<HuiInputDate
  label="Date of Birth"
  value={date}
  onChange={setDate}
  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 | | contentBefore | ReactNode | No | Content rendered inside the input, left-aligned (e.g. an icon) | | appearance | 'outline' \| 'underline' | No | Defaults to 'outline' | | disabled | boolean | No | Disables interaction | | readOnly | boolean | No | Suppresses the calendar popup/drawer; the field renders as plain read-only text |


HuiInputTime

Time picker with up/down arrows that adjust the active hh:mm[:ss] segment. Direct typing is disabled.

import { HuiInputTime, HuiTime } from 'handy-shadcnui';

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

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

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

HuiTime type:

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

| Prop | Type | Required | Description | |------|------|----------|-------------| | value | HuiTime \| null | Yes | Current time value | | onChange | (time: HuiTime \| null) => void | Yes | Change callback | | in24HourFormat | boolean | No | When false, 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. | | contentBefore | ReactNode | No | Content rendered inside the input, left-aligned. Shares space with the AM/PM toggle. | | appearance | 'outline' \| 'underline' | No | Defaults to 'outline' | | readOnly | boolean | No | Hides the up/down arrows and AM/PM toggle; the input becomes non-interactive |


HuiInputDropdown

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 */}
<HuiInputDropdown
  label="Country"
  value={country}
  onChange={(val) => setCountry(val as string | null)}
  options={options}
/>

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

| Prop | Type | Required | Description | |------|------|----------|-------------| | value | string \| string[] \| null | Yes | Selected value(s) | | onChange | (value: string \| string[] \| null) => void | Yes | Change callback | | options | HuiInputDropdownOption[] | 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 | The popup/drawer no longer accepts a new selection | | appearance | 'outline' \| 'underline' | No | Defaults to 'outline' |

HuiInputDropdownOption

| Prop | Type | Required | Description | |------|------|----------|-------------| | value | string | Yes | Option value | | text | string | Yes | Display text | | group | string | No | Group label | | disabled | boolean | No | Disables this option | | render | () => ReactNode | No | Custom option content renderer |


HuiRadioGroup / HuiRadio

Radio group with a shared label. HuiRadio items are independent components.

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

HuiRadioGroup 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. |

HuiRadio 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 |


HuiCheckbox

Checkbox with optional read-only mode.

<HuiCheckbox
  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 |


HuiSwitch

Toggle switch. onChange delivers a boolean directly.

<HuiSwitch
  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 |


HuiInputMultiLangText

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

<HuiInputMultiLangText
  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<HuiInputTextProps> | No | Overrides the inner text component. Defaults to HuiInputText. |


HuiInputGroup

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

<HuiInputGroup
  label="City / Zip"
  items={[
    { element: <HuiInputText value={city} onChange={setCity} placeholder="City" />, weight: 2 },
    { element: <HuiInputText 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.


HuiTable / HuiColumn

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

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

HuiTable 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 (a warning is logged when sorting without this handler). | | width | Pick<CSSProperties, 'width' \| 'minWidth' \| 'maxWidth'> | No | Minimum width of the scrollable table area; the table always fills the parent, and a horizontal scrollbar appears below this width | | langLabel | HuiTableLabel | No | Pagination text overrides. pageRange and paginationBar.nextN/previousN support template tokens ({{from}}, {{to}}, {{total}}, {{n}}). |

HuiColumn 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. |


HuiTabList / HuiTab

Tabbed panel with an inline content panel and an underline active-tab marker. On mobile the tab bar becomes horizontally scrollable and vertical layout is forced horizontal.

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

| Prop (HuiTabList) | 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 (forced horizontal on mobile) |

| Prop (HuiTab) | 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 | | disabled | boolean | No | Disables this tab |


HuiImageCarousel

Circular image carousel with autoplay and navigation controls.

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

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


HuiButtonPanel

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

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

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


HuiButton / HuiIconButton

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

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

<HuiIconButton aria-label="Close" icon={<X className="h-4 w-4" />} onClick={onClose} />

HuiButton props

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

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


HuiToggle

Pressable toggle button.

<HuiToggle checked={bold} icon={<Bold className="h-4 w-4" />} 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 | ReactNode | No | Icon rendered alongside the label | | iconPosition | 'before' \| 'after' | No | Defaults to 'before' |


HuiDivider

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

<HuiDivider>Section Title</HuiDivider>

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


HuiText and typography variants

Typography element covering label/title/subtitle/body/caption styles. HuiLabel, HuiTitle1, HuiTitle2, HuiSubTitle1, HuiSubTitle2, HuiBody1, HuiBody2, HuiCaption1, HuiCaption2 are HuiText fixed to the matching type.

<HuiTitle1>Page heading</HuiTitle1>
<HuiText 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. |


HuiTooltip

Wraps arbitrary content with a small message shown on hover (a Tooltip) or on click (a Popover, since the underlying tooltip primitive 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.

<HuiTooltip text="Delete this record">
  <HuiIconButton aria-label="Delete" icon={<Trash className="h-4 w-4" />} onClick={onDelete} />
</HuiTooltip>

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

| 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 primitive's own placement |


HuiAccordion / HuiAccordionItem

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

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

HuiAccordion 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 | ReactNode | 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. |

HuiAccordionItem 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 | ReactNode | No | Per-item icon override, falls back to the parent's expandIcon |


HuiCard / HuiCardHeader / HuiCardPreview / HuiCardFooter

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

<HuiCard>
  <HuiCardHeader header="Quarterly Report" description="Q2 2026" />
  <HuiCardPreview>
    <img alt="" src="/report-preview.png" />
  </HuiCardPreview>
  <HuiCardFooter>
    <HuiButton onClick={onView}>View</HuiButton>
  </HuiCardFooter>
</HuiCard>

| Prop (HuiCard) | 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 (HuiCardHeader) | Type | Description | |-------------------------|------|-------------| | image | ReactNode | Image or avatar related to the card | | header | ReactNode | Main header title | | description | ReactNode | Short description related to the title | | action | ReactNode | Content at the far end, e.g. an overflow menu button |

| Prop (HuiCardPreview) | Type | Description | |---------------------------|------|-------------| | logo | ReactNode | Small badge overlaid on the preview content | | children | ReactNode | The preview image or content itself |

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


HuiDrawer / HuiDrawerHeader / HuiDrawerBody

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.

<HuiDrawer open={open} onOpenChange={setOpen} position="end">
  <HuiDrawerHeader
    title="Filters"
    action={<HuiIconButton aria-label="Close" icon={<X className="h-4 w-4" />} onClick={() => setOpen(false)} />}
  />
  <HuiDrawerBody>
    <FiltersForm />
  </HuiDrawerBody>
</HuiDrawer>

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

HuiDrawerHeader takes title/action; HuiDrawerBody renders scrollable main content.


HuiMenuBar

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

<HuiMenuBar>
  <HuiMenuBarMenu label="File">
    <HuiMenuBarItem icon={<FilePlus className="h-4 w-4" />} shortcut="Ctrl+N" onClick={onNew}>
      New
    </HuiMenuBarItem>
    <HuiMenuBarSeparator />
    <HuiMenuBarCheckboxItem checked={autosave} onCheckedChange={setAutosave}>
      Autosave
    </HuiMenuBarCheckboxItem>
    <HuiMenuBarSub label="Export as">
      <HuiMenuBarRadioGroup value={format} onValueChange={setFormat}>
        <HuiMenuBarRadioItem value="pdf">PDF</HuiMenuBarRadioItem>
        <HuiMenuBarRadioItem value="csv">CSV</HuiMenuBarRadioItem>
      </HuiMenuBarRadioGroup>
    </HuiMenuBarSub>
  </HuiMenuBarMenu>
</HuiMenuBar>

| Component | Key props | |---|---| | HuiMenuBar | children: one or more HuiMenuBarMenu | | HuiMenuBarMenu | label, disabled | | HuiMenuBarItem | icon, shortcut, disabled, onClick | | HuiMenuBarCheckboxItem | checked, onCheckedChange, disabled, shortcut | | HuiMenuBarRadioGroup | value, onValueChange — wraps HuiMenuBarRadioItem children | | HuiMenuBarRadioItem | value, disabled | | HuiMenuBarSeparator | className | | HuiMenuBarLabel | Non-interactive group heading | | HuiMenuBarSub | label, disabled — nested dropdown, triggered from within a parent menu |


HuiBreadcrumb

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') });

<HuiBreadcrumb />

HuiBreadcrumb 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 },
});