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

@foi/design-system

v0.0.26

Published

A component library built with React 19 and TypeScript. Components are designed as first-class citizens of [React Hook Form](https://react-hook-form.com/) — pass `name` + `control` and validation is handled for you. Every component also supports a control

Readme

Design System

A component library built with React 19 and TypeScript. Components are designed as first-class citizens of React Hook Form — pass name + control and validation is handled for you. Every component also supports a controlled mode (no RHF) and a theme override mechanism via the ThemeProvider HOC.

Tech stack

| Tool | Version | Purpose | | --------------- | ------- | ----------------------- | | React | 19 | UI runtime | | TypeScript | 6 | Type safety | | Vite | 8 | Dev server & bundler | | Emotion | 11 | CSS-in-JS styling | | React Hook Form | 7 | Form state & validation | | Zod | 4 | Schema validation | | Storybook | 10 | Component explorer | | Vitest | 4 | Unit tests |

Commands

npm run dev              # Start dev server (Vite, HMR)
npm run build            # Type-check + production build
npm run preview          # Preview the production build locally

npm run storybook        # Open Storybook component explorer (port 6006)
npm run build-storybook  # Build Storybook as a static site

npm run test             # Run unit tests with Vitest UI
npm run pretty           # Format all files with Prettier
npm run eslint           # Lint and auto-fix with ESLint

Publishing to npm

npm run build:lib        # Build the library (JS + type declarations)
npm version patch        # Bump version: 0.0.x → 0.0.x+1
npm publish --access public

Components

Atoms

| Component | Description | | ------------- | ------------------------------------------------------------------------------------------ | | Badge | Notification indicator that wraps any element — shows a count, overflow cap, or dot | | Button | Primary action button with variant/size/color support | | Checkbox | Single checkbox — standalone, RHF, or inside a group | | Chip | Compact inline label for statuses and tags — themed variants, accepts text or ReactNode | | ColorField | Color picker field with HEX/RGB/HSL display formats, swatch preview, and RHF integration | | DatePicker | Date input with calendar menu, integrates with RHF | | Icon | Font-based icon using Material Symbols Rounded — pass a name prop | | IconButton | Clickable icon with button semantics | | NumberField | Numeric input with increment/decrement buttons, min/max/step support, and two layout modes | | Pagination | Page navigation bar with first/prev/next/last buttons and optional rows-per-page selector | | Radio | Single radio button — used inside RadioGroup | | Select | Dropdown select with RHF integration | | Skeleton | Animated placeholder shown while content is loading — rectangular or circular | | Slider | Range slider with RHF integration | | Switch | Toggle switch — boolean RHF field | | TextField | Text input (text, password, email) with label, helper text, and error state |

Molecules

| Component | Description | | --------------- | --------------------------------------------------------------------------------- | | CheckboxGroup | Manages a set of Checkbox children as a string[] RHF field | | CheckboxTree | Hierarchical checkbox group with parent/child selection logic | | Modal | Overlay dialog rendered via React portal — closes on ×, backdrop click, or Escape | | RadioGroup | Manages a set of Radio children as a single RHF field | | Toast | Notification system — push transient messages from anywhere via useToast |

Organisms

| Component | Description | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DataGrid | Async data table with server-side sorting, filtering (search / multiselect), pagination or infinite scroll, skeleton loading state, and custom action columns via render |

Usage with React Hook Form

All field components share the same integration pattern. Define a Zod schema, pass control down, and let the component handle registration, value sync, and error display.

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { TextField, NumberField, Button } from '@foi/design-system';

const schema = z.object({
  email: z.string().email('Invalid email'),
  age: z.number({ message: 'Required' }).min(18, 'Must be 18 or older'),
});

type FormValues = z.infer<typeof schema>;

const MyForm = () => {
  const { control, handleSubmit } = useForm<FormValues>({
    resolver: zodResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <TextField name='email' control={control} label='Email' />
      <NumberField name='age' control={control} label='Age' min={18} />
      <Button type='submit' label='Submit' />
    </form>
  );
};

Multi-select with CheckboxGroup

The form value is string[] — the value props of every checked child.

const schema = z.object({
  skills: z.array(z.string()).min(1, 'Select at least one'),
});

<CheckboxGroup name='skills' control={control} label='Skills'>
  <Checkbox value='react' label='React' />
  <Checkbox value='vue' label='Vue' />
  <Checkbox value='svelte' label='Svelte' />
</CheckboxGroup>;

Controlled mode (no RHF)

Pass checked + onChecked instead of name/control.

const [agreed, setAgreed] = useState(false);

<Checkbox checked={agreed} onChecked={setAgreed} label='I agree to the terms' />;

TextField

A text input field with a floating label, helper text, and full error state integration. Only accepts text, password, and email as input types. For numeric input use NumberField.

Props

| Prop | Type | Default | Description | | ---------------- | ------------------------------------------ | ----------- | --------------------------------------------------------- | | name | string | — | Field name, used by RHF | | label | string | — | Floating label text | | control | Control | — | RHF control object; omit for uncontrolled mode | | value | string | — | Value in uncontrolled mode | | onValueChange | (value: string) => void | — | Change handler in uncontrolled mode | | type | 'text' \| 'password' \| 'email' | 'text' | HTML input type | | width | 'small' \| 'medium' \| 'large' \| 'full' | — | Preset width | | helperText | string | — | Hint text shown below the field when there is no error | | showErrorText | boolean | true | Show the validation error message below the field | | hasPadding | boolean | false | Add bottom padding to reserve space for helper/error text | | startAdornment | JSX.Element | — | Element rendered to the left of the input | | endAdornment | JSX.Element | — | Element rendered to the right of the input | | regex | RegExp | — | Block keystrokes that don't match the pattern | | format | (value: string) => string | — | Transform the value before it is stored | | disabled | boolean | false | Disable the field | | theme | EVENTS | — | One-off token override for this instance | | variant | string | 'default' | Theme variant name |

Example

import { TextField, Icon } from '@foi/design-system';

<TextField
  name='email'
  control={control}
  label='Email'
  type='email'
  startAdornment={<Icon name='mail' />}
  helperText='We will never share your email'
/>;

NumberField

A numeric input that stores a number in RHF (or undefined when empty). The field always renders increment/decrement buttons; their position depends on the display prop.

Props

All TextField props apply except type, endAdornment, and startAdornment (in spinner mode). The following props are specific to NumberField:

| Prop | Type | Default | Description | | --------- | ------------------------- | ------------ | -------------------------------------------------------------------------------------- | | min | number | — | Minimum allowed value; decrement button disables at this bound | | max | number | — | Maximum allowed value; increment button disables at this bound | | step | number | 1 | Amount added or subtracted on each button click | | display | 'standard' \| 'spinner' | 'standard' | standard: both buttons on the right; spinner: remove left, input centre, add right |

Display modes

standard (default) — both buttons grouped on the right:

[startAdornment?]  [    input    ]  [− +]

spinner — decrement on the left, input centred, increment on the right:

[−]  [    input    ]  [+]

Example

import { NumberField } from '@foi/design-system';

// Standard — quantity picker with bounds
<NumberField
  name='quantity'
  control={control}
  label='Quantity'
  min={1}
  max={99}
  step={1}
/>

// Spinner layout
<NumberField
  name='quantity'
  control={control}
  label='Quantity'
  display='spinner'
  min={0}
  max={100}
/>

Theming

Wrap your app in ThemeProvider and pass one or more named themes. The active theme is selected by the theme prop and its tokens are injected as CSS custom properties that every component consumes.

import { ThemeProvider } from '@foi/design-system';
import { darkTheme } from '@foi/design-system/theme';

<ThemeProvider themes={[darkTheme]} theme='dark'>
  <App />
</ThemeProvider>;

Theme structure

A theme is a plain object with a name, an optional fonts map, and a components map. Each component key holds one or more variants. The DEFAULT variant is the fallback used at runtime when no other variant is selected — define it in any theme that will be used standalone. When adding extra variants to an existing theme (from another project), DEFAULT is optional.

Each variant contains:

  • ROOT — static styles applied unconditionally (e.g. border-radius, background-color)
  • EVENTS — styles scoped to interaction states. The complete set of possible states is:

| State | Description | | ---------------- | ------------------------------------------------------------------------------- | | ENABLED | Default resting state — element is interactive and fully visible | | VALUE | The filled or selected part of the element (e.g. track fill, typed text colour) | | HOVER | While the pointer is over the element | | ACTIVE | While the element is being pressed | | FOCUS | When the element has keyboard focus | | ERROR | Container styles when validation fails | | ERROR_VALUE | Value part when in error state | | ERROR_HOVER | Hover while in error state | | ERROR_ACTIVE | Press while in error state | | ERROR_FOCUS | Focus while in error state | | DISABLED | When the element is not interactive | | DISABLED_VALUE | Value part when disabled | | LOADING | During a pending async operation | | LOADING_VALUE | Value part during loading |

Not every component uses all states — each component defines only the states relevant to its interaction model. The TypeScript type exported from the component's theme file tells you exactly which states are available:

import type { BUTTON } from '@foi/design-system/theme';
// BUTTON includes the EVENTS type — autocomplete shows only the states Button supports
import type { BUTTON } from '@foi/design-system/theme';

const component = {
  BUTTON: {
    DEFAULT: {
      ROOT: {
        'border-radius': '8px',
      },
      EVENTS: {
        ENABLED: {
          'background-color': '#0070f3',
          'color-primary': '#ffffff',
        },
        HOVER: {
          'background-color': '#005bb5',
        },
        FOCUS: {
          'outline-color': '#93c5fd',
          'outline-width': '2px',
          'outline-style': 'solid',
          'outline-offset': '2px',
        },
        DISABLED: {
          'background-color': '#e5e7eb',
          'color-primary': '#9ca3af',
        },
      },
    },
    DANGER: {
      EVENTS: {
        ENABLED: {
          'background-color': '#dc2626',
          'color-primary': '#ffffff',
        },
        HOVER: {
          'background-color': '#b91c1c',
        },
      },
    },
  },
} as const satisfies BUTTON;

The satisfies BUTTON check enforces that every token key is valid for that component — TypeScript reports an error if a token name is misspelled. DEFAULT is optional, so you can use the same type whether you are authoring a full theme or just adding extra variants.

Adding variants from another project

If you only want to extend an existing theme with new variants, omit DEFAULT — the DS base theme already provides it as a runtime fallback:

import type { BUTTON } from '@foi/design-system/theme';

const myVariants = {
  BUTTON: {
    DANGER: {
      EVENTS: {
        ENABLED: { 'background-color': '#dc2626', 'color-primary': '#ffffff' },
        HOVER: { 'background-color': '#b91c1c' },
      },
    },
    GHOST: {
      EVENTS: {
        ENABLED: { 'background-color': 'transparent', 'color-primary': '#111827' },
        HOVER: { 'background-color': '#f3f4f6' },
      },
    },
  },
} as const satisfies BUTTON;

Selecting a variant

Pass the variant prop to a component to switch between the variants you defined in the theme. The default is always 'default'.

<Button label='Delete' variant='danger' />

One-off theme override

Any component also accepts a theme prop — a partial EVENTS object — to override tokens for that single instance without touching the global theme.

<Button label='Special' theme={{ ENABLED: { 'background-color': '#7c3aed' } }} />

DataGrid

An async data table that delegates all data operations to the server via onFetch.

Defining columns

import { DataGrid } from '@foi/design-system';
import type { DataGridColumn } from '@foi/design-system';

interface Product {
  id: number;
  name: string;
  price: number;
}

const columns: DataGridColumn<Product>[] = [
  {
    key: 'name',
    label: 'Product', // string → wrapped in the themed --DATAGRID-thLabel span
    type: 'text',
    filter: { type: 'search' },
  },
  {
    key: 'price',
    label: 'Price',
    type: 'number',
    render: value => `$${Number(value).toLocaleString('es-CL')}`,
  },
];

label accepts any ReactNode. A plain string is automatically wrapped in the themed --DATAGRID-thLabel span so it inherits the header typography. Pass a custom node to render icons, badges, or anything else without that wrapper:

import { Icon } from '@foi/design-system';

{
  key: 'price',
  label: (
    <span style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
      <Icon name='payments' size='sm' />
      Price
    </span>
  ),
  type: 'number',
}

The onFetch contract

onFetch receives { page, pageSize, sort?, filters? } and must return { data, total }. It is called automatically whenever the page, sort, or any filter changes.

const onFetch = async ({ page, pageSize, sort, filters }) => {
  const response = await api.getProducts({ page, pageSize, sort, filters });
  return { data: response.items, total: response.total };
};

<DataGrid<Product> columns={columns} onFetch={onFetch} />;

Action columns

Add a column with type: 'options' and a render prop to define row-level actions. The render receives (value, row) — use row to call your handlers or control Modal state.

const [pendingDelete, setPendingDelete] = useState<Product | null>(null);

const columnsWithActions: DataGridColumn<Product>[] = [
  ...columns,
  {
    key: 'actions',
    label: '',
    type: 'options',
    sortable: false,
    render: (_value, row) => (
      <>
        <IconButton icon={<Icon name='edit' size='sm' />} onClick={() => onEdit(row)} aria-label='Edit' />
        <IconButton icon={<Icon name='delete' size='sm' />} onClick={() => setPendingDelete(row)} aria-label='Delete' />
      </>
    ),
  },
];

Pagination modes

| Prop | Value | Behaviour | | ----------------- | -------------- | ------------------------------------------------------ | | paginationType | 'pagination' | Renders a <Pagination> bar below the table (default) | | paginationType | 'scroll' | Appends rows as the user scrolls down | | pageSizeOptions | number[] | Shows a rows-per-page selector in the bar |

Modal

A portal-based overlay dialog. Renders into document.body so it sits above all other content regardless of DOM nesting.

Basic usage

import { Modal, Button, Icon } from '@foi/design-system';

const [open, setOpen] = useState(false);

<Modal
  open={open}
  onClose={() => setOpen(false)}
  header={
    <>
      <Icon name='info' />
      <span className='--MODAL-title'>Información</span>
    </>
  }
  footer={
    <>
      <Button onClick={() => setOpen(false)} variant='ghost'>
        Cancelar
      </Button>
      <Button onClick={() => setOpen(false)}>Aceptar</Button>
    </>
  }
>
  Contenido del modal.
</Modal>;

Apply --MODAL-title to the title element inside header to receive the themed font and colour styles.

Props

| Prop | Type | Default | Description | | ----------------- | -------------- | ------- | ------------------------------------------------- | | open | boolean | — | Controls visibility | | onClose | () => void | — | Called when the modal requests to close | | header | ReactNode | — | Full header bar content (rendered before ×) | | showCloseButton | boolean | true | Whether to render the × icon button | | size | 'md' \| 'lg' | 'md' | 'md' = 480 px, 'lg' = 720 px | | children | ReactNode | — | Main body content | | footer | ReactNode | — | Footer row content (typically action buttons) | | staticBackdrop | boolean | false | When true, backdrop click and Escape do nothing | | className | string | — | Extra CSS class on the modal panel |

Close triggers

| Trigger | staticBackdrop=false | staticBackdrop=true | | -------------- | ---------------------- | --------------------- | | × button | ✓ | ✓ | | Backdrop click | ✓ | — | | Escape key | ✓ | — |

Badge

Wraps any element and overlays a small indicator at one of its corners. Renders a numeric count, a capped overflow label (max+), or a plain dot.

Props

| Prop | Type | Default | Description | | ---------- | -------------------------------------------------------------- | ------------- | ------------------------------------------------- | | children | ReactNode | — | Element to wrap | | count | number | — | Number to display; hidden when 0 or undefined | | max | number | — | Cap value — shows max+ when count exceeds it | | dot | boolean | false | Show a small dot instead of a number | | position | 'top-right' \| 'top-left' \| 'bottom-right' \| 'bottom-left' | 'top-right' | Corner where the indicator is placed | | variant | string | 'default' | Theme variant | | theme | EVENTS | — | One-off token override |

The indicator animates in with a spring-scale effect on mount. The animation direction adapts automatically to the chosen position.

Example

import { Badge, IconButton, Icon } from '@foi/design-system';

// Count
<Badge count={5}>
  <IconButton icon={<Icon name='notifications' />} onClick={openNotifications} />
</Badge>

// Overflow cap — displays "999+"
<Badge count={1200} max={999}>
  <IconButton icon={<Icon name='mail' />} onClick={openMail} />
</Badge>

// Dot — no number
<Badge dot position='bottom-right'>
  <IconButton icon={<Icon name='settings' />} onClick={openSettings} />
</Badge>

Theming

import type { BADGE } from '@foi/design-system/theme';

const badge = {
  BADGE: {
    DEFAULT: {
      EVENTS: {
        ENABLED: {
          'background-color': '#ef4444',
          'color-primary': '#ffffff',
        },
      },
    },
  },
} as const satisfies BADGE;

Button

The primary interactive element. Accepts any ReactNode as children and natively supports all standard <button> HTML attributes.

Props

| Prop | Type | Default | Description | | ----------- | ---------------------- | ----------- | ----------------------------------------------- | | children | ReactNode | — | Button label or content | | type | 'button' \| 'submit' | 'button' | HTML button type | | iconStart | JSX.Element | — | Icon rendered before the label | | iconEnd | JSX.Element | — | Icon rendered after the label | | loading | boolean | false | Shows a loading indicator; button stays mounted | | disabled | boolean | false | Prevents interaction | | className | string | — | Extra CSS class | | variant | string | 'default' | Theme variant | | theme | EVENTS | — | One-off token override |

Example

import { Button, Icon } from '@foi/design-system';

<Button onClick={handleSave}>Guardar</Button>
<Button type='submit' iconStart={<Icon name='send' />}>Enviar</Button>
<Button loading={isSaving} disabled={isSaving}>Guardando…</Button>
<Button variant='danger' onClick={handleDelete}>Eliminar</Button>

Icon

Renders a single icon from the Material Symbols Rounded font. The font is loaded automatically by ThemeProvider.

Props

| Prop | Type | Default | Description | | ----------- | ------------------------------ | ------- | ---------------------------------- | | name | string | — | Material Symbols ligature name | | fill | boolean | false | Use the filled variant of the icon | | size | 'sm' \| 'md' \| 'lg' \| 'xl' | 'md' | Icon size | | className | string | — | Extra CSS class | | style | CSSProperties | — | Inline styles |

Example

import { Icon } from '@foi/design-system';

<Icon name='home' />
<Icon name='search' size='lg' />
<Icon name='favorite' fill />

IconButton

A button that renders a single icon with no visible text. Accepts all standard <button> HTML attributes.

Props

| Prop | Type | Default | Description | | ----------- | ------------- | ----------- | ------------------------------------------------- | | icon | JSX.Element | — | Icon element to render (use <Icon />) | | onClick | function | — | Click handler | | isFlipped | boolean | false | Mirrors the icon horizontally (useful for arrows) | | disabled | boolean | false | Prevents interaction | | className | string | — | Extra CSS class | | variant | string | 'default' | Theme variant | | theme | EVENTS | — | One-off token override |

Example

import { IconButton, Icon } from '@foi/design-system';

<IconButton icon={<Icon name='close' />} onClick={() => setOpen(false)} />
<IconButton icon={<Icon name='arrow_forward' />} isFlipped onClick={goBack} />

Checkbox

A single checkbox. Can be used standalone (controlled or RHF), or as a child of CheckboxGroup / CheckboxTree.

Props

| Prop | Type | Default | Description | | --------------- | ---------------------------- | ----------- | ------------------------------------------------ | | name | string | — | RHF field name (required in standalone RHF mode) | | control | Control | — | RHF control object | | checked | boolean | — | Controlled mode value (use with onChecked) | | onChecked | (checked: boolean) => void | — | Controlled mode change handler | | label | string | — | Text label next to the checkbox | | icon | JSX.Element | checkmark | Custom icon shown when checked | | disabled | boolean | false | Prevents interaction | | helperText | string | — | Hint text below the checkbox | | showErrorText | boolean | true | Show validation error message | | variant | string | 'default' | Theme variant | | theme | EVENTS | — | One-off token override |

Example

import { Checkbox } from '@foi/design-system';

// RHF
<Checkbox name='agree' control={control} label='Acepto los términos' />;

// Controlled
const [checked, setChecked] = useState(false);
<Checkbox checked={checked} onChecked={setChecked} label='Recordarme' />;

Radio

A single radio button. Always used inside RadioGroup — it receives its state from the group context automatically.

Props

| Prop | Type | Default | Description | | ---------- | ------------- | ----------- | -------------------------------------------- | | value | string | — | The value this option represents | | label | string | — | Text label next to the radio | | icon | JSX.Element | filled dot | Custom icon shown when selected | | disabled | boolean | false | Overrides the group disabled for this item | | variant | string | 'default' | Theme variant | | theme | EVENTS | — | One-off token override |

Select

A dropdown select field backed by a virtual-scroll list. Integrates with RHF and stores the selected value string.

Props

| Prop | Type | Default | Description | | ------------------ | -------------- | ----------- | --------------------------------------------------- | | name | string | — | RHF field name | | label | string | — | Floating label | | control | Control | — | RHF control object | | options | OptionProp[] | — | List of { value, label, description? } items | | disabled | boolean | false | Prevents interaction | | range | number | 100 | Options per page (virtual scroll activates above 3) | | hasSearch | boolean | false | Shows a search input — filters after 3 characters | | hasDescription | boolean | false | Renders a secondary description line per option | | hasStaticOptions | boolean | false | Renders dropdown inline (no absolute positioning) | | helperText | string | — | Hint text below the field | | showErrorText | boolean | true | Show validation error message | | hasPadding | boolean | false | Add bottom padding for helper/error text | | variant | string | 'default' | Theme variant | | theme | EVENTS | — | One-off token override |

Example

import { Select } from '@foi/design-system';

const options = [
  { value: 'cl', label: 'Chile' },
  { value: 'ar', label: 'Argentina', description: 'Buenos Aires' },
];

<Select name='country' control={control} label='País' options={options} hasSearch />;

ColorField

A color picker field with a floating label and a swatch preview. The RHF value is always a #RRGGBB hex string. The color can be chosen via a gradient picker, a hue slider, or by typing directly in the HEX / RGB / HSL inputs inside the picker menu. The display format of the trigger (the text visible in the closed field) is controlled by the format prop; the format can also be cycled interactively from inside the menu.

Props

| Prop | Type | Default | Description | | --------------- | ------------------------- | ----------- | ----------------------------------------------------------------- | | name | string | — | RHF field name. The stored value is always a #RRGGBB hex string | | label | string | — | Floating label text | | control | Control | — | RHF control object | | format | 'HEX' \| 'RGB' \| 'HSL' | 'HEX' | Format used to display the color value in the closed trigger | | disabled | boolean | false | Prevents all interaction | | helperText | string | — | Hint text shown below the field when there is no error | | showErrorText | boolean | true | Show the validation error message below the field | | hasPadding | boolean | false | Add bottom padding to reserve space for helper/error text | | className | string | — | Extra CSS class on the root element | | variant | string | 'default' | Theme variant name | | theme | EVENTS | — | One-off token override for this instance |

Example

import { ColorField } from '@foi/design-system';
import { z } from 'zod';

const schema = z.object({
  brandColor: z.string().regex(/^#[0-9A-Fa-f]{6}$/, 'Select a valid color'),
});

<ColorField
  name='brandColor'
  control={control}
  label='Brand color'
  format='HEX'
  helperText='Used for headers and buttons'
/>;

Format display

| format | Trigger value example | | -------- | --------------------- | | 'HEX' | #3B82F6 | | 'RGB' | rgb(59, 130, 246) | | 'HSL' | hsl(217, 91%, 60%) |

The format can also be changed interactively from inside the picker menu using the up/down arrow buttons next to the color inputs, without affecting the stored hex value.

Theming

import type { COLORFIELD } from '@foi/design-system/theme';

const colorField = {
  COLORFIELD: {
    DEFAULT: {
      ROOT: { 'border-radius': '8px' },
      EVENTS: {
        ENABLED: {
          'background-color': '#ffffff',
          'border-color': '#d1d5db',
          'border-width': '1px',
          'border-style': 'solid',
          'color-primary': '#111827',
          'color-secondary': '#6b7280',
        },
        HOVER: { 'border-color': '#9ca3af' },
        FOCUS: { 'border-color': '#3b82f6' },
        VALUE: { 'border-color': '#3b82f6' },
        ERROR: { 'border-color': '#ef4444' },
        DISABLED: { 'background-color': '#f9fafb', 'color-primary': '#9ca3af' },
      },
    },
  },
} as const satisfies COLORFIELD;

DatePicker

A date input field with an inline or floating calendar. Stores a Date value in RHF.

Props

| Prop | Type | Default | Description | | ------------------ | -------------- | ----------- | ----------------------------------------------------- | | name | string | — | RHF field name | | label | string | — | Floating label | | control | Control | — | RHF control object | | language | 'es' \| 'en' | browser | Date format and labels (DD/MM/YYYY vs MM/DD/YYYY) | | minDate | Date | — | Earliest selectable date | | maxDate | Date | — | Latest selectable date | | disablePast | boolean | false | Disables all dates before today | | disableFuture | boolean | false | Disables all dates after today | | hasStaticOptions | boolean | false | Renders calendar inline instead of floating | | disabled | boolean | false | Prevents interaction | | helperText | string | — | Hint text below the field | | showErrorText | boolean | true | Show validation error message | | hasPadding | boolean | false | Add bottom padding for helper/error text | | variant | string | 'default' | Theme variant | | theme | EVENTS | — | One-off token override |

Example

import { DatePicker } from '@foi/design-system';

<DatePicker
  name='birthDate'
  control={control}
  label='Fecha de nacimiento'
  language='es'
  disableFuture
  maxDate={new Date('2005-01-01')}
/>;

Switch

A toggle switch that stores a boolean value in RHF.

Props

| Prop | Type | Default | Description | | ------------ | ------------- | ----------- | ----------------------------- | | name | string | — | RHF field name | | control | Control | — | RHF control object | | label | string | — | Text label next to the switch | | iconOn | JSX.Element | — | Icon on the thumb when on | | iconOff | JSX.Element | — | Icon on the thumb when off | | disabled | boolean | false | Prevents interaction | | helperText | string | — | Hint text below the switch | | variant | string | 'default' | Theme variant | | theme | EVENTS | — | One-off token override |

Example

import { Switch, Icon } from '@foi/design-system';

<Switch
  name='notifications'
  control={control}
  label='Activar notificaciones'
  iconOn={<Icon name='notifications' />}
  iconOff={<Icon name='notifications_off' />}
/>;

Slider

A range slider that stores a number[] value in RHF. Single-thumb mode uses a 1-element array; range mode uses a 2-element array.

Props

| Prop | Type | Default | Description | | ------------ | ---------------------------- | -------------- | ------------------------------------------------- | | name | string | — | RHF field name | | control | Control | — | RHF control object | | min | number | — | Minimum value | | max | number | — | Maximum value (Infinity disables click-to-seek) | | step | number | 1 | Snap granularity | | distance | number | 0 | Minimum gap between thumbs (range mode only) | | direction | 'horizontal' \| 'vertical' | 'horizontal' | Slider orientation | | track | 'normal' \| 'inverted' | 'normal' | Fill direction of the track | | showMarks | boolean | false | Show a dot at every step position | | iconMin | JSX.Element | — | Icon inside the min/single thumb | | iconMax | JSX.Element | — | Icon inside the max thumb (range mode only) | | helperText | string | — | Hint text below the slider | | disabled | boolean | false | Prevents interaction | | variant | string | 'default' | Theme variant | | theme | EVENTS | — | One-off token override |

Example

import { Slider } from '@foi/design-system';

// Single thumb — value is [number]
<Slider name='volume' control={control} min={0} max={100} />

// Range — value is [number, number]
<Slider name='priceRange' control={control} min={0} max={10000} step={100} distance={500} showMarks />

Skeleton

An animated placeholder rendered while content is loading. Has no logic — simply swap it out once your data arrives.

Props

| Prop | Type | Default | Description | | ----------- | ----------------------------- | --------------- | -------------------------- | | variant | 'rectangular' \| 'circular' | 'rectangular' | Shape of the placeholder | | width | string \| number | — | CSS width or pixel number | | height | string \| number | — | CSS height or pixel number | | className | string | — | Extra CSS class |

Example

import { Skeleton } from '@foi/design-system';

// While loading a user avatar
{
  isLoading ? <Skeleton variant='circular' width={40} height={40} /> : <Avatar src={user.photo} />;
}

// While loading a text block
{
  isLoading ? <Skeleton width='100%' height={20} /> : <p>{content}</p>;
}

Pagination

A standalone page-navigation bar. DataGrid uses this internally, but it can also be used independently.

Props

| Prop | Type | Default | Description | | ------------------ | ------------------------ | ------- | ------------------------------------------------- | | page | number | — | Zero-based current page index | | total | number | — | Total row count (used to compute page count) | | pageSize | number | — | Rows per page | | onPageChange | (page: number) => void | — | Fired when the user navigates to a different page | | pageSizeOptions | number[] | — | Shows a rows-per-page selector with these options | | onPageSizeChange | (size: number) => void | — | Fired when the user changes the page size | | loading | boolean | false | Disables all controls while data is fetching |

Example

import { Pagination } from '@foi/design-system';

<Pagination
  page={page}
  total={total}
  pageSize={pageSize}
  onPageChange={setPage}
  pageSizeOptions={[10, 25, 50]}
  onPageSizeChange={setPageSize}
/>;

CheckboxGroup

Manages a set of Checkbox children as a string[] RHF field. The group value is the array of value props of all checked children.

Props

| Prop | Type | Default | Description | | --------------- | ---------------------------- | ------------ | ---------------------------------------------- | | name | string | — | RHF field name | | control | Control | — | RHF control object | | children | ReactNode | — | <Checkbox> elements (each must have value) | | label | string | — | Group label above the checkboxes | | direction | 'vertical' \| 'horizontal' | 'vertical' | Layout direction of the items | | disabled | boolean | false | Disables all children | | helperText | string | — | Hint text below the group | | showErrorText | boolean | true | Show validation error message | | variant | string | 'default' | Theme variant | | theme | EVENTS | — | One-off token override |

Example

import { CheckboxGroup, Checkbox } from '@foi/design-system';

const schema = z.object({
  skills: z.array(z.string()).min(1, 'Selecciona al menos una'),
});

<CheckboxGroup name='skills' control={control} label='Habilidades' direction='horizontal'>
  <Checkbox value='react' label='React' />
  <Checkbox value='vue' label='Vue' />
  <Checkbox value='svelte' label='Svelte' />
</CheckboxGroup>;

CheckboxTree

A hierarchical checkbox group. The parent checkbox controls all children simultaneously and shows an indeterminate state when some (but not all) children are checked.

Props

| Prop | Type | Default | Description | | ------------------- | ------------- | ----------- | ------------------------------------------------- | | name | string | — | RHF field name | | control | Control | — | RHF control object | | children | ReactNode | — | <Checkbox> elements (each must have value) | | label | string | — | Label on the parent (select-all) checkbox | | iconChecked | JSX.Element | checkmark | Icon on the parent when all children are checked | | iconIndeterminate | JSX.Element | dash | Icon on the parent when some children are checked | | disabled | boolean | false | Disables the parent and all children | | helperText | string | — | Hint text below the tree | | showErrorText | boolean | true | Show validation error message | | variant | string | 'default' | Theme variant | | theme | EVENTS | — | One-off token override |

Example

import { CheckboxTree, Checkbox } from '@foi/design-system';

<CheckboxTree name='permissions' control={control} label='Seleccionar todo'>
  <Checkbox value='read' label='Leer' />
  <Checkbox value='write' label='Escribir' />
  <Checkbox value='delete' label='Eliminar' />
</CheckboxTree>;

RadioGroup

Manages a set of Radio children as a single string RHF field. The value is the value prop of the selected child.

Props

| Prop | Type | Default | Description | | --------------- | ---------------------------- | ------------ | ------------------------------------------- | | name | string | — | RHF field name | | control | Control | — | RHF control object | | children | ReactNode | — | <Radio> elements (each must have value) | | label | string | — | Group label above the radios | | direction | 'vertical' \| 'horizontal' | 'vertical' | Layout direction of the items | | disabled | boolean | false | Disables all children | | helperText | string | — | Hint text below the group | | showErrorText | boolean | true | Show validation error message | | variant | string | 'default' | Theme variant | | theme | EVENTS | — | One-off token override |

Example

import { RadioGroup, Radio } from '@foi/design-system';

const schema = z.object({
  plan: z.string().min(1, 'Selecciona un plan'),
});

<RadioGroup name='plan' control={control} label='Plan' direction='horizontal'>
  <Radio value='free' label='Gratis' />
  <Radio value='pro' label='Pro' />
  <Radio value='enterprise' label='Enterprise' />
</RadioGroup>;

Chip

A compact inline label for displaying statuses, tags, or categories. Height is fixed at 24px and width adapts to its content. Can optionally render as an interactive button.

Props

| Prop | Type | Default | Description | | ----------- | ------------ | ----------- | ------------------------------------------------------------ | | children | ReactNode | — | Label text, number, or custom content | | onClick | () => void | — | When provided, renders as <button> with interactive states | | disabled | boolean | false | Disables the button (only applies when onClick is set) | | variant | string | 'default' | Theme variant — any key defined under CHIP in your theme | | className | string | — | Extra CSS class | | theme | EVENTS | — | One-off token override |

When children is a string or number it is wrapped in a span.--CHIP-label that applies horizontal padding. Pass any other ReactNode to render custom content (e.g. icon + text) directly inside the chip.

When onClick is provided the chip renders as a <button type="button"> and the theme's HOVER, ACTIVE, FOCUS, and DISABLED states become active. Without onClick the chip is a non-interactive <div> and those states are ignored.

Example

import { Chip, Icon } from '@foi/design-system';

// Decorative — string or number, wrapped in padded span automatically
<Chip>Activo</Chip>
<Chip variant='myVariant'>{42}</Chip>

// Decorative — custom ReactNode renders directly inside the chip
<Chip variant='myVariant'>
  <Icon name='star' />
  <span>Destacado</span>
</Chip>

// Clickable — renders as <button>, hover/active/focus states apply
<Chip onClick={() => setFilter('react')}>React</Chip>
<Chip onClick={handleSelect} disabled>Deshabilitado</Chip>

Toast

A notification system built around a React context. ThemeProvider automatically includes the provider — no additional setup required. Call useToast from anywhere inside the tree to push transient messages.

Setup

ThemeProvider includes ToastProvider internally. Optionally configure position and default duration via the toast prop:

<ThemeProvider themes={[darkTheme]} theme='dark' toast={{ position: 'top-right', duration: 4000 }}>
  <App />
</ThemeProvider>

useToast

import { useToast } from '@foi/design-system/hooks';

const { push } = useToast();

push('Archivo guardado', { variant: 'success', icon: 'check_circle' });

push options

| Option | Type | Default | Description | | ---------- | --------- | ----------- | -------------------------------------------------- | | variant | string | 'default' | Theme variant (maps to a key defined in the theme) | | icon | string | — | Material Symbols icon name rendered on the left | | duration | number | 3000 | Milliseconds before the toast auto-dismisses | | closable | boolean | false | Render a × button so the user can dismiss manually |

ToastPosition

| Value | Description | | ----------------- | ------------- | | 'bottom-right' | Default | | 'bottom-center' | Bottom center | | 'bottom-left' | Bottom left | | 'top-right' | Top right | | 'top-center' | Top center | | 'top-left' | Top left |

Example

import { useToast } from '@foi/design-system/hooks';

const Notifier = () => {
  const { push } = useToast();

  const save = async () => {
    try {
      await api.save();
      push('Guardado correctamente', { icon: 'check_circle' });
    } catch {
      push('No se pudo guardar', { icon: 'error', closable: true, duration: 8000 });
    }
  };

  return <Button onClick={save}>Guardar</Button>;
};

Defining toast variants in the theme

Toast variants are defined in the theme under the TOAST key. Each variant must be self-contained (all tokens, not just overrides) because useCreateComponentStyles loads only the requested variant:

import type { TOAST } from '@foi/design-system/theme';

const toast = {
  TOAST: {
    DEFAULT: {
      ROOT: { 'background-color': '#1e293b', 'border-radius': '6px' },
      EVENTS: {
        ENABLED: {
          'color-primary': '#f8fafc',
          'border-color': '#334155',
          'icon-color': '#94a3b8',
        },
      },
    },
    // Add as many named variants as your design requires
    MY_VARIANT: {
      ROOT: { 'background-color': '#1e293b', 'border-radius': '6px' },
      EVENTS: {
        ENABLED: {
          'color-primary': '#f8fafc',
          'border-color': '#10b981',
          'icon-color': '#10b981',
        },
      },
    },
  },
} as const satisfies TOAST;

Project structure

src/
├── components/
│   ├── atoms/       # Primitive UI components
│   ├── molecules/   # Composed components built from atoms
│   └── organisms/   # Complex components composed from molecules and atoms
├── hocs/
│   ├── ThemeProvider/   # Global theme context and CSS-variable injection
│   ├── MarginPage/      # Page layout wrapper
│   ├── OutsideEvent/    # Click-outside detection
│   └── ScrollToTop/     # Scroll restoration on navigation
└── pages/
    └── Playground/      # Dev sandbox for manual testing