
Radix UI
Introduction
An open-source UI component library for building high-quality, accessible design systems and web apps.
Radix Primitives is a low-level UI component library with a focus on accessibility, customization and developer experience. You can use these components either as the base layer of your design system, or adopt them incrementally.
Vision
Most of us share similar definitions for common UI patterns like accordion, checkbox, combobox, dialog, dropdown, select, slider, and tooltip. These UI patterns are documented by WAI-ARIA and generally understood by the community.
However, the implementations provided to us by the web platform are inadequate. They're either non-existent, lacking in functionality, or cannot be customized sufficiently.
So, developers are forced to build custom components; an incredibly difficult task. As a result, most components on the web are inaccessible, non-performant, and lacking important features.
Our goal is to create a well-funded, open-source component library that the community can use to build accessible design systems.
Key Features
Accessible
Components adhere to the WAI-ARIA design patterns where possible. We handle many of the difficult implementation details related to accessibility, including aria and role attributes, focus management, and keyboard navigation. Learn more in our accessibility overview.
Unstyled
Components ship without styles, giving you complete control over the look and feel. Components can be styled with any styling solution. Learn more in our styling guide.
Opened
Radix Primitives are designed to be customized to suit your needs. Our open component architecture provides you granular access to each component part, so you can wrap them and add your own event listeners, props, or refs.
Uncontrolled
Where applicable, components are uncontrolled by default but can also be controlled, alternatively. All of the behavior wiring is handled internally, so you can get up and running as smoothly as possible, without needing to create any local states.
Developer experience
One of our main goals is to provide the best possible developer experience. Radix Primitives provides a fully-typed API. All components share a similar API, creating a consistent and predictable experience. We've also implemented an asChild prop, giving users full control over the rendered element.
Incremental adoption
We recommend installing the radix-ui package and importing the primitives you need. This is the simplest way to get started, prevent version conflicts or duplication, and makes it easy to manage updates. The package is tree-shakeable, so you should only ship the components you use.

