diff --git a/examples/shadcn-ui.dspack.json b/examples/shadcn-ui.dspack.json
index 838263e..9d2327d 100644
--- a/examples/shadcn-ui.dspack.json
+++ b/examples/shadcn-ui.dspack.json
@@ -3,7 +3,7 @@
"dspack": "0.4",
"name": "shadcn/ui",
"description": "A collection of reusable components built with Radix UI and Tailwind CSS. Components are copied into your project, not installed as a dependency.",
- "version": "3.0.0",
+ "version": "3.1.0",
"metadata": {
"source": "https://ui.shadcn.com",
"license": "MIT"
@@ -380,10 +380,10 @@
},
"alert-dialog": {
"name": "Alert Dialog",
- "description": "A modal dialog that interrupts the user with important content and expects a response. Built on Radix UI AlertDialog. Renders with a required action and a cancel option. The user cannot dismiss it by clicking the overlay or pressing Escape — they must choose an explicit action.",
+ "description": "A modal dialog that interrupts the user with important content and expects a response. Built on Radix UI AlertDialog. Renders with a required action and a cancel option. The user cannot dismiss it by clicking the overlay or pressing Escape \u2014 they must choose an explicit action.",
"status": "stable",
"whenToUse": "Use for confirming destructive or irreversible actions: deleting records, discarding unsaved changes, revoking access. Use when the consequence of proceeding is significant enough that the user should explicitly acknowledge it.",
- "whenNotToUse": "Do not use for informational messages or non-destructive confirmations. Use Dialog for those. Do not use when the user should be able to dismiss without choosing — that is what Dialog provides.",
+ "whenNotToUse": "Do not use for informational messages or non-destructive confirmations. Use Dialog for those. Do not use when the user should be able to dismiss without choosing \u2014 that is what Dialog provides.",
"props": {
"open": {
"type": "boolean",
@@ -547,7 +547,7 @@
"description": "A modal window that appears over the page content. Built on Radix UI Dialog. Can be dismissed by clicking the overlay, pressing Escape, or activating a close button.",
"status": "stable",
"whenToUse": "Use for forms, settings panels, detail views, or any content that benefits from focused attention without leaving the current page. Appropriate when the user should be able to dismiss the content without taking an action.",
- "whenNotToUse": "Do not use for confirming destructive actions. Use AlertDialog instead — it prevents accidental dismissal. Do not use for simple, brief messages; use a toast or inline alert.",
+ "whenNotToUse": "Do not use for confirming destructive actions. Use AlertDialog instead \u2014 it prevents accidental dismissal. Do not use for simple, brief messages; use a toast or inline alert.",
"props": {
"open": {
"type": "boolean",
@@ -684,7 +684,7 @@
"description": "A container for grouping related content and actions. Provides visual separation through a bordered surface.",
"status": "stable",
"whenToUse": "Use to group related information into a distinct visual unit. Appropriate for dashboard widgets, summary panels, list items with detail, or any content that benefits from visual containment.",
- "whenNotToUse": "Do not nest Cards inside other Cards. If content hierarchy is needed, use headings and spacing within a single Card. Do not use as a clickable element — wrap content in an interactive element instead.",
+ "whenNotToUse": "Do not nest Cards inside other Cards. If content hierarchy is needed, use headings and spacing within a single Card. Do not use as a clickable element \u2014 wrap content in an interactive element instead.",
"props": {
"className": {
"type": "string",
@@ -879,7 +879,7 @@
"description": "A menu that appears on activation of a trigger element. Built on Radix UI DropdownMenu. Supports items, checkboxes, radio groups, sub-menus, separators, and keyboard navigation.",
"status": "stable",
"whenToUse": "Use when a set of actions or options needs to be accessible from a single trigger without taking up permanent screen space. Appropriate for overflow menus, context-sensitive actions, and settings with multiple choices.",
- "whenNotToUse": "Do not use for navigation — use a navigation menu or links. Do not use when the options should always be visible; use RadioGroup or a set of Buttons instead.",
+ "whenNotToUse": "Do not use for navigation \u2014 use a navigation menu or links. Do not use when the options should always be visible; use RadioGroup or a set of Buttons instead.",
"props": {
"open": {
"type": "boolean",
@@ -1044,10 +1044,10 @@
},
"table": {
"name": "Table",
- "description": "A set of primitives for presenting tabular data with a meaningful row-and-column relationship. These are thin, presentational wrappers over the native table elements — Table renders a
inside a horizontally scrollable container, and the sub-components render thead, tbody, tfoot, tr, th, td, and caption. There is no built-in sorting, filtering, pagination, or selection; those are composed on top.",
+ "description": "A set of primitives for presenting tabular data with a meaningful row-and-column relationship. These are thin, presentational wrappers over the native table elements \u2014 Table renders a inside a horizontally scrollable container, and the sub-components render thead, tbody, tfoot, tr, th, td, and caption. There is no built-in sorting, filtering, pagination, or selection; those are composed on top.",
"status": "experimental",
"whenToUse": "Use to display data where each row is a record and each column is a comparable attribute: members and their roles, invoices, audit logs, permission lists. Reach for Table whenever the relationship between rows and columns carries meaning that assistive technology should be able to announce.",
- "whenNotToUse": "Do not use Table to lay out or align non-tabular content — use CSS grid or flexbox for layout. Do not use a table to present a single record's key/value pairs; use a description list (dl) or a Card instead. For large interactive datasets, still use these primitives for markup but drive state with a headless table library rather than building a bespoke component.",
+ "whenNotToUse": "Do not use Table to lay out or align non-tabular content \u2014 use CSS grid or flexbox for layout. Do not use a table to present a single record's key/value pairs; use a description list (dl) or a Card instead. For large interactive datasets, still use these primitives for markup but drive state with a headless table library rather than building a bespoke component.",
"accessibility": {
"role": "table",
"requiredAttributes": [
@@ -1073,7 +1073,7 @@
}
],
"labelRequirement": "required-accessible-name",
- "notes": "Preserve the native semantic structure: real thead, tbody, tfoot, th, and td elements — never reconstruct a table from styled divs, which strips the row/column semantics screen readers depend on. Do NOT use Table for visual layout, alignment, or positioning; that is the job of CSS grid or flexbox. Provide an accessible name through TableCaption (preferred) or aria-label/aria-labelledby. A sortable column header must be a real Button inside the th, with the th carrying aria-sort, so the control is keyboard-operable and announced as a button."
+ "notes": "Preserve the native semantic structure: real thead, tbody, tfoot, th, and td elements \u2014 never reconstruct a table from styled divs, which strips the row/column semantics screen readers depend on. Do NOT use Table for visual layout, alignment, or positioning; that is the job of CSS grid or flexbox. Provide an accessible name through TableCaption (preferred) or aria-label/aria-labelledby. A sortable column header must be a real Button inside the th, with the th carrying aria-sort, so the control is keyboard-operable and announced as a button."
},
"composition": {
"subComponents": [
@@ -1125,7 +1125,7 @@
"acceptsChildren": "text"
}
],
- "notes": "TableRow must appear inside TableHeader, TableBody, or TableFooter — not directly under Table. Header cells use TableHead (th); data cells use TableCell (td). When present, TableCaption is the first child of Table. These primitives carry no data behavior; compose sorting, filtering, pagination, and selection on top of them."
+ "notes": "TableRow must appear inside TableHeader, TableBody, or TableFooter \u2014 not directly under Table. Header cells use TableHead (th); data cells use TableCell (td). When present, TableCaption is the first child of Table. These primitives carry no data behavior; compose sorting, filtering, pagination, and selection on top of them."
},
"constraints": [
{
@@ -1155,7 +1155,7 @@
},
{
"context": "Any data table",
- "rule": "Give the table an accessible name describing its contents — a TableCaption (preferred), or aria-label/aria-labelledby",
+ "rule": "Give the table an accessible name describing its contents \u2014 a TableCaption (preferred), or aria-label/aria-labelledby",
"severity": "must"
}
],
@@ -1183,7 +1183,7 @@
"description": "A non-modal floating panel anchored to a trigger, rendered in a portal. Built on Radix UI Popover. The content element carries role=\"dialog\" and the trigger carries aria-haspopup=\"dialog\", aria-expanded, and aria-controls. Unlike Dialog, it is non-modal by default: focus is not trapped, the page behind stays scrollable and interactive, and the rest of the page is not hidden from assistive technology.",
"status": "stable",
"whenToUse": "Use for rich, interactive content that belongs to a specific control and should appear next to it: a date picker, a column-visibility menu, a filter builder, a small edit form, a combobox surface. Reach for Popover when the content is transient, contains focusable elements, and the user should be able to keep working with the page around it.",
- "whenNotToUse": "Do not use for plain descriptive text on hover — use Tooltip, which is announced via aria-describedby and costs no interaction. Do not use for content that must be dealt with before the user continues; use Dialog, which is modal and traps focus. Do not use for a list of commands or actions; use DropdownMenu, which implements the menu pattern with roving focus and typeahead. Do not use for large or edge-anchored panels such as navigation or detail drawers; use Sheet.",
+ "whenNotToUse": "Do not use for plain descriptive text on hover \u2014 use Tooltip, which is announced via aria-describedby and costs no interaction. Do not use for content that must be dealt with before the user continues; use Dialog, which is modal and traps focus. Do not use for a list of commands or actions; use DropdownMenu, which implements the menu pattern with roving focus and typeahead. Do not use for large or edge-anchored panels such as navigation or detail drawers; use Sheet.",
"props": {
"open": {
"type": "boolean",
@@ -1229,7 +1229,7 @@
}
],
"default": "bottom",
- "description": "On PopoverContent. Preferred edge of the anchor to open against. This is a preference, not a guarantee — with avoidCollisions left on, the popover flips to the opposite side when the preferred side would overflow the viewport."
+ "description": "On PopoverContent. Preferred edge of the anchor to open against. This is a preference, not a guarantee \u2014 with avoidCollisions left on, the popover flips to the opposite side when the preferred side would overflow the viewport."
},
"align": {
"type": "enum",
@@ -1325,7 +1325,7 @@
],
"focusManagement": "On open, focus moves into the content. On close, focus returns to PopoverTrigger. Focus is trapped only when modal is true; in the default non-modal mode Tab walks out of the panel into the rest of the page, which is why the panel must not hold a task that has to be finished before the user moves on.",
"labelRequirement": "required-accessible-name",
- "notes": "PopoverHeader, PopoverTitle, and PopoverDescription are plain presentational elements in shadcn's implementation — a div, a div, and a p. They are not Radix parts and carry no ARIA wiring, unlike DialogTitle and DialogDescription. Rendering a PopoverTitle therefore does not give the panel an accessible name; the aria-labelledby link must be authored by hand. Because the default mode is non-modal, screen reader users can navigate away from an open popover with no warning; do not rely on the panel holding their attention."
+ "notes": "PopoverHeader, PopoverTitle, and PopoverDescription are plain presentational elements in shadcn's implementation \u2014 a div, a div, and a p. They are not Radix parts and carry no ARIA wiring, unlike DialogTitle and DialogDescription. Rendering a PopoverTitle therefore does not give the panel an accessible name; the aria-labelledby link must be authored by hand. Because the default mode is non-modal, screen reader users can navigate away from an open popover with no warning; do not rely on the panel holding their attention."
},
"composition": {
"subComponents": [
@@ -1351,7 +1351,7 @@
{
"id": "popover-anchor",
"name": "PopoverAnchor",
- "description": "An optional alternative positioning reference. When present, PopoverContent positions against the anchor instead of the trigger. Use it when the visual origin of the panel differs from the control that opens it — anchoring a combobox list to the whole input while a chevron button triggers it, for example. Must be a descendant of the same Popover root.",
+ "description": "An optional alternative positioning reference. When present, PopoverContent positions against the anchor instead of the trigger. Use it when the visual origin of the panel differs from the control that opens it \u2014 anchoring a combobox list to the whole input while a chevron button triggers it, for example. Must be a descendant of the same Popover root.",
"slot": "anchor",
"acceptsChildren": "any"
},
@@ -1365,13 +1365,13 @@
{
"id": "popover-title",
"name": "PopoverTitle",
- "description": "The panel's visible title. A presentational element, not a Radix part — give it an id and reference it from PopoverContent's aria-labelledby to make it the accessible name.",
+ "description": "The panel's visible title. A presentational element, not a Radix part \u2014 give it an id and reference it from PopoverContent's aria-labelledby to make it the accessible name.",
"acceptsChildren": "text"
},
{
"id": "popover-description",
"name": "PopoverDescription",
- "description": "Supporting copy beneath the title. A presentational p element — reference its id from PopoverContent's aria-describedby to have it announced.",
+ "description": "Supporting copy beneath the title. A presentational p element \u2014 reference its id from PopoverContent's aria-describedby to have it announced.",
"acceptsChildren": "text"
}
],
@@ -1379,7 +1379,7 @@
"popover-trigger",
"popover-content"
],
- "notes": "Positioning props live on PopoverContent, never on the Popover root; open state props live on the root, never on the content. PopoverContent portals itself, so no explicit Portal sub-component is exported or needed. PopoverAnchor, when used, replaces the trigger as the positioning reference but not as the control — the trigger still opens the panel. PopoverHeader, PopoverTitle, and PopoverDescription were added to the shadcn registry after the original three-part API; older installs may only export Popover, PopoverTrigger, PopoverContent, and PopoverAnchor."
+ "notes": "Positioning props live on PopoverContent, never on the Popover root; open state props live on the root, never on the content. PopoverContent portals itself, so no explicit Portal sub-component is exported or needed. PopoverAnchor, when used, replaces the trigger as the positioning reference but not as the control \u2014 the trigger still opens the panel. PopoverHeader, PopoverTitle, and PopoverDescription were added to the shadcn registry after the original three-part API; older installs may only export Popover, PopoverTrigger, PopoverContent, and PopoverAnchor."
},
"constraints": [
{
@@ -1389,7 +1389,7 @@
},
{
"context": "Any PopoverContent",
- "rule": "Give the panel an accessible name by wiring aria-labelledby to the PopoverTitle's id, or by setting aria-label — Radix Popover does not wire this automatically",
+ "rule": "Give the panel an accessible name by wiring aria-labelledby to the PopoverTitle's id, or by setting aria-label \u2014 Radix Popover does not wire this automatically",
"severity": "must"
},
{
@@ -1399,7 +1399,7 @@
},
{
"context": "Content the user must act on before continuing, or that would leave the app in an inconsistent state if abandoned",
- "rule": "Use Dialog instead of Popover, or set modal on Popover — the default non-modal popover lets focus and pointer input leave freely",
+ "rule": "Use Dialog instead of Popover, or set modal on Popover \u2014 the default non-modal popover lets focus and pointer input leave freely",
"severity": "must-not"
},
{
@@ -1451,14 +1451,14 @@
"categories": [
"overlay"
],
- "notes": "Overlap to disambiguate later: Popover, Dialog, Sheet, DropdownMenu, and Tooltip all render portalled content above the page, and nothing in the type system stops an author from using any of them for the same job. The distinctions that matter are modality (Popover is non-modal by default, Dialog and Sheet are modal), anchoring (Popover and Tooltip position against a trigger; Dialog and Sheet position against the viewport), and interactivity (Tooltip content is unreachable, Popover content is fully interactive). A governance rule should later select on those axes rather than on component identity — for example, flagging interactive controls inside Tooltip content, and flagging a Popover carrying a form with a submit action that ought to be a Dialog. Token note: this entry references popover and popover-foreground, which are real shadcn theme variables (--popover, --popover-foreground) but are not yet present in this pack's token registry; they should be registered rather than substituted with background and foreground, which would misdescribe the component."
+ "notes": "Overlap to disambiguate later: Popover, Dialog, Sheet, DropdownMenu, and Tooltip all render portalled content above the page, and nothing in the type system stops an author from using any of them for the same job. The distinctions that matter are modality (Popover is non-modal by default, Dialog and Sheet are modal), anchoring (Popover and Tooltip position against a trigger; Dialog and Sheet position against the viewport), and interactivity (Tooltip content is unreachable, Popover content is fully interactive). A governance rule should later select on those axes rather than on component identity \u2014 for example, flagging interactive controls inside Tooltip content, and flagging a Popover carrying a form with a submit action that ought to be a Dialog. Token note: this entry references popover and popover-foreground, which are real shadcn theme variables (--popover, --popover-foreground) but are not yet present in this pack's token registry; they should be registered rather than substituted with background and foreground, which would misdescribe the component."
},
"tooltip": {
"name": "Tooltip",
"description": "A small text label that appears on hover or keyboard focus to describe the control it is attached to. Built on Radix UI Tooltip. The content carries role=\"tooltip\" and is linked to the trigger with aria-describedby while open. It is supplementary by definition: it does not receive focus, it closes on click and on pointer-down, and it never opens from touch input.",
"status": "stable",
- "whenToUse": "Use to name or clarify a control whose purpose is not fully carried by its visible presentation — an icon-only button, a truncated cell, an abbreviated column header, a disabled control whose reason for being disabled needs explaining. Keep the content to a short phrase.",
- "whenNotToUse": "Do not use for anything the user actually needs. Tooltip content is unreachable on touch devices and disappears on click, so information that is required to complete a task must live in the page, in a Popover, or in helper text. Do not put links, buttons, form fields, or any focusable element inside a tooltip — they cannot be reached. Do not use a tooltip as a control's only accessible name if the control is not the trigger itself.",
+ "whenToUse": "Use to name or clarify a control whose purpose is not fully carried by its visible presentation \u2014 an icon-only button, a truncated cell, an abbreviated column header, a disabled control whose reason for being disabled needs explaining. Keep the content to a short phrase.",
+ "whenNotToUse": "Do not use for anything the user actually needs. Tooltip content is unreachable on touch devices and disappears on click, so information that is required to complete a task must live in the page, in a Popover, or in helper text. Do not put links, buttons, form fields, or any focusable element inside a tooltip \u2014 they cannot be reached. Do not use a tooltip as a control's only accessible name if the control is not the trigger itself.",
"props": {
"delayDuration": {
"type": "number",
@@ -1481,7 +1481,7 @@
"open": {
"type": "boolean",
"propRole": "state",
- "description": "On Tooltip (root). Controlled open state. Use sparingly — forcing a tooltip open bypasses the hover and focus semantics screen reader and touch users depend on."
+ "description": "On Tooltip (root). Controlled open state. Use sparingly \u2014 forcing a tooltip open bypasses the hover and focus semantics screen reader and touch users depend on."
},
"defaultOpen": {
"type": "boolean",
@@ -1559,7 +1559,7 @@
"aria-label": {
"type": "string",
"propRole": "content",
- "description": "On TooltipContent. Replaces the visible content in the accessibility tree with this string, rendered into a visually hidden element that carries role=\"tooltip\". Use only when the visible content does not read well when announced — for example a formatted value where the spoken form should differ. Setting it removes role=\"tooltip\" from the visible element."
+ "description": "On TooltipContent. Replaces the visible content in the accessibility tree with this string, rendered into a visually hidden element that carries role=\"tooltip\". Use only when the visible content does not read well when announced \u2014 for example a formatted value where the spoken form should differ. Setting it removes role=\"tooltip\" from the visible element."
},
"asChild": {
"type": "boolean",
@@ -1574,7 +1574,7 @@
{
"attribute": "aria-describedby",
"condition": "wired automatically by Radix while the tooltip is open",
- "description": "Radix sets aria-describedby on the trigger, pointing at the content id. This makes the tooltip a description, not a name — an icon-only button still needs its own accessible name from aria-label or visually hidden text."
+ "description": "Radix sets aria-describedby on the trigger, pointing at the content id. This makes the tooltip a description, not a name \u2014 an icon-only button still needs its own accessible name from aria-label or visually hidden text."
},
{
"attribute": "aria-label",
@@ -1600,16 +1600,16 @@
"description": "Closes the tooltip, because activating the trigger dismisses it."
}
],
- "focusManagement": "The tooltip never takes focus — focus stays on the trigger the whole time it is open, and the content is not in the tab order. This is exactly why interactive content inside a tooltip is unreachable. The trigger must itself be focusable: a natively disabled button fires no focus or pointer events, so wrap it in a focusable element or mark it aria-disabled with the control still focusable if the tooltip needs to explain why it is unavailable.",
+ "focusManagement": "The tooltip never takes focus \u2014 focus stays on the trigger the whole time it is open, and the content is not in the tab order. This is exactly why interactive content inside a tooltip is unreachable. The trigger must itself be focusable: a natively disabled button fires no focus or pointer events, so wrap it in a focusable element or mark it aria-disabled with the control still focusable if the tooltip needs to explain why it is unavailable.",
"labelRequirement": "required-accessible-name",
- "notes": "Radix explicitly ignores pointer events whose pointerType is \"touch\", so a tooltip never opens from a tap or a touch hover — on phones and tablets the content simply does not exist. Pointer-down and click also close it. Treat every tooltip as decoration over information already available elsewhere. Because the content is linked with aria-describedby, it is announced after the control's name and role; a control whose name comes only from its tooltip will be announced as unnamed to some users and as nothing at all on touch."
+ "notes": "Radix explicitly ignores pointer events whose pointerType is \"touch\", so a tooltip never opens from a tap or a touch hover \u2014 on phones and tablets the content simply does not exist. Pointer-down and click also close it. Treat every tooltip as decoration over information already available elsewhere. Because the content is linked with aria-describedby, it is announced after the control's name and role; a control whose name comes only from its tooltip will be announced as unnamed to some users and as nothing at all on touch."
},
"composition": {
"subComponents": [
{
"id": "tooltip-provider",
"name": "TooltipProvider",
- "description": "Supplies shared timing context — delayDuration, skipDelayDuration, disableHoverableContent — to every Tooltip beneath it. Mount it once near the application root. It is not optional: a Tooltip rendered outside a provider throws, because Radix creates the provider context with no default value.",
+ "description": "Supplies shared timing context \u2014 delayDuration, skipDelayDuration, disableHoverableContent \u2014 to every Tooltip beneath it. Mount it once near the application root. It is not optional: a Tooltip rendered outside a provider throws, because Radix creates the provider context with no default value.",
"required": true,
"slot": "provider",
"acceptsChildren": "any"
@@ -1628,7 +1628,7 @@
{
"id": "tooltip-content",
"name": "TooltipContent",
- "description": "The floating label, portalled to the document body and carrying role=\"tooltip\". shadcn's implementation renders a built-in arrow inside it, so no separate Arrow sub-component is exported. Accepts short text only — it is not reachable by keyboard or touch.",
+ "description": "The floating label, portalled to the document body and carrying role=\"tooltip\". shadcn's implementation renders a built-in arrow inside it, so no separate Arrow sub-component is exported. Accepts short text only \u2014 it is not reachable by keyboard or touch.",
"required": true,
"slot": "content",
"acceptsChildren": "text"
@@ -1649,17 +1649,17 @@
},
{
"context": "Any application rendering a Tooltip",
- "rule": "Mount a single TooltipProvider at the application root — Tooltip throws when rendered outside one",
+ "rule": "Mount a single TooltipProvider at the application root \u2014 Tooltip throws when rendered outside one",
"severity": "must"
},
{
"context": "Links, buttons, form fields, or any focusable element inside tooltip content",
- "rule": "Move the interactive content into a Popover — tooltip content never receives focus and closes on click, so those controls are unreachable",
+ "rule": "Move the interactive content into a Popover \u2014 tooltip content never receives focus and closes on click, so those controls are unreachable",
"severity": "must-not"
},
{
"context": "Information a user needs in order to complete a task",
- "rule": "Do not put it only in a Tooltip — it is entirely absent on touch devices; put it in the page, in helper text, or in a Popover",
+ "rule": "Do not put it only in a Tooltip \u2014 it is entirely absent on touch devices; put it in the page, in helper text, or in a Popover",
"severity": "must-not"
},
{
@@ -1669,7 +1669,7 @@
},
{
"context": "A tooltip on a disabled control",
- "rule": "Keep the trigger focusable — use aria-disabled with the control still in the tab order, or wrap it, because a natively disabled button emits no pointer or focus events and the tooltip will never open",
+ "rule": "Keep the trigger focusable \u2014 use aria-disabled with the control still in the tab order, or wrap it, because a natively disabled button emits no pointer or focus events and the tooltip will never open",
"severity": "must"
},
{
@@ -1704,14 +1704,14 @@
"categories": [
"overlay"
],
- "notes": "Overlap to disambiguate later: Tooltip and Popover are both trigger-anchored portalled overlays and are frequently interchanged by mistake. The dividing line is reachability, not size — tooltip content cannot be focused, is dismissed by click, and is never shown to touch users, so anything load-bearing or interactive belongs in a Popover. A governance rule should later assert that TooltipContent's subtree contains no interactive-category component and no focusable element, and should flag a Tooltip whose content is the only place a piece of information appears. Token note: shadcn's TooltipContent inverts the surface, using bg-foreground with text-background rather than a dedicated tooltip token pair, so the tokens listed here are deliberately the base semantic colors and not a popover-style pair."
+ "notes": "Overlap to disambiguate later: Tooltip and Popover are both trigger-anchored portalled overlays and are frequently interchanged by mistake. The dividing line is reachability, not size \u2014 tooltip content cannot be focused, is dismissed by click, and is never shown to touch users, so anything load-bearing or interactive belongs in a Popover. A governance rule should later assert that TooltipContent's subtree contains no interactive-category component and no focusable element, and should flag a Tooltip whose content is the only place a piece of information appears. Token note: shadcn's TooltipContent inverts the surface, using bg-foreground with text-background rather than a dedicated tooltip token pair, so the tokens listed here are deliberately the base semantic colors and not a popover-style pair."
},
"sheet": {
"name": "Sheet",
- "description": "A modal panel that slides in from an edge of the viewport. Built on the same Radix UI Dialog primitive as Dialog — same modality, same focus trap, same dismissal behavior — differing only in that its content is pinned to a viewport edge rather than centered, and sized to that edge. Renders an overlay and, by default, a close button in the corner of the panel.",
+ "description": "A modal panel that slides in from an edge of the viewport. Built on the same Radix UI Dialog primitive as Dialog \u2014 same modality, same focus trap, same dismissal behavior \u2014 differing only in that its content is pinned to a viewport edge rather than centered, and sized to that edge. Renders an overlay and, by default, a close button in the corner of the panel.",
"status": "stable",
"whenToUse": "Use for a focused task or detail view that needs more room than a Popover and benefits from staying visually connected to the edge it came from: mobile navigation, a filters panel, a record's detail or edit view, a settings drawer. The left and right sides suit navigation and long forms; the bottom side suits mobile action panels.",
- "whenNotToUse": "Do not use for confirming a destructive action — use AlertDialog, which cannot be dismissed by accident. Do not use for a short message or a small form that fits in a centered window; Dialog is the more conventional choice and demands less travel from the user's eye. Do not use for content anchored to a specific control, such as a picker or a small menu; use Popover or DropdownMenu. Do not use for brief status feedback; use a toast.",
+ "whenNotToUse": "Do not use for confirming a destructive action \u2014 use AlertDialog, which cannot be dismissed by accident. Do not use for a short message or a small form that fits in a centered window; Dialog is the more conventional choice and demands less travel from the user's eye. Do not use for content anchored to a specific control, such as a picker or a small menu; use Popover or DropdownMenu. Do not use for brief status feedback; use a toast.",
"props": {
"open": {
"type": "boolean",
@@ -1733,7 +1733,7 @@
"type": "boolean",
"propRole": "flag",
"default": true,
- "description": "On Sheet (root). When true (the default) focus is trapped in the panel, the rest of the page is hidden from assistive technology, and outside pointer events are blocked. Setting it false breaks the expectation the overlay sets — the page still looks dimmed and unavailable while remaining interactive."
+ "description": "On Sheet (root). When true (the default) focus is trapped in the panel, the rest of the page is hidden from assistive technology, and outside pointer events are blocked. Setting it false breaks the expectation the overlay sets \u2014 the page still looks dimmed and unavailable while remaining interactive."
},
"side": {
"type": "enum",
@@ -1749,7 +1749,7 @@
},
{
"value": "top",
- "description": "Slide down from the top edge, height sized to content. Use sparingly — it collides with system and browser chrome and with sticky headers."
+ "description": "Slide down from the top edge, height sized to content. Use sparingly \u2014 it collides with system and browser chrome and with sticky headers."
},
{
"value": "bottom",
@@ -1763,12 +1763,12 @@
"type": "boolean",
"propRole": "flag",
"default": true,
- "description": "On SheetContent. Renders the built-in close button in the panel's top-right corner, an icon button with visually hidden \"Close\" text. Set false only when supplying an explicit SheetClose elsewhere in the panel — never to remove the visible dismissal affordance entirely."
+ "description": "On SheetContent. Renders the built-in close button in the panel's top-right corner, an icon button with visually hidden \"Close\" text. Set false only when supplying an explicit SheetClose elsewhere in the panel \u2014 never to remove the visible dismissal affordance entirely."
},
"onEscapeKeyDown": {
"type": "function",
"propRole": "handler",
- "description": "On SheetContent. Fires when Escape is pressed. Call preventDefault to keep the sheet open — do this only to confirm discarding unsaved work, never to make the panel undismissable."
+ "description": "On SheetContent. Fires when Escape is pressed. Call preventDefault to keep the sheet open \u2014 do this only to confirm discarding unsaved work, never to make the panel undismissable."
},
"onInteractOutside": {
"type": "function",
@@ -1818,9 +1818,9 @@
"description": "Closes the sheet and returns focus to SheetTrigger."
}
],
- "focusManagement": "On open, focus moves into the panel — to the first focusable element, which is usually the built-in close button. On close, focus returns to SheetTrigger. Focus is trapped inside the panel and the rest of the page is marked aria-hidden while open, so a screen reader user cannot wander out of it. When the sheet is opened programmatically rather than by a trigger, set focus deliberately on close, because there is no trigger to return to.",
+ "focusManagement": "On open, focus moves into the panel \u2014 to the first focusable element, which is usually the built-in close button. On close, focus returns to SheetTrigger. Focus is trapped inside the panel and the rest of the page is marked aria-hidden while open, so a screen reader user cannot wander out of it. When the sheet is opened programmatically rather than by a trigger, set focus deliberately on close, because there is no trigger to return to.",
"labelRequirement": "required-visible",
- "notes": "Because a Sheet is a Radix Dialog, all of Dialog's accessibility obligations apply unchanged. The panel is a scroll container in its own right; long content must scroll inside the panel rather than the page, and a footer with the primary action should stay reachable without scrolling past it. On small viewports a left or right sheet occupies three quarters of the width, so it is effectively full screen — check that the close affordance is inside the safe area."
+ "notes": "Because a Sheet is a Radix Dialog, all of Dialog's accessibility obligations apply unchanged. The panel is a scroll container in its own right; long content must scroll inside the panel rather than the page, and a footer with the primary action should stay reachable without scrolling past it. On small viewports a left or right sheet occupies three quarters of the width, so it is effectively full screen \u2014 check that the close affordance is inside the safe area."
},
"composition": {
"subComponents": [
@@ -1852,7 +1852,7 @@
{
"id": "sheet-title",
"name": "SheetTitle",
- "description": "The panel's title. This is Radix's Dialog.Title, so rendering it gives the sheet its accessible name via aria-labelledby. Required for an accessible sheet — hide it visually rather than omitting it.",
+ "description": "The panel's title. This is Radix's Dialog.Title, so rendering it gives the sheet its accessible name via aria-labelledby. Required for an accessible sheet \u2014 hide it visually rather than omitting it.",
"required": true,
"acceptsChildren": "text"
},
@@ -1967,14 +1967,14 @@
"categories": [
"overlay"
],
- "notes": "Overlap to disambiguate later: Sheet is the Radix Dialog primitive with edge-anchored positioning — the sub-component sets are one-to-one (Trigger, Content, Header, Title, Description, Footer, Close) and the accessibility contract is identical, so nothing structural distinguishes a Sheet from a Dialog. The choice between them is presentational and contextual: viewport size, how much room the task needs, and whether the panel should read as attached to a page edge. AlertDialog is the one genuine behavioral distinction in this family, because it refuses dismissal by Escape or overlay. A governance rule should later encode: destructive confirmation must be AlertDialog, never Sheet or Dialog; a modal overlay carrying navigation or a long form should prefer Sheet; a short confirmation or small form should prefer Dialog; and an author should not nest a Sheet inside a Dialog or another Sheet, since stacked focus traps have no defined return path. Because Sheet, Dialog, and AlertDialog share prop names and sub-component shapes, that rule should key on intent and content, not on prop values."
+ "notes": "Overlap to disambiguate later: Sheet is the Radix Dialog primitive with edge-anchored positioning \u2014 the sub-component sets are one-to-one (Trigger, Content, Header, Title, Description, Footer, Close) and the accessibility contract is identical, so nothing structural distinguishes a Sheet from a Dialog. The choice between them is presentational and contextual: viewport size, how much room the task needs, and whether the panel should read as attached to a page edge. AlertDialog is the one genuine behavioral distinction in this family, because it refuses dismissal by Escape or overlay. A governance rule should later encode: destructive confirmation must be AlertDialog, never Sheet or Dialog; a modal overlay carrying navigation or a long form should prefer Sheet; a short confirmation or small form should prefer Dialog; and an author should not nest a Sheet inside a Dialog or another Sheet, since stacked focus traps have no defined return path. Because Sheet, Dialog, and AlertDialog share prop names and sub-component shapes, that rule should key on intent and content, not on prop values."
},
"field": {
"name": "Field",
- "description": "The family of layout and labeling primitives shadcn ships for composing form fields: Field wraps one control with its label, helper text, and validation message; FieldSet and FieldLegend give a set of related controls real fieldset/legend semantics; FieldGroup stacks fields and establishes the container query that the responsive orientation reads. Field renders a div with role=\"group\" and data-orientation; the rest are thin wrappers over label, legend, fieldset, and p. The family holds no form state, generates no ids, and performs no validation — the label/control/error association is written by hand (htmlFor plus id, aria-invalid, data-invalid) or driven by a form library's controller.",
+ "description": "The family of layout and labeling primitives shadcn ships for composing form fields: Field wraps one control with its label, helper text, and validation message; FieldSet and FieldLegend give a set of related controls real fieldset/legend semantics; FieldGroup stacks fields and establishes the container query that the responsive orientation reads. Field renders a div with role=\"group\" and data-orientation; the rest are thin wrappers over label, legend, fieldset, and p. The family holds no form state, generates no ids, and performs no validation \u2014 the label/control/error association is written by hand (htmlFor plus id, aria-invalid, data-invalid) or driven by a form library's controller.",
"status": "stable",
- "whenToUse": "Use for every hand-composed form field: pair FieldLabel with an Input, Textarea, Select, Checkbox, Switch, or Slider and add FieldDescription and FieldError as needed. Use FieldSet with FieldLegend when several controls answer one question (a checkbox group, a radio group, an address block), FieldGroup to stack related fields with consistent spacing and to enable responsive orientation, and orientation=\"horizontal\" for controls that read better beside their label such as switches and checkboxes. This is also the layer shadcn's current form guides build on — drive it with react-hook-form's Controller, TanStack Form, or Formisch.",
- "whenNotToUse": "Do not use Field as a general-purpose layout box: it renders role=\"group\", which assistive technology announces, so non-form content wrapped in it is misdescribed. Do not use FieldSet and FieldLegend around a single control — the legend announces a grouping that does not exist. Do not expect Field to wire anything: if you want ids, aria-describedby, and aria-invalid applied for you inside a react-hook-form codebase, that is the older Form family (FormItem/FormControl). Do not mix the two families within one field.",
+ "whenToUse": "Use for every hand-composed form field: pair FieldLabel with an Input, Textarea, Select, Checkbox, Switch, or Slider and add FieldDescription and FieldError as needed. Use FieldSet with FieldLegend when several controls answer one question (a checkbox group, a radio group, an address block), FieldGroup to stack related fields with consistent spacing and to enable responsive orientation, and orientation=\"horizontal\" for controls that read better beside their label such as switches and checkboxes. This is also the layer shadcn's current form guides build on \u2014 drive it with react-hook-form's Controller, TanStack Form, or Formisch.",
+ "whenNotToUse": "Do not use Field as a general-purpose layout box: it renders role=\"group\", which assistive technology announces, so non-form content wrapped in it is misdescribed. Do not use FieldSet and FieldLegend around a single control \u2014 the legend announces a grouping that does not exist. Do not expect Field to wire anything: if you want ids, aria-describedby, and aria-invalid applied for you inside a react-hook-form codebase, that is the older Form family (FormItem/FormControl). Do not mix the two families within one field.",
"props": {
"orientation": {
"type": "enum",
@@ -1999,7 +1999,7 @@
"data-invalid": {
"type": "boolean",
"propRole": "state",
- "description": "Switches the whole field block into the error treatment, including the label and description. It is styling only and is never exposed to assistive technology, so it does not replace aria-invalid on the control itself — set both. With react-hook-form this is data-invalid={fieldState.invalid}."
+ "description": "Switches the whole field block into the error treatment, including the label and description. It is styling only and is never exposed to assistive technology, so it does not replace aria-invalid on the control itself \u2014 set both. With react-hook-form this is data-invalid={fieldState.invalid}."
}
},
"accessibility": {
@@ -2030,22 +2030,22 @@
"keyboardInteractions": [
{
"key": "Tab",
- "description": "Moves to the next focusable control. Field adds no keyboard behavior of its own — the wrappers are not focus stops, so tab order is exactly the order of the controls you place inside them."
+ "description": "Moves to the next focusable control. Field adds no keyboard behavior of its own \u2014 the wrappers are not focus stops, so tab order is exactly the order of the controls you place inside them."
},
{
"key": "Shift+Tab",
"description": "Moves to the previous focusable control."
}
],
- "focusManagement": "Field never moves focus. FieldError renders with role=\"alert\", so a message that appears after a failed submit is announced without stealing focus. Moving focus to the first invalid control after a failed submit is the form library's job — react-hook-form does it by default.",
- "notes": "Clicking a FieldLabel focuses or toggles the control it names, because FieldLabel renders a real label element (Radix Label) — this only works when htmlFor matches the control's id, or when the label wraps the control. FieldSet plus FieldLegend is the only construct in the family that gives a set of controls a shared accessible name; a bare FieldGroup is a layout wrapper with no semantics. FieldTitle is styled like a label but is a plain div, so it names nothing on its own: use it only inside a choice card whose surrounding FieldLabel carries the htmlFor. Placeholder text is never a label."
+ "focusManagement": "Field never moves focus. FieldError renders with role=\"alert\", so a message that appears after a failed submit is announced without stealing focus. Moving focus to the first invalid control after a failed submit is the form library's job \u2014 react-hook-form does it by default.",
+ "notes": "Clicking a FieldLabel focuses or toggles the control it names, because FieldLabel renders a real label element (Radix Label) \u2014 this only works when htmlFor matches the control's id, or when the label wraps the control. FieldSet plus FieldLegend is the only construct in the family that gives a set of controls a shared accessible name; a bare FieldGroup is a layout wrapper with no semantics. FieldTitle is styled like a label but is a plain div, so it names nothing on its own: use it only inside a choice card whose surrounding FieldLabel carries the htmlFor. Placeholder text is never a label."
},
"composition": {
"subComponents": [
{
"id": "field-set",
"name": "FieldSet",
- "description": "A semantic fieldset that groups controls answering one question — a checkbox group, a radio group, an address block. Pair it with a FieldLegend so the group has an accessible name, and put the individual Fields in a FieldGroup inside it.",
+ "description": "A semantic fieldset that groups controls answering one question \u2014 a checkbox group, a radio group, an address block. Pair it with a FieldLegend so the group has an accessible name, and put the individual Fields in a FieldGroup inside it.",
"acceptsChildren": "components",
"categories": [
"form"
@@ -2073,7 +2073,7 @@
{
"id": "field-content",
"name": "FieldContent",
- "description": "A flex column that keeps the label, title, description, and error together when the control sits beside them — the layout you need for horizontal and responsive fields. Omit it in a plain vertical field with no description.",
+ "description": "A flex column that keeps the label, title, description, and error together when the control sits beside them \u2014 the layout you need for horizontal and responsive fields. Omit it in a plain vertical field with no description.",
"slot": "content",
"acceptsChildren": "components",
"categories": [
@@ -2093,7 +2093,7 @@
{
"id": "field-title",
"name": "FieldTitle",
- "description": "Label-styled heading text for use inside FieldContent, typically as the headline of a choice card above its FieldDescription. It renders a div, not a label, so it provides no label association — the accessible name must still come from the surrounding FieldLabel or from the control.",
+ "description": "Label-styled heading text for use inside FieldContent, typically as the headline of a choice card above its FieldDescription. It renders a div, not a label, so it provides no label association \u2014 the accessible name must still come from the surrounding FieldLabel or from the control.",
"slot": "title",
"acceptsChildren": "any",
"categories": [
@@ -2113,7 +2113,7 @@
{
"id": "field-error",
"name": "FieldError",
- "description": "The validation message container, rendered as a div with role=\"alert\" so a message that appears after submit is announced. It takes either children or an errors array of objects with a message property — the shape react-hook-form's fieldState.error and Standard Schema issues (Zod, Valibot, ArkType) already have. Given several messages it de-duplicates them and renders a bulleted list; given nothing it renders nothing.",
+ "description": "The validation message container, rendered as a div with role=\"alert\" so a message that appears after submit is announced. It takes either children or an errors array of objects with a message property \u2014 the shape react-hook-form's fieldState.error and Standard Schema issues (Zod, Valibot, ArkType) already have. Given several messages it de-duplicates them and renders a bulleted list; given nothing it renders nothing.",
"slot": "error",
"acceptsChildren": "any",
"categories": [
@@ -2123,14 +2123,14 @@
{
"id": "field-separator",
"name": "FieldSeparator",
- "description": "A divider between sections of a FieldGroup, optionally with inline text such as \"Or continue with\". Use it sparingly — every separator is a section boundary a screen reader user has to interpret.",
+ "description": "A divider between sections of a FieldGroup, optionally with inline text such as \"Or continue with\". Use it sparingly \u2014 every separator is a section boundary a screen reader user has to interpret.",
"acceptsChildren": "text",
"categories": [
"form"
]
}
],
- "notes": "The nesting the docs prescribe is FieldSet > FieldLegend + FieldDescription + FieldGroup > Field, and inside a Field: FieldLabel, the control, FieldDescription, FieldError — in that reading order, with FieldError immediately after the control it describes. When the label sits beside the control, wrap the label, description, and error in FieldContent so they stay aligned. Any labelable control belongs inside a Field: Input, Textarea, Select, Checkbox, RadioGroupItem, Switch, Slider, InputGroup, or your own. No sub-component is marked required, deliberately: in the documented choice-card and radio patterns FieldLabel is an ancestor of Field rather than a descendant, so a structural must-contain rule would be false — the labeling requirement is carried as a constraint instead. FieldGroup is required in practice only for orientation=\"responsive\", which resolves against the container query FieldGroup establishes."
+ "notes": "The nesting the docs prescribe is FieldSet > FieldLegend + FieldDescription + FieldGroup > Field, and inside a Field: FieldLabel, the control, FieldDescription, FieldError \u2014 in that reading order, with FieldError immediately after the control it describes. When the label sits beside the control, wrap the label, description, and error in FieldContent so they stay aligned. Any labelable control belongs inside a Field: Input, Textarea, Select, Checkbox, RadioGroupItem, Switch, Slider, InputGroup, or your own. No sub-component is marked required, deliberately: in the documented choice-card and radio patterns FieldLabel is an ancestor of Field rather than a descendant, so a structural must-contain rule would be false \u2014 the labeling requirement is carried as a constraint instead. FieldGroup is required in practice only for orientation=\"responsive\", which resolves against the container query FieldGroup establishes."
},
"constraints": [
{
@@ -2140,7 +2140,7 @@
},
{
"context": "The association between a FieldLabel and its control",
- "rule": "Set htmlFor on the FieldLabel to the control's id — Field generates and wires nothing",
+ "rule": "Set htmlFor on the FieldLabel to the control's id \u2014 Field generates and wires nothing",
"severity": "must"
},
{
@@ -2160,7 +2160,7 @@
},
{
"context": "A single standalone control",
- "rule": "Do not wrap it in FieldSet and FieldLegend — the legend announces a group that does not exist",
+ "rule": "Do not wrap it in FieldSet and FieldLegend \u2014 the legend announces a group that does not exist",
"severity": "should-not"
},
{
@@ -2213,10 +2213,10 @@
},
"form": {
"name": "Form",
- "description": "The react-hook-form wrapper family. Form is a re-export of react-hook-form's FormProvider, FormField wraps its Controller and publishes the field name on context, and FormItem generates one React id per field from which every other id is derived: the control gets `${id}-form-item`, the description `${id}-form-item-description`, the message `${id}-form-item-message`. FormControl is a Radix Slot that clones the single control inside it and stamps on id, aria-describedby, and aria-invalid, so the label/description/error association is generated rather than hand-written. Form itself renders no DOM — you still write the native form element and pass form.handleSubmit(onSubmit) to its onSubmit.",
+ "description": "The react-hook-form wrapper family. Form is a re-export of react-hook-form's FormProvider, FormField wraps its Controller and publishes the field name on context, and FormItem generates one React id per field from which every other id is derived: the control gets `${id}-form-item`, the description `${id}-form-item-description`, the message `${id}-form-item-message`. FormControl is a Radix Slot that clones the single control inside it and stamps on id, aria-describedby, and aria-invalid, so the label/description/error association is generated rather than hand-written. Form itself renders no DOM \u2014 you still write the native form element and pass form.handleSubmit(onSubmit) to its onSubmit.",
"status": "stable",
"whenToUse": "Use in codebases already built on react-hook-form that want the field wiring generated: unique ids per field, htmlFor bound to the control, aria-describedby pointing at the description and, once the field errors, at the message, aria-invalid reflecting the field's error state, and the error text pulled straight out of formState without touching it.",
- "whenNotToUse": "Do not use without react-hook-form: every part reads context from FormProvider and FormField, so outside them useFormField throws or produces ids like \"undefined-form-item\". Do not use it for new work in preference to Field — shadcn still ships form in the registry, but the component no longer has a documentation page and the current form guides compose Field with Controller instead, which also gives you horizontal and responsive layouts, fieldset grouping, and support for TanStack Form and Formisch. Do not reach for it when you need a field layout the family does not have: FormItem is a plain vertical stack.",
+ "whenNotToUse": "Do not use without react-hook-form: every part reads context from FormProvider and FormField, so outside them useFormField throws or produces ids like \"undefined-form-item\". Do not use it for new work in preference to Field \u2014 shadcn still ships form in the registry, but the component no longer has a documentation page and the current form guides compose Field with Controller instead, which also gives you horizontal and responsive layouts, fieldset grouping, and support for TanStack Form and Formisch. Do not reach for it when you need a field layout the family does not have: FormItem is a plain vertical stack.",
"props": {
"control": {
"type": "object",
@@ -2232,7 +2232,7 @@
"handleSubmit": {
"type": "function",
"propRole": "handler",
- "description": "The submit wrapper from useForm(), carried on context along with the rest of the hook result. It belongs on the native form element as onSubmit={form.handleSubmit(onSubmit)} — Form renders no element, so it cannot receive an onSubmit of its own."
+ "description": "The submit wrapper from useForm(), carried on context along with the rest of the hook result. It belongs on the native form element as onSubmit={form.handleSubmit(onSubmit)} \u2014 Form renders no element, so it cannot receive an onSubmit of its own."
}
},
"accessibility": {
@@ -2256,7 +2256,7 @@
{
"attribute": "aria-invalid",
"condition": "applied automatically by FormControl when the field has an error",
- "description": "Reflects the field's error state from formState. Do not set aria-invalid by hand on a control inside FormControl — FormControl owns it and will overwrite or contradict yours depending on prop order."
+ "description": "Reflects the field's error state from formState. Do not set aria-invalid by hand on a control inside FormControl \u2014 FormControl owns it and will overwrite or contradict yours depending on prop order."
},
{
"attribute": "aria-labelledby",
@@ -2267,7 +2267,7 @@
"keyboardInteractions": [
{
"key": "Tab",
- "description": "Moves to the next control. The Form parts add no keyboard behavior of their own — FormItem is a div and FormControl renders no element, so tab order is the order of the controls."
+ "description": "Moves to the next control. The Form parts add no keyboard behavior of their own \u2014 FormItem is a div and FormControl renders no element, so tab order is the order of the controls."
},
{
"key": "Shift+Tab",
@@ -2278,15 +2278,15 @@
"description": "Submits the form from a single-line text control, which is native form behavior and requires a submit button in the form. It runs the resolver first, so an invalid form errors instead of submitting."
}
],
- "focusManagement": "On a failed submit react-hook-form moves focus to the first invalid field (shouldFocusError defaults to true), which works because the control keeps the ref that Controller passes through FormControl. Note that FormMessage returns null while the field is valid, so the element aria-describedby points at exists only once the field errors — and it is a plain p, not a live region, so the message is announced when the control is next read rather than at the moment it appears. If you need immediate announcement, use the Field family's FieldError, which renders with role=\"alert\".",
- "notes": "The family declares no ARIA role of its own: Form and FormField render nothing, FormControl renders nothing, and FormItem is a plain div — form semantics come from the native form element you write. Every part depends on both contexts: FormField supplies the field name, FormItem supplies the generated id. A FormLabel or FormControl outside a FormItem still renders, but its ids collapse to \"undefined-form-item\", so every such field on the page collides on the same id. A control that is not wrapped in FormControl receives no id, no aria-describedby, and no aria-invalid — the wiring is the whole point of the wrapper, and skipping it leaves an unlabeled field that looks correct on screen. FormLabel must carry visible text; a placeholder is not a label."
+ "focusManagement": "On a failed submit react-hook-form moves focus to the first invalid field (shouldFocusError defaults to true), which works because the control keeps the ref that Controller passes through FormControl. Note that FormMessage returns null while the field is valid, so the element aria-describedby points at exists only once the field errors \u2014 and it is a plain p, not a live region, so the message is announced when the control is next read rather than at the moment it appears. If you need immediate announcement, use the Field family's FieldError, which renders with role=\"alert\".",
+ "notes": "The family declares no ARIA role of its own: Form and FormField render nothing, FormControl renders nothing, and FormItem is a plain div \u2014 form semantics come from the native form element you write. Every part depends on both contexts: FormField supplies the field name, FormItem supplies the generated id. A FormLabel or FormControl outside a FormItem still renders, but its ids collapse to \"undefined-form-item\", so every such field on the page collides on the same id. A control that is not wrapped in FormControl receives no id, no aria-describedby, and no aria-invalid \u2014 the wiring is the whole point of the wrapper, and skipping it leaves an unlabeled field that looks correct on screen. FormLabel must carry visible text; a placeholder is not a label."
},
"composition": {
"subComponents": [
{
"id": "form-field",
"name": "FormField",
- "description": "Wraps react-hook-form's Controller and publishes the field name on context so the parts beneath it can resolve their state. It takes name, control, and a render prop — the field's UI is returned from render({ field, fieldState }), not passed as children — and renders no element itself.",
+ "description": "Wraps react-hook-form's Controller and publishes the field name on context so the parts beneath it can resolve their state. It takes name, control, and a render prop \u2014 the field's UI is returned from render({ field, fieldState }), not passed as children \u2014 and renders no element itself.",
"required": true,
"acceptsChildren": "none",
"categories": [
@@ -2307,7 +2307,7 @@
{
"id": "form-label",
"name": "FormLabel",
- "description": "The field's label, rendered as a Radix Label with htmlFor bound to the generated control id and data-error set when the field is invalid, which is what turns the label text destructive. It takes no error prop — it reads the field's error from context.",
+ "description": "The field's label, rendered as a Radix Label with htmlFor bound to the generated control id and data-error set when the field is invalid, which is what turns the label text destructive. It takes no error prop \u2014 it reads the field's error from context.",
"required": true,
"slot": "label",
"acceptsChildren": "text",
@@ -2318,7 +2318,7 @@
{
"id": "form-control",
"name": "FormControl",
- "description": "A Radix Slot that must wrap exactly one control and renders no element of its own. It merges id, aria-describedby, and aria-invalid onto that child, which is what makes the association automatic. The child must forward props and ref — every shadcn control does; a plain function component that drops them silently loses the wiring.",
+ "description": "A Radix Slot that must wrap exactly one control and renders no element of its own. It merges id, aria-describedby, and aria-invalid onto that child, which is what makes the association automatic. The child must forward props and ref \u2014 every shadcn control does; a plain function component that drops them silently loses the wiring.",
"required": true,
"slot": "control",
"acceptsChildren": "components",
@@ -2329,7 +2329,7 @@
{
"id": "form-description",
"name": "FormDescription",
- "description": "Helper text rendered as a p carrying the generated description id. FormControl references that id from aria-describedby unconditionally, so include a FormDescription in any FormItem where guidance helps — and know that omitting it leaves the reference pointing at nothing.",
+ "description": "Helper text rendered as a p carrying the generated description id. FormControl references that id from aria-describedby unconditionally, so include a FormDescription in any FormItem where guidance helps \u2014 and know that omitting it leaves the reference pointing at nothing.",
"slot": "description",
"acceptsChildren": "any",
"categories": [
@@ -2339,7 +2339,7 @@
{
"id": "form-message",
"name": "FormMessage",
- "description": "The validation message, rendered as a p carrying the generated message id. It prints the field's error message when there is one, falls back to its children when there is not, and returns null when there is neither — so the element exists only while the field is invalid. Author it in every FormItem: FormControl adds its id to aria-describedby the moment the field errors.",
+ "description": "The validation message, rendered as a p carrying the generated message id. It prints the field's error message when there is one, falls back to its children when there is not, and returns null when there is neither \u2014 so the element exists only while the field is invalid. Author it in every FormItem: FormControl adds its id to aria-describedby the moment the field errors.",
"required": true,
"slot": "message",
"acceptsChildren": "text",
@@ -2353,7 +2353,7 @@
"form-item",
"form-control"
],
- "notes": "The shape is fixed: