Getting started

A quick tutorial to get you up and running with Radix Primitives.

In this quick tutorial, we will install, style, and export a Popover component. This example uses Stitches, but any styling solution is compatible.

Install the component from your command line.

npm install @radix-ui/react-popover

Import and export the parts. Add styles where desired.

// Popover.tsx
import { styled } from '@stitches/react';
import * as PopoverPrimitive from '@radix-ui/react-popover';
export const Popover = PopoverPrimitive.Root;
export const PopoverTrigger = PopoverPrimitive.Trigger;
export const PopoverContent = styled(PopoverPrimitive.Content, {
// your styles
});

Import each of the component parts into your app, then assemble your Popover.

// App.tsx
import { Popover, PopoverTrigger, PopoverContent, } from 'your-components/Popover';
function App() {
return (
<Popover>
<PopoverTrigger>...</PopoverTrigger>
<PopoverContent>...</PopoverContent>
</Popover>
);
}

Here's a live and interactive demo.

import React from 'react';
import { styled, keyframes } from '@stitches/react';
import { violet, mauve, blackA } from '@radix-ui/colors';
import { MixerHorizontalIcon, Cross2Icon } from '@radix-ui/react-icons';
import * as PopoverPrimitive from '@radix-ui/react-popover';
const slideUpAndFade = keyframes({
'0%': { opacity: 0, transform: 'translateY(2px)' },
'100%': { opacity: 1, transform: 'translateY(0)' },
});
const slideRightAndFade = keyframes({
'0%': { opacity: 0, transform: 'translateX(-2px)' },
'100%': { opacity: 1, transform: 'translateX(0)' },
});
const slideDownAndFade = keyframes({
'0%': { opacity: 0, transform: 'translateY(-2px)' },
'100%': { opacity: 1, transform: 'translateY(0)' },
});
const slideLeftAndFade = keyframes({
'0%': { opacity: 0, transform: 'translateX(2px)' },
'100%': { opacity: 1, transform: 'translateX(0)' },
});
const StyledContent = styled(PopoverPrimitive.Content, {
borderRadius: 4,
padding: 20,
width: 260,
backgroundColor: 'white',
boxShadow: 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
'@media (prefers-reduced-motion: no-preference)': {
animationDuration: '400ms',
animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
willChange: 'transform, opacity',
'&[data-state="open"]': {
'&[data-side="top"]': { animationName: slideDownAndFade },
'&[data-side="right"]': { animationName: slideLeftAndFade },
'&[data-side="bottom"]': { animationName: slideUpAndFade },
'&[data-side="left"]': { animationName: slideRightAndFade },
},
},
'&:focus': {
boxShadow: `hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px, 0 0 0 2px ${violet.violet7}`,
},
});
const StyledArrow = styled(PopoverPrimitive.Arrow, {
fill: 'white',
});
const StyledClose = styled(PopoverPrimitive.Close, {
all: 'unset',
fontFamily: 'inherit',
borderRadius: '100%',
height: 25,
width: 25,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
color: violet.violet11,
position: 'absolute',
top: 5,
right: 5,
'&:hover': { backgroundColor: violet.violet4 },
'&:focus': { boxShadow: `0 0 0 2px ${violet.violet7}` },
});
// Exports
export const Popover = PopoverPrimitive.Root;
export const PopoverTrigger = PopoverPrimitive.Trigger;
export const PopoverContent = StyledContent;
export const PopoverArrow = StyledArrow;
export const PopoverClose = StyledClose;
// Your app...
const Flex = styled('div', { display: 'flex' });
const IconButton = styled('button', {
all: 'unset',
fontFamily: 'inherit',
borderRadius: '100%',
height: 35,
width: 35,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
color: violet.violet11,
backgroundColor: 'white',
boxShadow: `0 2px 10px ${blackA.blackA7}`,
'&:hover': { backgroundColor: violet.violet3 },
'&:focus': { boxShadow: `0 0 0 2px black` },
});
const Fieldset = styled('fieldset', {
all: 'unset',
display: 'flex',
gap: 20,
alignItems: 'center',
});
const Label = styled('label', {
fontSize: 13,
color: violet.violet11,
width: 75,
});
const Input = styled('input', {
all: 'unset',
width: '100%',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
flex: '1',
borderRadius: 4,
padding: '0 10px',
fontSize: 13,
lineHeight: 1,
color: violet.violet11,
boxShadow: `0 0 0 1px ${violet.violet7}`,
height: 25,
'&:focus': { boxShadow: `0 0 0 2px ${violet.violet8}` },
});
const Text = styled('div', {
margin: 0,
color: mauve.mauve12,
fontSize: 15,
lineHeight: '19px',
variants: {
faded: {
true: { color: mauve.mauve10 },
},
bold: {
true: { fontWeight: 500 },
},
},
});
const PopoverDemo = () => (
<Popover>
<PopoverTrigger asChild>
<IconButton aria-label="Update dimensions">
<MixerHorizontalIcon />
</IconButton>
</PopoverTrigger>
<PopoverContent sideOffset={5} >
<Flex css={{ flexDirection: 'column', gap: 10 }}>
<Text bold css={{ marginBottom: 10 }}>
Dimensions
</Text>
<Fieldset>
<Label htmlFor="width">Width</Label>
<Input id="width" defaultValue="100%" />
</Fieldset>
<Fieldset>
<Label htmlFor="maxWidth">Max. width</Label>
<Input id="maxWidth" defaultValue="300px" />
</Fieldset>
<Fieldset>
<Label htmlFor="height">Height</Label>
<Input id="height" defaultValue="25px" />
</Fieldset>
<Fieldset>
<Label htmlFor="maxHeight">Max. height</Label>
<Input id="maxHeight" defaultValue="none" />
</Fieldset>
</Flex>
<PopoverArrow />
<PopoverClose aria-label="Close">
<Cross2Icon />
</PopoverClose>
</PopoverContent>
</Popover>
);
export default PopoverDemo;

The steps above outline briefly what's involved in using a Radix Primitive in your application.

These components are low-level enough to give you control over how you want to wrap them. You're free to introduce your own high-level API to better suit the needs of your team and product.

In a few simple steps, we've implemented a fully accessible Popover component, without having to worry about many of its complexities.

  • Adheres to WAI-ARIA design pattern.

  • Can be controlled or uncontrolled.

  • Customize side, alignment, offsets, collision handling.

  • Optionally render a pointing arrow.

  • Focus is fully managed and customizable.

  • Dismissing and layering behavior is highly customizable.