# fma-ds
> A personal component registry in the shadcn/ui format, built on react-aria-components: components, blocks, libs, hooks and themes copied into your project by the shadcn CLI (not an npm package).
Register the namespace in components.json:
```json
{
"registries": {
"@fma-ds": "https://ds.fmartinsandre.dev.br/r/{name}.json"
}
}
```
Then add items by name: `pnpm dlx shadcn@latest add @fma-ds/button`. Dependencies between items resolve through the same namespace. Everything installs into `fma-ds` folders (`components/fma-ds/`, `hooks/fma-ds/`, `lib/fma-ds/`), so it never collides with the official shadcn components.
The APIs follow React Aria, not Radix: `isDisabled` instead of `disabled`, `onPress` instead of `onClick`, and no `asChild` (use `LinkButton` for a link that looks like a button). Wrap the app in React Aria's `RouterProvider` for client-side links (see the Routing guide).
License: MIT, except for themes adapted from tweakcn (Apache-2.0). Each registry item states its own in meta.license; upstream notices are in https://github.com/fmartins-andre/fma-ds/blob/main/THIRD_PARTY_NOTICES.md.
## Guides
### Compatibility
fma-ds is code you copy, so it runs on your project's toolchain. These are the versions it is built and tested against.
| Requirement | Version | How you get it |
| --- | --- | --- |
| React and React DOM | `>=19.0.0` | Your project |
| Tailwind CSS | `>=4.0.0` | Your project; v3 doesn't work, tokens are CSS-first |
| shadcn CLI | `^4.22.1` | Run with `npx`, or as a dev dependency |
| react-aria-components | `^1.22.1` | Installed by the CLI with each item |
Each item lists the rest of its npm dependencies in its registry file, and the CLI installs them.
Node.js only matters to the shadcn CLI, so use a version it supports. The Node and pnpm versions in this repository's `package.json` apply to working on the registry itself, not to using it.
## Project setup
You need a project initialized for shadcn/ui: a `components.json` and a stylesheet with Tailwind v4. Then follow the [installation steps](/docs).
### Dark mode
Dark mode is on by a `dark` class on an ancestor, usually ``. Every theme defines both palettes, so adding the class is all it takes.
Your stylesheet needs this variant, which the shadcn setup adds. With it, the tokens switch by themselves:
```css
@custom-variant dark (&:is(.dark *));
```
fma-ds doesn't ship a toggle or a provider, and it doesn't read `prefers-color-scheme` by itself: which mode applies is your app's decision.
## Avoid a flash on load
Set the class in an inline script in `
`, before the first paint. This one follows the saved choice and falls back to the system preference:
```html
```
If your framework renders on the server, add `suppressHydrationWarning` to ``: the class is set before React hydrates.
## Add a toggle
```tsx
function setMode(mode: "light" | "dark") {
document.documentElement.classList.toggle("dark", mode === "dark");
localStorage.setItem("mode", mode);
}
```
Portaled overlays render under ``, so keep the class on `` or they will stay in the other mode.
### Routing
Links in fma-ds are react-aria links. Without a router they do a full page load, so wrap your app in React Aria's `RouterProvider` to send them through your router.
It takes `navigate`, called when a link is pressed, and `useHref`, which turns a path into the real `href` (needed when the app runs under a base path).
## TanStack Router
Put it in the root route, around the ``. This site runs this same code:
```tsx
import { Outlet, useRouter } from "@tanstack/react-router";
import { RouterProvider } from "react-aria-components";
// Links outside the app go through the browser.
const isPlainLink = (href: string) => /^([a-z][a-z\d+.-]*:|\/\/)/i.test(href);
function RootComponent() {
const router = useRouter();
return (
{
if (isPlainLink(href)) window.location.assign(href);
else void router.navigate({ href });
}}
useHref={(href) =>
isPlainLink(href) ? href : router.buildLocation({ href } as never).href
}
>
);
}
```
## React Router
Render it inside the router, because it uses the router's hooks:
```tsx
import { Outlet, useHref, useNavigate } from "react-router";
import { RouterProvider } from "react-aria-components";
export function RootLayout() {
const navigate = useNavigate();
return (
);
}
```
## Current page
`RouterProvider` handles navigation only. For active states, pass `aria-current="page"` where the component takes it, or use components that already do, such as `TabNav` and `Pagination`.
### Offline and privacy
fma-ds makes no network requests at runtime, with one exception. There is no telemetry, and themes don't load fonts from a CDN.
This was audited against every published item, looking for `fetch`, `XMLHttpRequest`, `sendBeacon`, WebSockets, remote imports and hard-coded URLs.
## The exception: emoji-picker
`EmojiPicker` downloads its data from `cdn.jsdelivr.net` (a pinned `emojibase-data` release) the first time it opens. To avoid that, do one of these:
- **`data`**: pass the emoji you already have, bundled or loaded on your server. Nothing is fetched.
- **`dataUrl`**: point it at your own copy of `emojibase-data`.
- **`shortcodes={false}`**: skips the optional shortcode requests.
## Fonts
Themes with catalog fonts install them as `@fontsource` packages, imported into your CSS and bundled by your build. A theme using a font outside the catalog says so when installed, and loading that font is up to you.
## Images
`Avatar`, `FileUpload` and similar components only load the URLs you give them. Nothing points to an external image by default.
## Offline-first apps
With the emoji data handled, the components work with no connection. Component code is in your bundle, so it's cached the way the rest of your app is.
### Touch
Controls meet WCAG 2.2 AA for pointer targets (2.5.8): at least 24 by 24 CSS pixels, or spaced far enough apart that a 24px circle on one doesn't touch the next. They are sized for desktop density, so an app used mostly by touch should pick larger sizes.
This was measured in the browser across every Storybook story, counting the invisible hit areas below.
## Sizes
| Control | Hit area |
| --- | --- |
| `Button` | 32px tall; `lg` is 36px, `sm` 28px, `xs` 24px |
| `Input`, `Select` and other fields | 32px tall |
| `Checkbox`, `Radio` | 16px box, 40 by 32px hit area |
| `Slider` thumb | 12px thumb, 28px hit area |
The hit area of the small controls is an `::after` pseudo-element, so give them room: a neighboring control placed inside it takes the press.
## Spaced instead of sized
These stay under 24px and rely on spacing: `Carousel` dots, `CodeBlock` fold toggles, `ColorPicker` swatches and `EmojiPicker` category tabs. Links inside a sentence are exempt, as in `Breadcrumb`.
## Larger targets
For touch-first screens, aim for the 44px of WCAG 2.5.5 (AAA). No size prop reaches it, so set the height where you use the component:
```tsx
```
### Updating items
The code lives in your project, so nothing updates by itself: you choose when to pull changes and which ones to keep.
Items have no version numbers. Each one has a content hash, which changes whenever its published code does:
```bash
curl -s https:///r/button.json | jq -r .meta.contentHash
```
Save the hash when you install an item, in a comment or a commit message, and a different value later means there is something new. The CLI rewrites imports on install, so hashing your local copy won't match. To see what changed, open the **Changelog** link on the item's page: it is the Git history of that item.
## Check before overwriting
```bash
npx shadcn@latest add @fma-ds/button --dry-run
npx shadcn@latest add @fma-ds/button --diff
```
`--dry-run` lists what would change, and `--diff` shows the difference for a file. Neither writes anything.
## Apply an update
```bash
npx shadcn@latest add @fma-ds/button --overwrite
```
This replaces your copy, including local edits. Commit first, then review the diff in git and bring your changes back. Keeping customizations in wrapper components, instead of editing the files, makes updates much easier.
### Contributing
fma-ds is a personal registry, developed in the open. Issues are welcome, and new components start from a real need in an app.
## Proposing a change
1. Open an [issue](https://github.com/fmartins-andre/fma-ds/issues) describing the need: what the app has to do, not how it should look.
2. Check that no existing item covers it. Prefer a variant or a prop over a new component.
3. Wait for agreement before opening a pull request, unless it's a small fix.
## Scope of the blocks
Blocks come from what real apps needed, mostly SaaS screens such as sign-in, pricing, billing and settings. They are not a general application kit, so a block enters when an app needs it, not to complete the catalog. Components stay general.
## What a new item needs
- A folder in `packages/ds/src/core//` with the component, `meta.json` and a story.
- React Aria for interaction, and the existing tokens: hover, focus and selection use `accent`.
- English default text, overridable through props.
- Stories that show real usage, because they are the examples on this site, with `play` functions for interaction tests.
- Accessible names, keyboard support and no automated accessibility violations.
The generated registry files are committed with the change. Before opening a pull request, run:
```bash
pnpm lint && pnpm type-check && pnpm test && pnpm --filter @fma-ds/ds build
```
## Going deeper
[`CONTRIBUTING.md`](https://github.com/fmartins-andre/fma-ds/blob/main/CONTRIBUTING.md) covers the environment, the anatomy of a component, themes and the Git workflow.
## Components
### Accordion
A vertically stacked set of headers that expand to reveal or hide associated content. Supports single or multiple items open at once.
- Install: `pnpm dlx shadcn@latest add @fma-ds/accordion`
- Registry item: https://ds.fmartinsandre.dev.br/r/accordion.json
- Docs: https://ds.fmartinsandre.dev.br/components/accordion
- npm: cn@^0.5.0, lucide-react@^1.61.0, react-aria-components@^1.22.1
```tsx
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/fma-ds/accordion";
export function AccordionDemo() {
return (
Is it accessible?Yes. It adheres to the WAI-ARIA design pattern.Is it styled?
Yes. It comes with default styles that match the other components' aesthetic.
Is it animated?
Yes. It's animated by default via CSS height transitions.
);
}
```
### Alert
A callout for important, static information — supports an icon, title, description, and an optional action, with semantic variants (destructive/info/success/warning/invert) and a `brand` variant for announcements and promos in the theme's second identity color.
- Install: `pnpm dlx shadcn@latest add @fma-ds/alert`
- Registry item: https://ds.fmartinsandre.dev.br/r/alert.json
- Docs: https://ds.fmartinsandre.dev.br/components/alert
- npm: class-variance-authority@^0.7.1, cn@^0.5.0
```tsx
import { Alert, AlertDescription, AlertTitle } from "@/components/fma-ds/alert";
export function AlertDemo() {
return (
Heads up!You can add components to your app using the CLI.
);
}
```
### Alert Dialog
A modal dialog that interrupts the user with important content and expects an explicit response (e.g. confirming a destructive action) — unlike Dialog, it can't be dismissed by clicking outside or pressing Escape.
- Install: `pnpm dlx shadcn@latest add @fma-ds/alert-dialog`
- Registry item: https://ds.fmartinsandre.dev.br/r/alert-dialog.json
- Docs: https://ds.fmartinsandre.dev.br/components/alert-dialog
- npm: cn@^0.5.0, react-aria-components@^1.22.1
- Registry: @fma-ds/button
```tsx
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/fma-ds/alert-dialog";
import { Button } from "@/components/fma-ds/button";
export function AlertDialogDemo() {
return (
Are you absolutely sure?
This action cannot be undone. This will permanently delete your account and remove your
data from our servers.
CancelContinue
);
}
```
### Aspect Ratio
Constrains content (typically an image or video) to a fixed width/height ratio, so it scales without distortion or layout shift.
- Install: `pnpm dlx shadcn@latest add @fma-ds/aspect-ratio`
- Registry item: https://ds.fmartinsandre.dev.br/r/aspect-ratio.json
- Docs: https://ds.fmartinsandre.dev.br/components/aspect-ratio
- npm: cn@^0.5.0
```tsx
import { AspectRatio } from "@/components/fma-ds/aspect-ratio";
export function AspectRatioDemo() {
return (
);
}
```
### Attachment
A file/attachment chip for chat-style UIs — shows a name, optional upload progress or error state, and an independently clickable remove action.
- Install: `pnpm dlx shadcn@latest add @fma-ds/attachment`
- Registry item: https://ds.fmartinsandre.dev.br/r/attachment.json
- Docs: https://ds.fmartinsandre.dev.br/components/attachment
- npm: class-variance-authority@^0.7.1, cn@^0.5.0
- Registry: @fma-ds/button
```tsx
import { FileTextIcon, XIcon } from "lucide-react";
import type * as React from "react";
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
} from "@/components/fma-ds/attachment";
function FileAttachment({
name = "sales-dashboard.pdf",
description = "PDF · 2.4 MB",
icon = ,
...props
}: React.ComponentProps & {
name?: string;
description?: string;
icon?: React.ReactNode;
}) {
return (
{icon}{name}{description}
);
}
export function AttachmentDemo() {
return (
);
}
```
### Avatar
A user/entity image with a text or icon fallback for when the image fails to load. Composes into overlapping groups for showing multiple participants.
- Install: `pnpm dlx shadcn@latest add @fma-ds/avatar`
- Registry item: https://ds.fmartinsandre.dev.br/r/avatar.json
- Docs: https://ds.fmartinsandre.dev.br/components/avatar
- npm: cn@^0.5.0
```tsx
import { Avatar, AvatarFallback, AvatarImage } from "@/components/fma-ds/avatar";
export function AvatarDemo() {
return (
CN
);
}
```
### Badge
A small label for status, count, or category — inline with text or attached to another element.
- **Semantic variants**: info, success, warning, destructive and invert, in solid, `-light` and `-outline` styles.
- **Neutral**: `muted`/`muted-outline` for inactive or pending states.
- **Brand**: `brand`, `brand-light`, `brand-outline` and `brand-dot` in the theme's second identity color, for identity labels ("New", "AI", "Pro"), never for status.
- **Dots**: a `*-dot` family (primary, secondary, brand, success, warning, info, destructive, muted) that shows a colored dot before plain text, for select items and dense lists.
- **Shape**: size and radius options.
Can also render as a link.
- Install: `pnpm dlx shadcn@latest add @fma-ds/badge`
- Registry item: https://ds.fmartinsandre.dev.br/r/badge.json
- Docs: https://ds.fmartinsandre.dev.br/components/badge
- npm: class-variance-authority@^0.7.1, cn@^0.5.0
```tsx
import { Badge } from "@/components/fma-ds/badge";
export function BadgeDemo() {
return Badge;
}
```
### Breadcrumb
Shows the user's current location within a hierarchy, as a trail of links back to parent pages, with an ellipsis for collapsing long paths.
- Install: `pnpm dlx shadcn@latest add @fma-ds/breadcrumb`
- Registry item: https://ds.fmartinsandre.dev.br/r/breadcrumb.json
- Docs: https://ds.fmartinsandre.dev.br/components/breadcrumb
- npm: cn@^0.5.0, lucide-react@^1.61.0, react-aria-components@^1.22.1
```tsx
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
} from "@/components/fma-ds/breadcrumb";
export function BreadcrumbDemo() {
return (
HomeComponentsBreadcrumb
);
}
```
### Bubble
A single chat message bubble — start/end alignment for sent vs. received, several color variants, and an optional reactions pill overlaid on a corner.
- Install: `pnpm dlx shadcn@latest add @fma-ds/bubble`
- Registry item: https://ds.fmartinsandre.dev.br/r/bubble.json
- Docs: https://ds.fmartinsandre.dev.br/components/bubble
- npm: class-variance-authority@^0.7.1, cn@^0.5.0
```tsx
import { Bubble, BubbleContent } from "@/components/fma-ds/bubble";
export function BubbleDemo() {
return (
Hey, how's the new design system coming along?
);
}
```
### Button
Displays a button, or a component that looks like one, with a set of visual variants and sizes. Also exports LinkButton (react-aria-components' Link, same variants) for link-as-button use cases.
- Install: `pnpm dlx shadcn@latest add @fma-ds/button`
- Registry item: https://ds.fmartinsandre.dev.br/r/button.json
- Docs: https://ds.fmartinsandre.dev.br/components/button
- npm: class-variance-authority@^0.7.1, cn@^0.5.0, react-aria-components@^1.22.1
```tsx
import { Button } from "@/components/fma-ds/button";
export function ButtonDemo() {
return ;
}
```
### Button Group
Visually joins a row (or column) of buttons into one connected control, with shared borders and rounded end corners. Can include a separator or a plain text segment.
- Install: `pnpm dlx shadcn@latest add @fma-ds/button-group`
- Registry item: https://ds.fmartinsandre.dev.br/r/button-group.json
- Docs: https://ds.fmartinsandre.dev.br/components/button-group
- npm: class-variance-authority@^0.7.1, cn@^0.5.0
- Registry: @fma-ds/separator
```tsx
import { Button } from "@/components/fma-ds/button";
import { ButtonGroup } from "@/components/fma-ds/button-group";
export function ButtonGroupDemo() {
return (
);
}
```
### Calendar
A date grid for selecting a single date, multiple dates or a range.
- **Navigation**: the header opens month and year grids for fast navigation.
- **Granularity**: `granularity` switches the selection to whole months or years.
- **Availability**: `unavailableRanges` blocks booked spans, and an already-selected value that becomes blocked travels to the nearest available date.
- Install: `pnpm dlx shadcn@latest add @fma-ds/calendar`
- Registry item: https://ds.fmartinsandre.dev.br/r/calendar.json
- Docs: https://ds.fmartinsandre.dev.br/components/calendar
- npm: @internationalized/date@^3.12.4, class-variance-authority@^0.7.1, cn@^0.5.0, lucide-react@^1.61.0, react-aria-components@^1.22.1
- Registry: @fma-ds/button, @fma-ds/select, @fma-ds/use-available-date-correction, @fma-ds/date-granularity, @fma-ds/interval-helpers, @fma-ds/types
```tsx
import { Calendar } from "@/components/fma-ds/calendar";
export function CalendarDemo() {
return ;
}
```
### Card
A container for grouping related content, with header, title, description, action, content, and footer slots. Supports a compact size="sm" variant.
- Install: `pnpm dlx shadcn@latest add @fma-ds/card`
- Registry item: https://ds.fmartinsandre.dev.br/r/card.json
- Docs: https://ds.fmartinsandre.dev.br/components/card
- npm: cn@^0.5.0
```tsx
import { Button } from "@/components/fma-ds/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/fma-ds/card";
export function CardDemo() {
return (
Project alphaLast updated 2 hours ago
Card content goes here.
);
}
```
### Carousel
A carousel for cycling through slides (images, cards, or any content) with keyboard, swipe, and button navigation.
Built on embla-carousel-react; `CarouselPrevious`/`CarouselNext` are `Button`s wired to the carousel API.
- **Autoplay**: `autoplay` (`true` or `{ delay }`, 5000 ms by default) advances on its own and wraps to the first slide. Rotation pauses while the pointer is over the carousel or focus is inside it, and starts paused under reduced motion.
- **`CarouselAutoplayToggle`**: pause/resume — required for moving content by WCAG 2.2.2; a dev warning flags its absence.
- **`CarouselDots`**: one dot per slide; marks the current one and goes to a slide on press.
- **Announcements**: slide changes are announced (`aria-live`) only when not rotating.
The toggle and dots are not positioned; place them where they fit.
Built-in labels are in English; translate them with `i18n.labels`.
- Install: `pnpm dlx shadcn@latest add @fma-ds/carousel`
- Registry item: https://ds.fmartinsandre.dev.br/r/carousel.json
- Docs: https://ds.fmartinsandre.dev.br/components/carousel
- npm: cn@^0.5.0, embla-carousel-react@^8.6.0, lucide-react@^1.61.0
- Registry: @fma-ds/button
```tsx
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/fma-ds/carousel";
export function CarouselDemo() {
return (
{Array.from({ length: 5 }, (_, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: fixed-length demo slides, index IS the identity
{index + 1}
))}
);
}
```
### Chart
Composable chart primitives (ChartContainer, ChartTooltip, ChartLegend) wrapping Recharts, themed via CSS custom properties from a ChartConfig. Supports area, bar, line, and pie charts.
- Install: `pnpm dlx shadcn@latest add @fma-ds/chart`
- Registry item: https://ds.fmartinsandre.dev.br/r/chart.json
- Docs: https://ds.fmartinsandre.dev.br/components/chart
- npm: cn@^0.5.0, recharts@3.10.1
```tsx
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/fma-ds/chart";
const multiSeriesData = [
{ month: "January", desktop: 186, mobile: 80 },
{ month: "February", desktop: 305, mobile: 200 },
{ month: "March", desktop: 237, mobile: 120 },
{ month: "April", desktop: 73, mobile: 190 },
{ month: "May", desktop: 209, mobile: 130 },
{ month: "June", desktop: 214, mobile: 140 },
];
const multiSeriesConfig = {
desktop: { label: "Desktop", color: "var(--chart-1)" },
mobile: { label: "Mobile", color: "var(--chart-2)" },
} satisfies ChartConfig;
export function ChartStackedAreaChart() {
return (
value.slice(0, 3)}
/>
} />
);
}
```
### Checkbox
A control for a single on/off/indeterminate value.
Pair it with an external `Label` (via `id`/`htmlFor`) laid out as a flex row, rather than passing label text as children.
- **`CheckboxGroup`**: gathers several under one label and a `value` array (react-aria `CheckboxGroup`; each `Checkbox` takes a `value`).
- **`CheckboxCard`**: a choice card — the whole card is the checkbox, its children are the content (an `IconTile`, `FieldContent` with `FieldTitle` and `FieldDescription`) and a square indicator sits at the end. Selected cards take a `primary` tint, hovered ones `accent`.
- Install: `pnpm dlx shadcn@latest add @fma-ds/checkbox`
- Registry item: https://ds.fmartinsandre.dev.br/r/checkbox.json
- Docs: https://ds.fmartinsandre.dev.br/components/checkbox
- npm: cn@^0.5.0, lucide-react@^1.61.0, react-aria-components@^1.22.1
- Registry: @fma-ds/field
```tsx
import { Checkbox } from "@/components/fma-ds/checkbox";
import { Label } from "@/components/fma-ds/label";
export function CheckboxDemo() {
return (
);
}
```
### Code Block
A code block with shiki syntax highlighting, for docs and AI chat UIs. `` is complete on its own.
### Parts
- **`CodeBlockHeader`**: the chrome — `CodeBlockTitle`, `CodeBlockLanguage`, `CodeBlockWrapToggle`, `CodeBlockCopyButton`, `CodeBlockDownloadButton`.
- **`CodeBlockExpandButton`**: with `maxLines`.
- **`CodeBlockContent`**: to place inside your own scroll container.
- **`CodeBlockLineActions`**: one action group on the hovered or focused line.
A pinned copy button stays put while the code scrolls.
### Highlighting
shiki loads lazily on the first highlight (JavaScript regex engine, no WebAssembly) through the `code-block-highlight` lib. Pass `lines` from `highlightCode()` in a server loader, or `highlight={false}`, to never load it. Light and dark token colors switch with the theme.
### Lines
- **Marking**: `showLineNumbers`/`startLine`, `highlightedLines`/`highlightedWords`/`focusedLines` (line specs like `"2-4,7"`), `diff` and `lineLevels` — or shiki `transformers`.
- **`streaming`**: grows the code with a caret and stick-to-bottom, re-rendering only changed lines.
- **`foldable`**: folds by indentation.
- **`selectable`**: turns lines into a multi-select listbox.
- Install: `pnpm dlx shadcn@latest add @fma-ds/code-block`
- Registry item: https://ds.fmartinsandre.dev.br/r/code-block.json
- Docs: https://ds.fmartinsandre.dev.br/components/code-block
- npm: cn@^0.5.0, lucide-react@^1.61.0
- Registry: @fma-ds/button, @fma-ds/copy-button, @fma-ds/code-block-highlight
```tsx
import { CodeBlock, CodeBlockCopyButton } from "@/components/fma-ds/code-block";
const CODE = `import { useMemo } from "react";
type Item = { price: number; quantity: number };
export function useTotals(items: Item[]) {
return useMemo(() => {
const subtotal = items.reduce((sum, item) => {
return sum + item.price * item.quantity;
}, 0);
const tax = subtotal * 0.08;
return { subtotal, tax, total: subtotal + tax };
}, [items]);
}`;
export function CodeBlockDemo() {
return (
);
}
```
### Collapsible
Shows or hides a single section of content behind a trigger. A thin wrapper around react-aria-components' Disclosure — for several mutually-exclusive sections, use Accordion instead.
- Install: `pnpm dlx shadcn@latest add @fma-ds/collapsible`
- Registry item: https://ds.fmartinsandre.dev.br/r/collapsible.json
- Docs: https://ds.fmartinsandre.dev.br/components/collapsible
- npm: react-aria-components@^1.22.1
```tsx
import { InfoIcon } from "lucide-react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/fma-ds/collapsible";
export function CollapsibleDemo() {
return (
Can I use this in my project?
Yes. Free to use for personal and commercial projects. No attribution required.
);
}
```
### Color Picker
A composable color picker: `ColorPicker` holds the color and shares it with parts laid out in any way.
Without children, `ColorPicker` is a swatch that opens the default panel in a popover.
### Parts
- **`ColorPickerTrigger`** + **`ColorPickerContent`**: a popover.
- **`ColorPickerPanel`**: a column of controls, or a standalone inline picker.
- **`ColorPickerArea`**: OKLCH chroma × lightness at the hue (the part sRGB can't show is left blank), or HSB saturation × brightness.
- **`ColorPickerWheel`**: a hue ring whose children fill the square inside it.
- **`ColorPickerSlider`**: any channel of OKLCH, HSL, HSB or RGB, or alpha; horizontal or vertical, painted with the colors it reaches.
- **`ColorPickerFields`**: a format menu with hex or per-channel fields; `ColorPickerChannelInput` for a single channel.
- **`ColorPickerInput`**: a text field taking any CSS color (Escape reverts), with a swatch that opens the popover against it.
- **`ColorPickerSwatches`**: presets, Tailwind's palette by default; `ColorPickerSwatch` for one.
- **Extras**: `ColorPickerValue`, `ColorPickerCopy`, `ColorPickerEyeDropper`, `ColorPickerPreview` and `ColorPickerFixContrast`.
- **`useColorPicker`**: reads and sets the color from custom parts.
### Value
- **Events**: `onChange` fires while dragging and `onChangeEnd` once an interaction ends.
- **`format`**: what they emit — `oklch` by default, `hex`, `rgb` or `hsl`; `alpha` adds opacity controls.
- **Forms**: `name` submits the value with a form (a hidden input), and a form reset restores the starting color.
### Contrast
Given a background (`contrastWith`), every part shows contrast: the area hatches what fails AA and draws the AA and AAA thresholds across it, tracks hatch and mark them, presets failing AA are hatched, the preview shows the WCAG ratio, and "Fix to AA/AAA" jumps to the nearest passing lightness keeping the hue.
`labels` translates every text it shows or announces.
- Install: `pnpm dlx shadcn@latest add @fma-ds/color-picker`
- Registry item: https://ds.fmartinsandre.dev.br/r/color-picker.json
- Docs: https://ds.fmartinsandre.dev.br/components/color-picker
- npm: cn@^0.5.0, culori@^4.0.2, lucide-react@^1.61.0, react-aria-components@^1.22.1
- Registry: @fma-ds/button, @fma-ds/copy-button, @fma-ds/input, @fma-ds/input-group, @fma-ds/popover, @fma-ds/select, @fma-ds/color-contrast
```tsx
import { useState } from "react";
import { ColorPicker } from "@/components/fma-ds/color-picker";
export function ColorPickerDemo() {
const [value, setValue] = useState("oklch(0.6 0.2 260)");
return (
{value}
);
}
```
### Combobox
A searchable, filterable select combining a text input with a listbox popover. Supports single selection (ComboboxValue) or a multi-select tag/chip input (ComboboxChips + ComboboxChip).
- Install: `pnpm dlx shadcn@latest add @fma-ds/combobox`
- Registry item: https://ds.fmartinsandre.dev.br/r/combobox.json
- Docs: https://ds.fmartinsandre.dev.br/components/combobox
- npm: cn@^0.5.0, lucide-react@^1.61.0, react-aria-components@^1.22.1
- Registry: @fma-ds/button, @fma-ds/input-group
```tsx
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@/components/fma-ds/combobox";
const frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"];
export function ComboboxDemo() {
return (
No items found. ({ id: item, name: item }))}>
{(item) => {item.name}}
);
}
```
### Command
A searchable list of commands or actions, filtered live as you type.
- **`CommandDialog`**: wraps it in a `Dialog` for a full command-palette (⌘K) pattern.
- **Empty state**: pass `CommandEmpty` through `CommandList`'s `renderEmptyState`, not as a child — the list only accepts items, groups and separators.
- Install: `pnpm dlx shadcn@latest add @fma-ds/command`
- Registry item: https://ds.fmartinsandre.dev.br/r/command.json
- Docs: https://ds.fmartinsandre.dev.br/components/command
- npm: cn@^0.5.0, lucide-react@^1.61.0, react-aria-components@^1.22.1
- Registry: @fma-ds/dialog, @fma-ds/input-group
```tsx
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/fma-ds/command";
export function CommandDemo() {
return (
{/* The empty state goes through renderEmptyState: a plain child in the
collection would empty the whole list. */}
No results found.}>
CalendarSearch Emoji
Calculator
ProfileBillingSettings
);
}
```
### Confirm Dialog
A ready-made yes/no dialog on top of the `use-dialog-promise` hook.
`const ask = useDialogPromise()`, render `` once, then `const result = await ask.open(data)` and check `result.status`: `confirmed`, `cancelled`, or `dismissed` by Escape.
- **Content**: `title` and `description` take the opened `data` when given as functions; `media` adds an icon.
- **Actions**: `tone="destructive"` marks a destructive action; `cancelLabel={null}` leaves a single action.
- **`onConfirm`**: runs with the dialog open and the button pending; if it throws, the dialog stays open with `errorMessage`.
For dialogs that collect a value or hold a form, render your own dialog with the same hook and call `confirm(value)`.
Labels default to English; pass translations through the label props.
- Install: `pnpm dlx shadcn@latest add @fma-ds/confirm-dialog`
- Registry item: https://ds.fmartinsandre.dev.br/r/confirm-dialog.json
- Docs: https://ds.fmartinsandre.dev.br/components/confirm-dialog
- npm: cn@^0.5.0, lucide-react@^1.61.0
- Registry: @fma-ds/alert-dialog, @fma-ds/button, @fma-ds/use-dialog-promise
```tsx
import { BanIcon } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/fma-ds/button";
import { ConfirmDialog } from "@/components/fma-ds/confirm-dialog";
import { useDialogPromise } from "@/hooks/fma-ds/use-dialog-promise";
export function ConfirmDialogDemo() {
const block = useDialogPromise<{ name: string }>();
const [status, setStatus] = React.useState("Ativo");
async function handleBlock() {
const result = await block.open({ name: "Padaria Pão Quente" });
if (result.status === "confirmed") setStatus("Bloqueado");
}
return (
Situação: {status}
}
title={(data) => `Bloquear ${data?.name}?`}
description="O cliente perde o acesso ao gestor de pedidos até ser reativado."
confirmLabel="Bloquear"
cancelLabel="Cancelar"
/>
);
}
```
### Context Menu
A menu that opens on right-click (or long-press on touch) instead of a regular trigger click. Supports nested submenus, separators, and keyboard shortcut display.
- Install: `pnpm dlx shadcn@latest add @fma-ds/context-menu`
- Registry item: https://ds.fmartinsandre.dev.br/r/context-menu.json
- Docs: https://ds.fmartinsandre.dev.br/components/context-menu
- npm: class-variance-authority@^0.7.1, cn@^0.5.0, lucide-react@^1.61.0, react-aria-components@^1.22.1
```tsx
import { Pressable } from "react-aria-components";
import { ContextMenu, ContextMenuItem, ContextMenuTrigger } from "@/components/fma-ds/context-menu";
export function ContextMenuDemo() {
return (
Right click here
ProfileBillingTeamSubscription
);
}
```
### Copy Button
An icon button that copies a value too long to type (protocol, tracking code, Pix code, token) to the clipboard.
- **Feedback**: the icon turns into a check, or a cross on failure, for a moment, and the outcome is announced in a separate live region.
- **Labels**: translatable; an optional tooltip shows them.
- **`useCopyToClipboard`**: also exported, for other triggers such as a menu item.
- Install: `pnpm dlx shadcn@latest add @fma-ds/copy-button`
- Registry item: https://ds.fmartinsandre.dev.br/r/copy-button.json
- Docs: https://ds.fmartinsandre.dev.br/components/copy-button
- npm: cn@^0.5.0, lucide-react@^1.61.0
- Registry: @fma-ds/button, @fma-ds/tooltip
```tsx
import { CopyButton } from "@/components/fma-ds/copy-button";
export function CopyButtonDemo() {
return ;
}
```
### Credit Card
A payment card drawn from its data, to show a saved card or preview one while it's typed.
- **Front**: the chip, the issuer's logo and color, the number padded with `•` and grouped by issuer (Amex 4-6-5), the holder and the expiry.
- **Back**: the stripe, the signature panel and the security code.
### Props
- **Data**: `number`, `name`, `expiry` (`MM/YY`, `MM/YYYY` or digits only) and `cvc` are plain strings. `*` or `x` in `number` hide digits of a saved card.
- **`issuer`**: `visa`, `mastercard`, `amex`… — pass it for a saved card; by default it is detected from the number.
- **`focused`**: `number`, `name`, `expiry` or `cvc` highlights the field being typed, and `cvc` turns the card over (Amex keeps its code on the front). With reduced motion it turns without the 3D spin.
- **Size**: its width sets its size (`w-72` by default) and the text scales with it.
- **`labels`**: translates the captions and the accessible name. The card is one `role="img"` ("Visa card ending in 4242, expires 12/28").
Logos for Visa, Mastercard, Elo, American Express, Hipercard, Diners Club, Discover and JCB; other issuers show their name on the theme gray.
Issuer detection and the form's validation (number, expiry, security code) live in the `credit-card-validation` lib, usable without the card. Pair the number field with the `credit-card` mask, which follows the issuer too.
- Install: `pnpm dlx shadcn@latest add @fma-ds/credit-card`
- Registry item: https://ds.fmartinsandre.dev.br/r/credit-card.json
- Docs: https://ds.fmartinsandre.dev.br/components/credit-card
- npm: cn@^0.5.0
- Registry: @fma-ds/credit-card-validation
```tsx
import { CreditCard } from "@/components/fma-ds/credit-card";
export function CreditCardDemo() {
return ;
}
```
### Data Grid
A data grid driven by a TanStack Table v9 instance and rendered with react-aria's `Table`, which brings cell keyboard navigation, `aria-sort`, selection, column resizing, row drag and drop and virtualization.
### Table
Create the table with `useTable({ features: dataGridFeatures, ... })`. The exported features cover sorting, filtering with facets, pagination, selection, row expanding (tree rows from `getSubRows`), row and column pinning, ordering, visibility and sizing, with client-side row models. For server-side data (or to show every row), set `manualSorting`/`manualFiltering`/`manualPagination`.
Column `meta` takes:
- **`headerTitle`**, **`headerClassName`** and **`cellClassName`**.
- **`skeleton`**: the cell shown in loading rows.
- **`autoSize`**: the column takes the remaining width (not measured from content).
- **`headerColSpan`**: the header spans the next visible columns on its pin side, e.g. "Quantity" over the amount and its unit. Covered headers take its accessible name and the last one keeps the resize handle.
### Parts
Wrap them in `DataGrid table={table} recordCount={…}`:
- **`DataGridTable`** and **`DataGridPagination`**.
- **`DataGridColumnHeader`**: sort indicator, plus a menu for pinning, moving and visibility when `tableLayout` enables them.
- **`DataGridColumnVisibility`**: a column visibility menu.
- **`DataGridColumnFilter`**: faceted; use `filterFn: "arrHas"`.
- **`DataGridRowSelect`**: a row checkbox, with `enableRowSelection`.
- **`DataGridRowPin`**: pins to the top, or the bottom with `position="bottom"`.
- **`DataGridRowActions`**: a row's actions as `DropdownMenuItem`s behind a menu button, named from `rowLabel`. It doesn't trigger `onRowClick`, and with no items it shows a disabled "no actions" entry.
- **`DataGridRowDragHandle`**: with `onRowsReorder`.
- **`DataGridRowExpand`**: shows `getSubRows` children, indented by depth — or, with `getRowCanExpand` and `DataGridTable`'s `renderExpandedRow`, a full-width details row.
- **`DataGridSelectionBar`**: shows while rows are selected — the count (announced, with how many are on other pages), your bulk actions as children and "Clear selection".
### Rows
- **`onRowClick`**: makes rows actionable (click or Enter).
- **`onRowHover`**: fires when the pointer enters a row, e.g. to prefetch its details.
- **`isLoading`**: shows skeleton rows (`meta.skeleton`) or a spinner.
- **Footer**: `DataGridTable`'s `footerContent` takes footer rows (`DataGridTableFootRow` with `DataGridTableFootRowCell`, `colSpan` to span columns); `tableLayout.footerBackground` tints them.
### Infinite scroll
`DataGridTable`'s `onFetchMore` fires near the end of the loaded rows (`fetchMoreScrollOffset`, in viewport heights), `isFetchingMore` shows a loading row and `hasMore={false}` stops it with `allRowsLoadedMessage`. Works with `isVirtualized`.
### Layout and styling
- **`tableLayout.columnsDraggable`**: lets users drag headers to reorder columns; keyboard users use the header menu's move actions, and pinned columns stay put.
- **`tableLayout`**: also takes `rowRounded`, `headerBorder` and `columnsResizeMode` (`"onEnd"` commits the width on release and draws a line at the pointer while dragging).
- **`tableClassNames`**: adds classes to parts of the table (`base`, `header`, `headerSticky`, `body`, `bodyRow`, `footer`, `edgeCell`).
- **Surfaces**: pinned cells and sticky headers paint `--data-grid-surface` (else `--card` inside a `Card`, else `--background`) and carry `data-pinned`/`data-last-col`.
### i18n
The `i18n` prop on `DataGrid` replaces every built-in string (`{ labels: { sortAscending, rowsPerPage, paginationInfo: ({ from, to, count }) => …, goToPage: (page) => …, … } }`, defaults in `defaultDataGridI18n`). Specific props like `emptyMessage` or the pagination labels still win.
- Install: `pnpm dlx shadcn@latest add @fma-ds/data-grid`
- Registry item: https://ds.fmartinsandre.dev.br/r/data-grid.json
- Docs: https://ds.fmartinsandre.dev.br/components/data-grid
- npm: @tanstack/react-table@^9.3.1, cn@^0.5.0, lucide-react@^1.61.0, react-aria-components@^1.22.1
- Registry: @fma-ds/badge, @fma-ds/button, @fma-ds/checkbox, @fma-ds/dropdown-menu, @fma-ds/input, @fma-ds/popover, @fma-ds/select, @fma-ds/skeleton, @fma-ds/spinner
```tsx
import {
type ColumnDef,
type ColumnFiltersState,
type ColumnOrderState,
type ColumnPinningState,
type ColumnVisibilityState,
type PaginationState,
type RowPinningState,
type RowSelectionState,
type SortingState,
type Table,
useTable,
} from "@tanstack/react-table";
import * as React from "react";
import { Badge } from "@/components/fma-ds/badge";
import {
DataGrid,
DataGridColumnHeader,
DataGridContainer,
type DataGridFeatures,
type DataGridI18nOverrides,
type DataGridLayout,
DataGridPagination,
DataGridRowExpand,
DataGridRowSelect,
DataGridTable,
type DataGridTableClassNames,
dataGridFeatures,
} from "@/components/fma-ds/data-grid";
import { Skeleton } from "@/components/fma-ds/skeleton";
type User = {
id: string;
name: string;
role: string;
status: "Active" | "Inactive" | "Pending";
joined: string;
};
const USERS: User[] = [
{
id: "USR-001",
name: "Alice Martin",
role: "Frontend Engineer",
status: "Active",
joined: "2023-01-15",
},
{
id: "USR-002",
name: "Bob Chen",
role: "Product Manager",
status: "Active",
joined: "2022-07-08",
},
{
id: "USR-003",
name: "Carol Smith",
role: "UX Designer",
status: "Inactive",
joined: "2021-11-22",
},
{
id: "USR-004",
name: "David Kim",
role: "Backend Engineer",
status: "Active",
joined: "2023-05-03",
},
{
id: "USR-005",
name: "Eva Brown",
role: "Data Analyst",
status: "Pending",
joined: "2024-02-14",
},
{
id: "USR-006",
name: "Frank Lee",
role: "DevOps Engineer",
status: "Active",
joined: "2022-03-19",
},
{
id: "USR-007",
name: "Grace Liu",
role: "QA Engineer",
status: "Inactive",
joined: "2023-09-01",
},
{
id: "USR-008",
name: "Henry Park",
role: "Engineering Manager",
status: "Active",
joined: "2020-06-30",
},
];
type UserColumn = ColumnDef;
const header =
(title: string): UserColumn["header"] =>
({ column }) => ;
const COLUMNS: UserColumn[] = [
{ accessorKey: "id", header: header("ID"), size: 110, meta: { headerTitle: "ID" } },
{
accessorKey: "name",
header: header("Name"),
size: 180,
meta: { headerTitle: "Name", autoSize: true },
},
{ accessorKey: "role", header: header("Role"), size: 190, meta: { headerTitle: "Role" } },
{
accessorKey: "status",
header: header("Status"),
size: 120,
filterFn: "arrHas",
cell: ({ row }) => (
{row.original.status}
),
meta: { headerTitle: "Status", skeleton: },
},
{ accessorKey: "joined", header: header("Joined"), size: 130, meta: { headerTitle: "Joined" } },
];
const SELECT_COLUMN: UserColumn = {
id: "select",
size: 48,
enableSorting: false,
enableResizing: false,
enableHiding: false,
header: () => ,
cell: () => ,
meta: { headerTitle: "Select" },
};
type UsersDataGridProps = {
data?: User[];
columns?: UserColumn[];
isLoading?: boolean;
loadingMode?: "skeleton" | "spinner";
emptyMessage?: React.ReactNode;
onRowClick?: (user: User) => void;
layout?: DataGridLayout;
selectable?: boolean;
pageSize?: number;
toolbar?: (table: Table) => React.ReactNode;
/** Adds an expand column and a details row under expanded rows. */
expandable?: boolean;
initialPinning?: ColumnPinningState;
i18n?: DataGridI18nOverrides;
tableClassNames?: DataGridTableClassNames;
/** Footer rows for `footerContent`. */
footer?: (table: Table) => React.ReactNode;
};
const EXPAND_COLUMN: UserColumn = {
id: "expand",
size: 48,
enableSorting: false,
enableResizing: false,
enableHiding: false,
header: () => Details,
cell: ({ row }) => ,
meta: { headerTitle: "Details" },
};
function UsersDataGrid({
data = USERS,
columns = COLUMNS,
isLoading,
loadingMode,
emptyMessage,
onRowClick,
layout,
selectable,
pageSize = 5,
toolbar,
expandable,
initialPinning = { start: [], end: [] },
i18n,
footer,
tableClassNames,
}: UsersDataGridProps) {
const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize });
const [sorting, setSorting] = React.useState([]);
const [rowSelection, setRowSelection] = React.useState({});
const [columnFilters, setColumnFilters] = React.useState([]);
const [columnVisibility, setColumnVisibility] = React.useState({});
const [columnOrder, setColumnOrder] = React.useState([]);
const [columnPinning, setColumnPinning] = React.useState(initialPinning);
const [rowPinning, setRowPinning] = React.useState({ top: [], bottom: [] });
const table = useTable({
features: dataGridFeatures,
data,
columns: [
...(expandable ? [EXPAND_COLUMN] : []),
...(selectable ? [SELECT_COLUMN] : []),
...columns,
],
getRowCanExpand: () => !!expandable,
getRowId: (row: User) => row.id,
state: {
pagination,
sorting,
rowSelection,
columnFilters,
columnVisibility,
columnOrder,
columnPinning,
rowPinning,
},
onPaginationChange: setPagination,
onSortingChange: setSorting,
onRowSelectionChange: setRowSelection,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onColumnOrderChange: setColumnOrder,
onColumnPinningChange: setColumnPinning,
onRowPinningChange: setRowPinning,
enableRowSelection: selectable,
keepPinnedRows: true,
});
return (
{toolbar?.(table)}
aria-label="Users"
footerContent={footer?.(table)}
renderExpandedRow={
expandable
? (row) => (