Implementing a Popover
In this quick tutorial, we will install and style the Popover component.
1. Install the primitive
Install Radix Primitives from your command line.
npm install radix-ui@latest
2. Import the parts
Import and structure the parts.
// index.jsx
import * as React from "react";
import { Popover } from "radix-ui";
const PopoverDemo = () => (
<Popover.Root>
<Popover.Trigger>More info</Popover.Trigger>
<Popover.Portal>
<Popover.Content>
Some more info…
<Popover.Arrow />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
export default PopoverDemo;
Each primitive is also available from its own entrypoint. Importing from the subpath can help some bundlers tree-shake more effectively:
import * as Popover from "radix-ui/popover";
3. Add your styles
Add styles where desired
// index.jsx
import * as React from "react";
import { Popover } from "radix-ui";
import "./styles.css";
const PopoverDemo = () => (
<Popover.Root>
<Popover.Trigger className="PopoverTrigger">Show info</Popover.Trigger>
<Popover.Portal>
<Popover.Content className="PopoverContent">
Some content
<Popover.Arrow className="PopoverArrow" />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
export default PopoverDemo;
/* styles.css */
.PopoverTrigger {
background-color: white;
border-radius: 4px;
}
.PopoverContent {
border-radius: 4px;
padding: 20px;
width: 260px;
background-color: white;
}
.PopoverArrow {
fill: white;
}
Styling
Radix Primitives are unstyled—and compatible with any styling solution—giving you complete control over styling.
Styling overview
Functional styles
You are in control of all aspects of styling, including functional styles. For example—by default—a Dialog Overlay won't cover the entire viewport. You're responsible for adding those styles, plus any presentation styles.
Classes
All components and their parts accept a className prop. This class will be passed through to the DOM element. You can use it in CSS as expected.
Data attributes
When components are stateful, their state will be exposed in a data-state attribute. For example, when an Accordion Item is opened, it includes a data-state="open" attribute.
Styling with CSS
Styling a part
You can style a component part by targeting the className that you provide.
import * as React from "react";
import { Accordion } from "radix-ui";
import "./styles.css";
export const AccordionDemo = () => (
<Accordion.Root>
<Accordion.Item className="AccordionItem" value="item-1" />
{/* … */}
</Accordion.Root>
);
Styling a state
You can style a component state by targeting its data-state attribute.
.AccordionItem {
border-bottom: 1px solid gainsboro;
}
.AccordionItem[data-state="open"] {
border-bottom-width: 2px;
}
Styling with CSS-in-JS
The examples below are using styled-components, but you can use any CSS-in-JS library of your choice.
Styling a part
Most CSS-in-JS libraries export a function for passing components and their styles. You can provide the Radix primitive component directly.
import * as React from "react";
import { Accordion } from "radix-ui";
import styled from "styled-components";
const StyledItem = styled(Accordion.Item)`
border-bottom: 1px solid gainsboro;
`;
export const AccordionDemo = () => (
<Accordion.Root>
<StyledItem value="item-1" />
{/* … */}
</Accordion.Root>
);
Styling a state
You can style a component state by targeting its data-state attribute.
import { Accordion } from "radix-ui";
import styled from "styled-components";
const StyledItem = styled(Accordion.Item)`
border-bottom: 1px solid gainsboro;
&[data-state="open"] {
border-bottom-width: 2px;
}
`;
Extending a primitive
Extending a primitive is done the same way you extend any React component.
import * as React from "react";
import { Accordion as AccordionPrimitive } from "radix-ui";
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>((props, forwardedRef) => (
<AccordionPrimitive.Item {...props} ref={forwardedRef} />
));
AccordionItem.displayName = "AccordionItem";Animation
Animate Radix Primitives with CSS keyframes or the JavaScript animation library of your choice.
Adding animation to Radix Primitives should feel similar to any other component, but there are some caveats noted here in regards to exiting animations with JS animation libraries.
Animating with CSS animation
The simplest way to animate Primitives is with CSS.
You can use CSS animation to animate both mount and unmount phases. The latter is possible because the Radix Primitives will suspend unmount while your animation plays out.
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.DialogOverlay[data-state="open"],
.DialogContent[data-state="open"] {
animation: fadeIn 300ms ease-out;
}
.DialogOverlay[data-state="closed"],
.DialogContent[data-state="closed"] {
animation: fadeOut 300ms ease-in;
}
Delegating unmounting for JavaScript Animation
When many stateful Primitives are hidden from view, they are actually removed from the React Tree, and their elements removed from the DOM. JavaScript animation libraries need control of the unmounting phase, so we provide the forceMount prop on many components to allow consumers to delegate the mounting and unmounting of children based on the animation state determined by those libraries.
For example, if you want to use React Spring to animate a Dialog, you would do so by conditionally rendering the dialog Overlay and Content parts based on the animation state from one of its hooks like useTransition:
import { Dialog } from "radix-ui";
import { useTransition, animated, config } from "react-spring";
function Example() {
const [open, setOpen] = React.useState(false);
const transitions = useTransition(open, {
from: { opacity: 0, y: -10 },
enter: { opacity: 1, y: 0 },
leave: { opacity: 0, y: 10 },
config: config.stiff,
});
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
{transitions((styles, item) =>
item ? (
<>
<Dialog.Overlay forceMount asChild>
<animated.div
style={{
opacity: styles.opacity,
}}
/>
</Dialog.Overlay>
<Dialog.Content forceMount asChild>
<animated.div style={styles}>
<h1>Hello from inside the Dialog!</h1>
<Dialog.Close>close</Dialog.Close>
</animated.div>
</Dialog.Content>
</>
) : null,
)}
</Dialog.Root>
);
}Composition
Use the asChild prop to compose Radix's functionality onto alternative element types or your own React components.
All Radix primitive parts that render a DOM element accept an asChild prop. When asChild is set to true, Radix will not render a default DOM element, instead cloning the part's child and passing it the props and behavior required to make it functional.
Changing the element type
In the majority of cases you shouldn’t need to modify the element type as Radix has been designed to provide the most appropriate defaults. However, there are cases where it is helpful to do so.
A good example is with Tooltip.Trigger. By default this part is rendered as a button, though you may want to add a tooltip to a link (a tag) as well. Let's see how you can achieve this using asChild:
import * as React from "react";
import { Tooltip } from "radix-ui";
export default () => (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<a href="https://www.radix-ui.com/">Radix UI</a>
</Tooltip.Trigger>
<Tooltip.Portal>…</Tooltip.Portal>
</Tooltip.Root>
);
If you do decide to change the underlying element type, it is your responsibility to ensure it remains accessible and functional. In the case of
Tooltip.Triggerfor example, it must be a focusable element that can respond to pointer and keyboard events. If you were to switch it to adiv, it would no longer be accessible.
In reality, you will rarely modify the underlying DOM element like we've seen above. Instead it's more common to use your own React components. This is especially true for most Trigger parts, as you usually want to compose the functionality with the custom buttons and links in your design system.
Composing with your own React components
This works exactly the same as above, you pass asChild to the part and then wrap your own component with it. However, there are a few gotchas to be aware of.
Your component must spread props
When Radix clones your component, it will pass its own props and event handlers to make it functional and accessible. If your component doesn't support those props, it will break.
This is done by spreading all of the props onto the underlying DOM node.
// before
const MyButton = () => <button />;
// after
const MyButton = (props) => <button {...props} />;
We recommend always doing this so that you are not concerned with implementation details (ie. which props/events to accept). We find this is good practice for "leaf" components in general.
Similarly to when changing the element type directly, it is your responsibility to ensure the element type rendered by your custom component remains accessible and functional.
Your component must forward ref
Additionally, Radix will sometimes need to attach a ref to your component (for example to measure its size). If your component doesn't accept a ref, then it will break.
This is done using React.forwardRef (read more on react.dev).
// before
const MyButton = (props) => <button {...props} />;
// after
const MyButton = React.forwardRef((props, forwardedRef) => (
<button {...props} ref={forwardedRef} />
));
Whilst this isn't necessary for all parts, we recommend always doing it so that you are not concerned with implementation details. This is also generally good practice anyway for leaf components.
Composing multiple primitives
asChild can be used as deeply as you need to. This means it is a great way to compose multiple primitive's behavior together. Here is an example of how you can compose Tooltip.Trigger and Dialog.Trigger together with your own button:
import * as React from "react";
import { Dialog, Tooltip } from "radix-ui";
const MyButton = React.forwardRef((props, forwardedRef) => (
<button {...props} ref={forwardedRef} />
));
export default () => {
return (
<Dialog.Root>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Dialog.Trigger asChild>
<MyButton>Open dialog</MyButton>
</Dialog.Trigger>
</Tooltip.Trigger>
<Tooltip.Portal>…</Tooltip.Portal>
</Tooltip.Root>
<Dialog.Portal>...</Dialog.Portal>
</Dialog.Root>
);
};Server-side rendering
Radix Primitives can be rendered on the server. However, Primitives in React versions less than 18 rely on hydration for ids.
Overview
Server-side rendering or SSR, is a technique used to render components to HTML on the server, as opposed to rendering them only on the client.
Static rendering is another similar approach. Instead it pre-renders pages to HTML at build time rather than on each request.
You should be able to use all of our primitives with both approaches, for example with Next.js, Remix, or Gatsby.
Gotcha
Primitives in React versions less than 18 rely on hydration for ids (used in aria attributes) to avoid server/client mismatch errors.
In other words, the equivalent of Time to Interactive for screen reader users will depend on the download speed of the JS bundle. If you'd like to generate ids server-side to improve this experience, we suggest upgrading to React 18.
Components
Accordion
A vertically stacked set of interactive headings that each reveal an associated section of content.
import * as React from "react";
import { Accordion } from "radix-ui";
import classNames from "classnames";
import { ChevronDownIcon } from "@radix-ui/react-icons";
import "./styles.css";
const AccordionDemo = () => (
<Accordion.Root
className="AccordionRoot"
type="single"
defaultValue="item-1"
collapsible
>
<Accordion.Item className="AccordionItem" value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionContent>
</Accordion.Item>
<Accordion.Item className="AccordionItem" value="item-2">
<AccordionTrigger>Is it unstyled?</AccordionTrigger>
<AccordionContent>
Yes. It's unstyled by default, giving you freedom over the look and
feel.
</AccordionContent>
</Accordion.Item>
<Accordion.Item className="AccordionItem" value="item-3">
<AccordionTrigger>Can it be animated?</AccordionTrigger>
<Accordion.Content className="AccordionContent">
<div className="AccordionContentText">
Yes! You can animate the Accordion with CSS or JavaScript.
</div>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
);
const AccordionTrigger = React.forwardRef(function AccordionTrigger(
{ children, className, ...props },
forwardedRef,
) {
return (
<Accordion.Header className="AccordionHeader">
<Accordion.Trigger
className={classNames("AccordionTrigger", className)}
{...props}
ref={forwardedRef}
>
{children}
<ChevronDownIcon className="AccordionChevron" aria-hidden />
</Accordion.Trigger>
</Accordion.Header>
);
});
const AccordionContent = React.forwardRef(function AccordionContent(
{ children, className, ...props },
forwardedRef,
) {
return (
<Accordion.Content
className={classNames("AccordionContent", className)}
{...props}
ref={forwardedRef}
>
<div className="AccordionContentText">{children}</div>
</Accordion.Content>
);
});
export default AccordionDemo;
Features
Full keyboard navigation.
Supports horizontal/vertical orientation.
Supports Right to Left direction.
Can expand one or multiple items.
Can be controlled or uncontrolled.
Anatomy
Import all parts and piece them together.
import { Accordion } from "radix-ui";
export default () => (
<Accordion.Root>
<Accordion.Item>
<Accordion.Header>
<Accordion.Trigger />
</Accordion.Header>
<Accordion.Content />
</Accordion.Item>
</Accordion.Root>
);
API Reference
Root
Contains all the parts of an accordion.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
type* | enum | No default value |
value | string | No default value |
defaultValue | string | No default value |
onValueChange | function | No default value |
value | string[] | [] |
defaultValue | string[] | [] |
onValueChange | function | No default value |
collapsible | boolean | false |
disabled | boolean | false |
dir | enum | "ltr" |
orientation | enum | "vertical" |
| Data attribute | Values |
|---|---|
[data-orientation] | "vertical" | "horizontal" |
Item
Contains all the parts of a collapsible section.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
disabled | boolean | false |
value* | string | No default value |
| Data attribute | Values |
|---|---|
[data-state] | "open" | "closed" |
[data-disabled] | Present when disabled |
[data-orientation] | "vertical" | "horizontal" |
Header
Wraps an Accordion.Trigger. Use the asChild prop to update it to the appropriate heading level for your page.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
| Data attribute | Values |
|---|---|
[data-state] | "open" | "closed" |
[data-disabled] | Present when disabled |
[data-orientation] | "vertical" | "horizontal" |
Trigger
Toggles the collapsed state of its associated item. It should be nested inside of an Accordion.Header.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
| Data attribute | Values |
|---|---|
[data-state] | "open" | "closed" |
[data-disabled] | Present when disabled |
[data-orientation] | "vertical" | "horizontal" |
Content
Contains the collapsible content for an item.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
forceMount | boolean | No default value |
| Data attribute | Values |
|---|---|
[data-state] | "open" | "closed" |
[data-disabled] | Present when disabled |
[data-orientation] | "vertical" | "horizontal" |
| CSS Variable | Description |
|---|---|
--radix-accordion-content-width | The width of the content when it opens/closes |
--radix-accordion-content-height | The height of the content when it opens/closes |
Examples
Expanded by default
Use the defaultValue prop to define the open item by default.
<Accordion.Root type="single" defaultValue="item-2">
<Accordion.Item value="item-1">…</Accordion.Item>
<Accordion.Item value="item-2">…</Accordion.Item>
</Accordion.Root>
Allow collapsing all items
Use the collapsible prop to allow all items to close.
<Accordion.Root type="single" collapsible>
<Accordion.Item value="item-1">…</Accordion.Item>
<Accordion.Item value="item-2">…</Accordion.Item>
</Accordion.Root>
Multiple items open at the same time
Set the type prop to multiple to enable opening multiple items at once.
<Accordion.Root type="multiple">
<Accordion.Item value="item-1">…</Accordion.Item>
<Accordion.Item value="item-2">…</Accordion.Item>
</Accordion.Root>
Rotated icon when open
You can add extra decorative elements, such as chevrons, and rotate it when the item is open.
// index.jsx
import { Accordion } from "radix-ui";
import { ChevronDownIcon } from "@radix-ui/react-icons";
import "./styles.css";
export default () => (
<Accordion.Root type="single">
<Accordion.Item value="item-1">
<Accordion.Header>
<Accordion.Trigger className="AccordionTrigger">
<span>Trigger text</span>
<ChevronDownIcon className="AccordionChevron" aria-hidden />
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>…</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
);
/* styles.css */
.AccordionChevron {
transition: transform 300ms;
}
.AccordionTrigger[data-state="open"] > .AccordionChevron {
transform: rotate(180deg);
}
Horizontal orientation
Use the orientation prop to create a horizontal accordion.
<Accordion.Root orientation="horizontal">
<Accordion.Item value="item-1">…</Accordion.Item>
<Accordion.Item value="item-2">…</Accordion.Item>
</Accordion.Root>
Animating content size
Use the --radix-accordion-content-width and/or --radix-accordion-content-height CSS variables to animate the size of the content when it opens/closes:
// index.jsx
import { Accordion } from "radix-ui";
import "./styles.css";
export default () => (
<Accordion.Root type="single">
<Accordion.Item value="item-1">
<Accordion.Header>…</Accordion.Header>
<Accordion.Content className="AccordionContent">…</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
);
/* styles.css */
.AccordionContent {
overflow: hidden;
}
.AccordionContent[data-state="open"] {
animation: slideDown 300ms ease-out;
}
.AccordionContent[data-state="closed"] {
animation: slideUp 300ms ease-out;
}
@keyframes slideDown {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes slideUp {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
Accessibility
Adheres to the Accordion WAI-ARIA design pattern.
Checkbox
A control that allows the user to toggle between checked and not checked.
import * as React from "react";
import { Checkbox } from "radix-ui";
import { CheckIcon } from "@radix-ui/react-icons";
import "./styles.css";
const CheckboxDemo = () => (
<form>
<div style={{ display: "flex", alignItems: "center" }}>
<Checkbox.Root className="CheckboxRoot" defaultChecked id="c1">
<Checkbox.Indicator className="CheckboxIndicator">
<CheckIcon />
</Checkbox.Indicator>
</Checkbox.Root>
<label className="Label" htmlFor="c1">
Accept terms and conditions.
</label>
</div>
</form>
);
export default CheckboxDemo;
Features
Supports indeterminate state.
Full keyboard navigation.
Can be controlled or uncontrolled.
Anatomy
Import all parts and piece them together.
import { Checkbox } from "radix-ui";
export default () => (
<Checkbox.Root>
<Checkbox.Indicator />
</Checkbox.Root>
);
API Reference
Root
Contains all the parts of a checkbox. An input will also render when used within a form to ensure events propagate correctly.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
defaultChecked | boolean | 'indeterminate' | No default value |
checked | boolean | 'indeterminate' | No default value |
onCheckedChange | function | No default value |
disabled | boolean | No default value |
required | boolean | No default value |
name | string | No default value |
value | string | on |
| Data attribute | Values |
|---|---|
[data-state] | "checked" | "unchecked" | "indeterminate" |
[data-disabled] | Present when disabled |
Indicator
Renders when the checkbox is in a checked or indeterminate state. You can style this element directly, or you can use it as a wrapper to put an icon into, or both.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
forceMount | boolean | No default value |
| Data attribute | Values |
|---|---|
[data-state] | "checked" | "unchecked" | "indeterminate" |
[data-disabled] | Present when disabled |
Examples
Indeterminate
You can set the checkbox to indeterminate by taking control of its state.
import { DividerHorizontalIcon, CheckIcon } from "@radix-ui/react-icons";
import { Checkbox } from "radix-ui";
export default () => {
const [checked, setChecked] = React.useState("indeterminate");
return (
<>
<StyledCheckbox checked={checked} onCheckedChange={setChecked}>
<Checkbox.Indicator>
{checked === "indeterminate" && <DividerHorizontalIcon />}
{checked === true && <CheckIcon />}
</Checkbox.Indicator>
</StyledCheckbox>
<button
type="button"
onClick={() =>
setChecked((prevIsChecked) =>
prevIsChecked === "indeterminate" ? false : "indeterminate",
)
}
>
Toggle indeterminate
</button>
</>
);
};
Decoupling the hidden input
By default, Checkbox.Root renders a visually hidden input for form submission. To recompose, move, or exclude that input, you can build the checkbox from its lower-level parts instead.
Important: These parts are unstable and prefixed with
unstable_, so their API may change in a future release.
Checkbox.unstable_Providerprovides the checkbox state and accepts the form-related props (name,value,checked,defaultChecked,required,disabled,onCheckedChange).Checkbox.unstable_Triggeris the interactive button that wrapsCheckbox.Indicator.Checkbox.unstable_BubbleInputis the visually hidden input thatCheckbox.Rootrenders by default. Omit it when you don't need form submission.
import { Checkbox } from "radix-ui";
export default () => (
<Checkbox.unstable_Provider name="terms">
<Checkbox.unstable_Trigger>
<Checkbox.Indicator />
</Checkbox.unstable_Trigger>
<Checkbox.unstable_BubbleInput />
</Checkbox.unstable_Provider>
);
Accessibility
Adheres to the tri-state Checkbox WAI-ARIA design pattern.
Hover Card
For sighted users to preview content available behind a link.

import * as React from "react";
import { HoverCard } from "radix-ui";
import "./styles.css";
const HoverCardDemo = () => (
<HoverCard.Root>
<HoverCard.Trigger asChild>
<a
className="ImageTrigger"
href="https://twitter.com/radix_ui"
target="_blank"
rel="noreferrer noopener"
>
<img
className="Image normal"
src="https://pbs.twimg.com/profile_images/1337055608613253126/r_eiMp2H_400x400.png"
alt="Radix UI"
/>
</a>
</HoverCard.Trigger>
<HoverCard.Portal>
<HoverCard.Content className="HoverCardContent" sideOffset={5}>
<div style={{ display: "flex", flexDirection: "column", gap: 7 }}>
<img
className="Image large"
src="https://pbs.twimg.com/profile_images/1337055608613253126/r_eiMp2H_400x400.png"
alt="Radix UI"
/>
<div style={{ display: "flex", flexDirection: "column", gap: 15 }}>
<div>
<div className="Text bold">Radix</div>
<div className="Text faded">@radix_ui</div>
</div>
<div className="Text">
Components, icons, colors, and templates for building
high-quality, accessible UI. Free and open-source.
</div>
<div style={{ display: "flex", gap: 15 }}>
<div style={{ display: "flex", gap: 5 }}>
<div className="Text bold">0</div>{" "}
<div className="Text faded">Following</div>
</div>
<div style={{ display: "flex", gap: 5 }}>
<div className="Text bold">2,900</div>{" "}
<div className="Text faded">Followers</div>
</div>
</div>
</div>
</div>
<HoverCard.Arrow className="HoverCardArrow" />
</HoverCard.Content>
</HoverCard.Portal>
</HoverCard.Root>
);
export default HoverCardDemo;
Features
Can be controlled or uncontrolled.
Customize side, alignment, offsets, collision handling.
Optionally render a pointing arrow.
Supports custom open and close delays.
Ignored by screen readers.
Anatomy
Import all parts and piece them together.
import { HoverCard } from "radix-ui";
export default () => (
<HoverCard.Root>
<HoverCard.Trigger />
<HoverCard.Portal>
<HoverCard.Content>
<HoverCard.Arrow />
</HoverCard.Content>
</HoverCard.Portal>
</HoverCard.Root>
);
API Reference
Root
Contains all the parts of a hover card.
| Prop | Type | Default |
|---|---|---|
defaultOpen | boolean | No default value |
open | boolean | No default value |
onOpenChange | function | No default value |
openDelay | number | 700 |
closeDelay | number | 300 |
Trigger
The link that opens the hover card when hovered.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
| Data attribute | Values |
|---|---|
[data-state] | "open" | "closed" |
Portal
When used, portals the content part into the body.
| Prop | Type | Default |
|---|---|---|
forceMount | boolean | No default value |
container | HTMLElement | document.body |
Content
The component that pops out when the hover card is open.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
forceMount | boolean | No default value |
side | enum | "bottom" |
sideOffset | number | 0 |
align | enum | "center" |
alignOffset | number | 0 |
avoidCollisions | boolean | true |
collisionBoundary | Boundary | [] |
collisionPadding | number | Padding | 0 |
arrowPadding | number | 0 |
sticky | enum | "partial" |
hideWhenDetached | boolean | false |
| Data attribute | Values |
|---|---|
[data-state] | "open" | "closed" |
[data-side] | "left" | "right" | "bottom" | "top" |
[data-align] | "start" | "end" | "center" |
| CSS Variable | Description |
|---|---|
--radix-hover-card-content-transform-origin | The transform-origin computed from the content and arrow positions/offsets |
--radix-hover-card-content-available-width | The remaining width between the trigger and the boundary edge |
--radix-hover-card-content-available-height | The remaining height between the trigger and the boundary edge |
--radix-hover-card-trigger-width | The width of the trigger |
--radix-hover-card-trigger-height | The height of the trigger |
Arrow
An optional arrow element to render alongside the hover card. This can be used to help visually link the trigger with the HoverCard.Content. Must be rendered inside HoverCard.Content.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
width | number | 10 |
height | number | 5 |
Examples
Show instantly
Use the openDelay prop to control the time it takes for the hover card to open.
import { HoverCard } from "radix-ui";
export default () => (
<HoverCard.Root openDelay={0}>
<HoverCard.Trigger>…</HoverCard.Trigger>
<HoverCard.Content>…</HoverCard.Content>
</HoverCard.Root>
);
Constrain the content size
You may want to constrain the width of the content so that it matches the trigger width. You may also want to constrain its height to not exceed the viewport.
We expose several CSS custom properties such as --radix-hover-card-trigger-width and --radix-hover-card-content-available-height to support this. Use them to constrain the content dimensions.
// index.jsx
import { HoverCard } from "radix-ui";
import "./styles.css";
export default () => (
<HoverCard.Root>
<HoverCard.Trigger>…</HoverCard.Trigger>
<HoverCard.Portal>
<HoverCard.Content className="HoverCardContent" sideOffset={5}>
…
</HoverCard.Content>
</HoverCard.Portal>
</HoverCard.Root>
);
/* styles.css */
.HoverCardContent {
width: var(--radix-hover-card-trigger-width);
max-height: var(--radix-hover-card-content-available-height);
}
Origin-aware animations
We expose a CSS custom property --radix-hover-card-content-transform-origin. Use it to animate the content from its computed origin based on side, sideOffset, align, alignOffset and any collisions.
// index.jsx
import { HoverCard } from "radix-ui";
import "./styles.css";
export default () => (
<HoverCard.Root>
<HoverCard.Trigger>…</HoverCard.Trigger>
<HoverCard.Content className="HoverCardContent">…</HoverCard.Content>
</HoverCard.Root>
);
/* styles.css */
.HoverCardContent {
transform-origin: var(--radix-hover-card-content-transform-origin);
animation: scaleIn 0.5s ease-out;
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0);
}
to {
opacity: 1;
transform: scale(1);
}
}
Collision-aware animations
We expose data-side and data-align attributes. Their values will change at runtime to reflect collisions. Use them to create collision and direction-aware animations.
// index.jsx
import { HoverCard } from "radix-ui";
import "./styles.css";
export default () => (
<HoverCard.Root>
<HoverCard.Trigger>…</HoverCard.Trigger>
<HoverCard.Content className="HoverCardContent">…</HoverCard.Content>
</HoverCard.Root>
);
/* styles.css */
.HoverCardContent {
animation-duration: 0.6s;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
.HoverCardContent[data-side="top"] {
animation-name: slideUp;
}
.HoverCardContent[data-side="bottom"] {
animation-name: slideDown;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
Accessibility
The hover card is intended for sighted users only, the content will be inaccessible to keyboard users.
Navigation Menu
A collection of links for navigating websites.
import * as React from "react";
import { NavigationMenu } from "radix-ui";
import classNames from "classnames";
import { CaretDownIcon } from "@radix-ui/react-icons";
import "./styles.css";
const NavigationMenuDemo = () => {
return (
<NavigationMenu.Root className="NavigationMenuRoot">
<NavigationMenu.List className="NavigationMenuList">
<NavigationMenu.Item>
<NavigationMenu.Trigger className="NavigationMenuTrigger">
Learn <CaretDownIcon className="CaretDown" aria-hidden />
</NavigationMenu.Trigger>
<NavigationMenu.Content className="NavigationMenuContent">
<ul className="List one">
<li style={{ gridRow: "span 3" }}>
<NavigationMenu.Link asChild>
<a className="Callout" href="#">
<svg
aria-hidden
width="38"
height="38"
viewBox="0 0 25 25"
fill="white"
>
<path d="M12 25C7.58173 25 4 21.4183 4 17C4 12.5817 7.58173 9 12 9V25Z"></path>
<path d="M12 0H4V8H12V0Z"></path>
<path d="M17 8C19.2091 8 21 6.20914 21 4C21 1.79086 19.2091 0 17 0C14.7909 0 13 1.79086 13 4C13 6.20914 14.7909 8 17 8Z"></path>
</svg>
<div className="CalloutHeading">Radix Primitives</div>
<p className="CalloutText">
Unstyled, accessible components for React.
</p>
</a>
</NavigationMenu.Link>
</li>
<ListItem href="https://stitches.dev/" title="Stitches">
CSS-in-JS with best-in-class developer experience.
</ListItem>
<ListItem href="/colors" title="Colors">
Beautiful, thought-out palettes with auto dark mode.
</ListItem>
<ListItem href="https://icons.radix-ui.com/" title="Icons">
A crisp set of 15x15 icons, balanced and consistent.
</ListItem>
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Trigger className="NavigationMenuTrigger">
Overview <CaretDownIcon className="CaretDown" aria-hidden />
</NavigationMenu.Trigger>
<NavigationMenu.Content className="NavigationMenuContent">
<ul className="List two">
<ListItem
title="Introduction"
href="/primitives/docs/overview/introduction"
>
Build high-quality, accessible design systems and web apps.
</ListItem>
<ListItem
title="Getting started"
href="/primitives/docs/overview/getting-started"
>
A quick tutorial to get you up and running with Radix
Primitives.
</ListItem>
<ListItem title="Styling" href="/primitives/docs/guides/styling">
Unstyled and compatible with any styling solution.
</ListItem>
<ListItem
title="Animation"
href="/primitives/docs/guides/animation"
>
Use CSS keyframes or any animation library of your choice.
</ListItem>
<ListItem
title="Accessibility"
href="/primitives/docs/overview/accessibility"
>
Tested in a range of browsers and assistive technologies.
</ListItem>
<ListItem
title="Releases"
href="/primitives/docs/overview/releases"
>
Radix Primitives releases and their changelogs.
</ListItem>
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Link
className="NavigationMenuLink"
href="https://github.com/radix-ui"
>
Github
</NavigationMenu.Link>
</NavigationMenu.Item>
<NavigationMenu.Indicator className="NavigationMenuIndicator">
<div className="Arrow" />
</NavigationMenu.Indicator>
</NavigationMenu.List>
<div className="ViewportPosition">
<NavigationMenu.Viewport className="NavigationMenuViewport" />
</div>
</NavigationMenu.Root>
);
};
const ListItem = React.forwardRef(function ListItem(
{ className, children, title, ...props },
forwardedRef,
) {
return (
<li>
<NavigationMenu.Link asChild>
<a
className={classNames("ListItemLink", className)}
{...props}
ref={forwardedRef}
>
<div className="ListItemHeading">{title}</div>
<p className="ListItemText">{children}</p>
</a>
</NavigationMenu.Link>
</li>
);
});
export default NavigationMenuDemo;
Features
Can be controlled or uncontrolled.
Flexible layout structure with managed tab focus.
Supports submenus.
Optional active item indicator.
Full keyboard navigation.
Exposes CSS variables for advanced animation.
Supports custom timings.
Anatomy
Import all parts and piece them together.
import { NavigationMenu } from "radix-ui";
export default () => (
<NavigationMenu.Root>
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger />
<NavigationMenu.Content>
<NavigationMenu.Link />
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Link />
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Trigger />
<NavigationMenu.Content>
<NavigationMenu.Sub>
<NavigationMenu.List />
<NavigationMenu.Viewport />
</NavigationMenu.Sub>
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Indicator />
</NavigationMenu.List>
<NavigationMenu.Viewport />
</NavigationMenu.Root>
);
API Reference
Root
Contains all the parts of a navigation menu.
| Prop | Type | Default |
|---|---|---|
defaultValue | string | No default value |
value | string | No default value |
onValueChange | function | No default value |
delayDuration | number | 200 |
skipDelayDuration | number | 300 |
dir | enum | No default value |
orientation | enum | "horizontal" |
| Data attribute | Values |
|---|---|
[data-orientation] | "vertical" | "horizontal" |
Sub
Signifies a submenu. Use it in place of the root part when nested to create a submenu.
| Prop | Type | Default |
|---|---|---|
defaultValue | string | No default value |
value | string | No default value |
onValueChange | function | No default value |
orientation | enum | "horizontal" |
| Data attribute | Values |
|---|---|
[data-orientation] | "vertical" | "horizontal" |
List
Contains the top level menu items.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
| Data attribute | Values |
|---|---|
[data-orientation] | "vertical" | "horizontal" |
Item
A top level menu item, contains a link or trigger with content combination.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
value | string | No default value |
Trigger
The button that toggles the content.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
| Data attribute | Values |
|---|---|
[data-state] | "open" | "closed" |
[data-disabled] | Present when disabled |
Content
Contains the content associated with each trigger.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
onEscapeKeyDown | function | No default value |
onPointerDownOutside | function | No default value |
onFocusOutside | function | No default value |
onInteractOutside | function | No default value |
forceMount | boolean | No default value |
| Data attribute | Values |
|---|---|
[data-state] | "open" | "closed" |
[data-motion] | "to-start" | "to-end" | "from-start" | "from-end" |
[data-orientation] | "vertical" | "horizontal" |
Link
A navigational link.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
active | boolean | false |
onSelect | function | No default value |
| Data attribute | Values |
|---|---|
[data-active] | Present when active |
Indicator
An optional indicator element that renders below the list, is used to highlight the currently active trigger.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
forceMount | boolean | No default value |
| Data attribute | Values |
|---|---|
[data-state] | "visible" | "hidden" |
[data-orientation] | "vertical" | "horizontal" |
| CSS Variable | Description |
|---|---|
--radix-navigation-menu-indicator-translate-x | The horizontal offset of the indicator, computed from the active trigger's position. Present when the menu is horizontal. |
--radix-navigation-menu-indicator-translate-y | The vertical offset of the indicator, computed from the active trigger's position. Present when the menu is vertical. |
Viewport
An optional viewport element that is used to render active content outside of the list.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
forceMount | boolean | No default value |
| Data attribute | Values |
|---|---|
[data-state] | "open" | "closed" |
[data-orientation] | "vertical" | "horizontal" |
| CSS Variable | Description |
|---|---|
--radix-navigation-menu-viewport-width | The width of the viewport when visible/hidden, computed from the active content |
--radix-navigation-menu-viewport-height | The height of the viewport when visible/hidden, computed from the active content |
Examples
Vertical
You can create a vertical menu by using the orientation prop.
<NavigationMenu.Root orientation="vertical">
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item one</NavigationMenu.Trigger>
<NavigationMenu.Content>Item one content</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item two</NavigationMenu.Trigger>
<NavigationMenu.Content>Item Two content</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
</NavigationMenu.Root>
Flexible layouts
Use the Viewport part when you need extra control over where Content is rendered. This can be helpful when your design requires an adjusted DOM structure or if you need flexibility to achieve advanced animation. Tab focus will be maintained automatically.
<NavigationMenu.Root>
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item one</NavigationMenu.Trigger>
<NavigationMenu.Content>Item one content</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item two</NavigationMenu.Trigger>
<NavigationMenu.Content>Item two content</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
{/* NavigationMenu.Content will be rendered here when active */}
<NavigationMenu.Viewport />
</NavigationMenu.Root>
With indicator
You can use the optional Indicator part to highlight the currently active Trigger, this is useful when you want to provide an animated visual cue such as an arrow or highlight to accompany the Viewport.
// index.jsx
import { NavigationMenu } from "radix-ui";
import "./styles.css";
export default () => (
<NavigationMenu.Root>
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item one</NavigationMenu.Trigger>
<NavigationMenu.Content>Item one content</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item two</NavigationMenu.Trigger>
<NavigationMenu.Content>Item two content</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Indicator className="NavigationMenuIndicator" />
</NavigationMenu.List>
<NavigationMenu.Viewport />
</NavigationMenu.Root>
);
/* styles.css */
.NavigationMenuIndicator {
background-color: grey;
}
.NavigationMenuIndicator[data-orientation="horizontal"] {
height: 3px;
transition:
width,
transform,
250ms ease;
}
With submenus
Create a submenu by nesting your NavigationMenu and using the Sub part in place of its Root. Submenus work differently to Root navigation menus and are similar to Tabs in that one item should always be active, so be sure to assign and set a defaultValue.
<NavigationMenu.Root>
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item one</NavigationMenu.Trigger>
<NavigationMenu.Content>Item one content</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item two</NavigationMenu.Trigger>
<NavigationMenu.Content>
<NavigationMenu.Sub defaultValue="sub1">
<NavigationMenu.List>
<NavigationMenu.Item value="sub1">
<NavigationMenu.Trigger>Sub item one</NavigationMenu.Trigger>
<NavigationMenu.Content>
Sub item one content
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item value="sub2">
<NavigationMenu.Trigger>Sub item two</NavigationMenu.Trigger>
<NavigationMenu.Content>
Sub item two content
</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
</NavigationMenu.Sub>
</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
</NavigationMenu.Root>
With client side routing
If you need to use the Link component provided by your routing package then we recommend composing with NavigationMenu.Link via a custom component. This will ensure accessibility and consistent keyboard control is maintained. Here's an example using Next.js:
// index.jsx
import { usePathname } from "next/navigation";
import NextLink from "next/link";
import { NavigationMenu } from "radix-ui";
import "./styles.css";
const Link = ({ href, ...props }) => {
const pathname = usePathname();
const isActive = href === pathname;
return (
<NavigationMenu.Link asChild active={isActive}>
<NextLink href={href} className="NavigationMenuLink" {...props} />
</NavigationMenu.Link>
);
};
export default () => (
<NavigationMenu.Root>
<NavigationMenu.List>
<NavigationMenu.Item>
<Link href="/">Home</Link>
</NavigationMenu.Item>
<NavigationMenu.Item>
<Link href="/about">About</Link>
</NavigationMenu.Item>
</NavigationMenu.List>
</NavigationMenu.Root>
);
/* styles.css */
.NavigationMenuLink {
text-decoration: none;
}
.NavigationMenuLink[data-active] {
text-decoration: "underline";
}
Advanced animation
We expose --radix-navigation-menu-viewport-[width|height] and data-motion['from-start'|'to-start'|'from-end'|'to-end'] attributes to allow you to animate Viewport size and Content position based on the enter/exit direction.
Combining these with position: absolute; allows you to create smooth overlapping animation effects when moving between items.
// index.jsx
import { NavigationMenu } from "radix-ui";
import "./styles.css";
export default () => (
<NavigationMenu.Root>
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item one</NavigationMenu.Trigger>
<NavigationMenu.Content className="NavigationMenuContent">
Item one content
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Item two</NavigationMenu.Trigger>
<NavigationMenu.Content className="NavigationMenuContent">
Item two content
</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
<NavigationMenu.Viewport className="NavigationMenuViewport" />
</NavigationMenu.Root>
);
/* styles.css */
.NavigationMenuContent {
position: absolute;
top: 0;
left: 0;
animation-duration: 250ms;
animation-timing-function: ease;
}
.NavigationMenuContent[data-motion="from-start"] {
animation-name: enterFromLeft;
}
.NavigationMenuContent[data-motion="from-end"] {
animation-name: enterFromRight;
}
.NavigationMenuContent[data-motion="to-start"] {
animation-name: exitToLeft;
}
.NavigationMenuContent[data-motion="to-end"] {
animation-name: exitToRight;
}
.NavigationMenuViewport {
position: relative;
width: var(--radix-navigation-menu-viewport-width);
height: var(--radix-navigation-menu-viewport-height);
transition:
width,
height,
250ms ease;
}
@keyframes enterFromRight {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enterFromLeft {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes exitToRight {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes exitToLeft {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
Accessibility
Adheres to the navigationrole requirements.
Differences to menubar
NavigationMenu should not be confused with menubar, although this primitive shares the name menu in the colloquial sense to refer to a set of navigation links, it does not use the WAI-ARIA menu role. This is because menu and menubars behave like native operating system menus most commonly found in desktop application windows, as such they feature complex functionality like composite focus management and first-character navigation.
These features are often considered unnecessary for website navigation and at worst can confuse users who are familiar with established website patterns.
See the W3C Disclosure Navigation Menu example for more information.
Link usage and aria-current
It's important to use NavigationMenu.Link for all navigational links within a menu, this not only applies to the main list but also within any content rendered via NavigationMenu.Content. This will ensure consistent keyboard interactions and accessibility while also giving access to the active prop for setting aria-current and the active styles. See this example for more information on usage with third party routing components.
Join Techsnap Creators
Share your knowledge and earn ??
Want to showcase your tech expertise and get rewarded for your insights? Join the Techsnap creator network!
Write insightful blogs, stay ahead of industry trends, and grow your professional brand while helping others in the community.
Ready to make an impact?

Comments