web-ui-radix-ui-1dd1ff — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-ui-radix-ui-1dd1ff (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide: Radix UI provides unstyled, accessible primitives for building design systems. Use compound component patterns (Root, Trigger, Content),asChildfor polymorphism, anddata-stateattributes for animations. Focus on behavior and accessibility - defer styling decisions to your styling solution. Current: v1.4.x (May 2025) - Full React 19 and RSC compatibility with new preview primitives.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)(You MUST use compound component anatomy - Root, Trigger, Portal, Content, Close - for overlay components)
(You MUST use `forwardRef` and spread all props when using `asChild` with custom components - unless using React 19+ where `ref` is a regular prop)
(You MUST use Portal for overlays to escape CSS stacking contexts and parent overflow constraints)
(You MUST provide accessible labels via Title/Description components or ARIA attributes - Dialog logs console errors for missing Title)
</critical_requirements>
Auto-detection: Radix UI, radix-ui, @radix-ui, Dialog, Dropdown, DropdownMenu, Select, Popover, Tooltip, Accordion, Tabs, AlertDialog, asChild, Slot, Portal, data-state, OneTimePasswordField, PasswordToggleField, unstable_Form, Form.Field, Form.Message
When to use:
asChild patternWhen NOT to use:
Package Installation:
# Recommended: Unified tree-shakeable package (prevents version conflicts)
npm i radix-ui
# Alternative: Individual packages
npm i @radix-ui/react-dialog @radix-ui/react-dropdown-menuDetailed Resources:
<philosophy>
Radix UI Primitives provide behavioral and accessibility foundations without imposing visual design. Each primitive handles:
Radix is styling-agnostic: Apply styles via className prop using your styling solution. The primitives expose data-state attributes for state-based styling.
Compound Component Model: Each primitive consists of multiple parts (Root, Trigger, Content, etc.) that share context. This enables flexible composition while maintaining coordinated behavior.
React 19 & RSC Support (v1.4.3): Full compatibility with React 19 and React Server Components. Enhanced keyboard handling avoids browser hotkey interference.
</philosophy>
<patterns>
Radix primitives use a compound component pattern where multiple parts work together through shared context.
#### Standard Structure for Overlay Components
import { Dialog } from "radix-ui";
// Root provides context and state management
// Trigger opens the dialog
// Portal renders content outside React tree
// Overlay covers the page
// Content contains the dialog body
// Close dismisses the dialog
// Title and Description provide accessibility
<Dialog.Root>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className={className} />
<Dialog.Content className={className}>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Accessible description</Dialog.Description>
{/* Dialog content */}
<Dialog.Close>Close</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>Why this structure: Root manages state and context, Portal escapes CSS stacking contexts, Overlay provides visual backdrop, Title/Description ensure screen reader accessibility
Radix primitives support both controlled and uncontrolled state patterns.
#### Uncontrolled (Radix Manages State)
// Let Radix manage internal state - simpler for basic use cases
<Dialog.Root defaultOpen={false}>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Content>
{/* Content */}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>When to use: Simple dialogs without external state requirements
#### Controlled (You Manage State)
import { useState } from "react";
import { Dialog } from "radix-ui";
function ControlledDialog() {
const [open, setOpen] = useState(false);
const handleSave = async () => {
await saveData();
setOpen(false); // Programmatically close after async operation
};
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Content>
<Dialog.Title>Edit Profile</Dialog.Title>
<button onClick={handleSave}>Save</button>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}When to use: Programmatic control needed (close after async, open from external trigger, sync with URL state)
The asChild prop enables Radix to merge behavior onto your custom components or different element types.
#### Changing Element Type
import { Tooltip } from "radix-ui";
// Tooltip trigger defaults to button, but you may want a link
<Tooltip.Root>
<Tooltip.Trigger asChild>
<a href="/docs">Documentation</a>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content>View the docs</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>Why good: Radix passes all required props and event handlers to the anchor, maintaining accessibility
#### With Custom Components
import { forwardRef } from "react";
import { Dialog } from "radix-ui";
// Custom component MUST use forwardRef and spread props
const CustomButton = forwardRef<HTMLButtonElement, React.ComponentProps<"button">>(
({ className, ...props }, ref) => {
return <button ref={ref} className={className} {...props} />;
}
);
CustomButton.displayName = "CustomButton";
// Use with asChild
<Dialog.Trigger asChild>
<CustomButton className="custom-class">Open Dialog</CustomButton>
</Dialog.Trigger>Why this works: forwardRef allows Radix to attach refs for positioning/focus, spreading props passes event handlers and ARIA attributes
Use the Slot utility to build your own components with asChild support.
import { forwardRef } from "react";
import { Slot } from "radix-ui";
export type ButtonProps = React.ComponentProps<"button"> & {
asChild?: boolean;
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ asChild = false, className, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return <Comp ref={ref} className={className} {...props} />;
}
);
Button.displayName = "Button";
// Usage - renders as button
<Button>Click me</Button>
// Usage with asChild - renders as anchor
<Button asChild>
<a href="/page">Navigate</a>
</Button>Why good: Slot merges all props onto the child element, eliminating wrapper elements while preserving behavior
Portal renders content outside the React component tree to escape CSS stacking contexts.
import { Popover } from "radix-ui";
<Popover.Root>
<Popover.Trigger>Toggle Popover</Popover.Trigger>
<Popover.Portal>
{/* Rendered in document.body, escaping parent overflow:hidden */}
<Popover.Content className={className}>
<Popover.Arrow />
Popover content
</Popover.Content>
</Popover.Portal>
</Popover.Root>When to use: All overlay components (dialogs, popovers, tooltips, dropdown menus)
#### Custom Portal Container
import { useRef } from "react";
import { Dialog } from "radix-ui";
function DialogWithCustomContainer() {
const containerRef = useRef<HTMLDivElement>(null);
return (
<>
<div ref={containerRef} />
<Dialog.Root>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Portal container={containerRef.current}>
<Dialog.Content>Content in custom container</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</>
);
}When to use: Micro-frontends, iframes, or specific DOM hierarchy requirements
Radix primitives expose data-state attributes for CSS-based animations. The unmount is suspended while exit animations complete. Use CSS @keyframes (not transition) -- Radix detects animation end events.
/* CSS keyframes — Radix suspends unmount until animation completes */
.dialog-overlay[data-state="open"] {
animation: fadeIn 150ms ease-out;
}
.dialog-overlay[data-state="closed"] {
animation: fadeOut 150ms ease-in;
}Critical: CSS transition does NOT delay unmount -- only @keyframes animation works for exit animations.
#### JavaScript Animation Libraries
For complex orchestrated animations, use forceMount on Portal, Overlay, and Content to prevent Radix from unmounting during exit animations. Wrap with your animation library's presence detection.
// Key pattern: controlled state + forceMount + conditional rendering
<Dialog.Root open={open} onOpenChange={setOpen}>
{open && (
<Dialog.Portal forceMount>
<Dialog.Overlay asChild forceMount>{/* animated overlay */}</Dialog.Overlay>
<Dialog.Content asChild forceMount>{/* animated content */}</Dialog.Content>
</Dialog.Portal>
)}
</Dialog.Root>See examples/animation.md for complete CSS keyframe and accordion height animation examples.
Radix handles focus automatically for accessible interactions.
#### Default Behavior
// Focus automatically trapped in modal dialogs
// Focus returns to trigger on close
<Dialog.Root>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Content>
{/* Focus trapped here until closed */}
<input autoFocus /> {/* Receives focus on open */}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>#### Custom Focus Control
import { useRef } from "react";
import { AlertDialog } from "radix-ui";
function AlertDialogWithCustomFocus() {
const cancelRef = useRef<HTMLButtonElement>(null);
return (
<AlertDialog.Root>
<AlertDialog.Trigger>Delete</AlertDialog.Trigger>
<AlertDialog.Portal>
<AlertDialog.Content
onOpenAutoFocus={(e) => {
e.preventDefault();
cancelRef.current?.focus(); // Focus cancel instead of first element
}}
>
<AlertDialog.Title>Confirm Delete</AlertDialog.Title>
<AlertDialog.Cancel ref={cancelRef}>Cancel</AlertDialog.Cancel>
<AlertDialog.Action>Delete</AlertDialog.Action>
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog.Root>
);
}Why custom focus: Destructive dialogs should focus the safe action (Cancel) by default
Radix provides Title and Description components for screen reader accessibility.
import { Dialog } from "radix-ui";
<Dialog.Root>
<Dialog.Trigger>Settings</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Content
aria-describedby={undefined} // Remove if no description
>
{/* Title is announced when dialog opens */}
<Dialog.Title>Account Settings</Dialog.Title>
{/* Description provides additional context */}
<Dialog.Description>
Manage your account preferences and security settings.
</Dialog.Description>
{/* Or visually hide but keep accessible */}
<VisuallyHidden asChild>
<Dialog.Description>
This description is read by screen readers but not visible.
</Dialog.Description>
</VisuallyHidden>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>Why mandatory: Screen readers announce Title when dialog opens, Description provides context for the interaction
</patterns>
<integration>
Radix is behavior-only: Components are unstyled. Apply styles via className prop using your styling solution.
Works with:
asChild components with the Slot component from radix-uiforceMount for JavaScript animation controlCommon Component Pairs:
| Primitive | Use Case |
|---|---|
| Dialog | Modal dialogs, forms, confirmations |
| AlertDialog | Destructive confirmations requiring explicit action |
| DropdownMenu | Navigation menus, action menus |
| Select | Form selects with custom styling |
| Popover | Non-modal floating content |
| Tooltip | Contextual information on hover/focus |
| Accordion | Expandable content sections |
| Tabs | Tabbed interfaces |
| Progress | Progress bars (supports value={undefined} for indeterminate) |
Preview Components (Unstable API):
| Primitive | Use Case | Import Prefix | Version |
|---|---|---|---|
| OneTimePasswordField | OTP input with keyboard nav, paste, autofill | unstable_ | 0.1.8 |
| PasswordToggleField | Password visibility toggle with focus management | unstable_ | 0.1.3 |
| Form | Form validation with constraint API | unstable_ | 0.1.8 |
Note: Preview components use unstable_ prefix. APIs may change before stable release.
</integration>
<red_flags>
High Priority Issues:
forwardRef on custom asChild components -- Radix cannot attach refs for positioning and focus managementasChild components -- ARIA attributes and event handlers are lostoverflow: hidden or z-index issuesGotchas & Edge Cases:
transition does NOT delay unmount -- only @keyframes animation works for exitdata-state changes to "closed" before exit animation startsforwardRef wrapper no longer needed -- ref is a regular propradix-ui package over individual @radix-ui/* packages to prevent version conflictsSee reference.md for full anti-pattern examples with code and decision frameworks.
</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST use compound component anatomy - Root, Trigger, Portal, Content, Close - for overlay components)
(You MUST use `forwardRef` and spread all props when using `asChild` with custom components - unless using React 19+ where `ref` is a regular prop)
(You MUST use Portal for overlays to escape CSS stacking contexts and parent overflow constraints)
(You MUST provide accessible labels via Title/Description components or ARIA attributes - Dialog logs console errors for missing Title)
Failure to follow these rules will break accessibility, focus management, and proper DOM rendering.
</critical_reminders>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.