Overview
Radix UI Primitives are completely unstyled by default . This gives you complete control over the styling without having to override opinionated design decisions.
You can style components using:
Plain CSS or CSS Modules
CSS-in-JS libraries (styled-components, Emotion, etc.)
Utility-first frameworks (Tailwind CSS)
Inline styles
Any other styling solution that works with React
The unstyled approach means you’re responsible for all visual styling, including layouts, colors, typography, animations, and responsive behavior.
Styling Approaches
Plain CSS
The simplest approach is using regular CSS classes:
import * as Switch from '@radix-ui/react-switch' ;
import './switch.css' ;
export function SwitchDemo () {
return (
< div className = "switch-container" >
< label className = "label" htmlFor = "airplane-mode" >
Airplane mode
</ label >
< Switch.Root className = "switch-root" id = "airplane-mode" >
< Switch.Thumb className = "switch-thumb" />
</ Switch.Root >
</ div >
);
}
CSS Modules
CSS Modules provide scoped styling:
Dialog.tsx
dialog.module.css
import * as Dialog from '@radix-ui/react-dialog' ;
import styles from './dialog.module.css' ;
export function DialogDemo () {
return (
< Dialog.Root >
< Dialog.Trigger className = { styles . trigger } >
Open Dialog
</ Dialog.Trigger >
< Dialog.Portal >
< Dialog.Overlay className = { styles . overlay } />
< Dialog.Content className = { styles . content } >
< Dialog.Title className = { styles . title } >
Edit Profile
</ Dialog.Title >
< Dialog.Description className = { styles . description } >
Make changes to your profile here.
</ Dialog.Description >
< Dialog.Close className = { styles . closeButton } >
Close
</ Dialog.Close >
</ Dialog.Content >
</ Dialog.Portal >
</ Dialog.Root >
);
}
Styled Components
Use styled-components or Emotion for CSS-in-JS:
import * as Dialog from '@radix-ui/react-dialog' ;
import styled from 'styled-components' ;
const StyledOverlay = styled ( Dialog . Overlay ) `
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
inset: 0;
animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
` ;
const StyledContent = styled ( Dialog . Content ) `
background-color: white;
border-radius: 8px;
box-shadow: 0 10px 38px -10px rgba(0, 0, 0, 0.35);
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90vw;
max-width: 450px;
padding: 25px;
` ;
const StyledTitle = styled ( Dialog . Title ) `
margin: 0;
font-weight: 600;
font-size: 17px;
` ;
export function DialogDemo () {
return (
< Dialog.Root >
< Dialog.Trigger > Open </ Dialog.Trigger >
< Dialog.Portal >
< StyledOverlay />
< StyledContent >
< StyledTitle > Edit Profile </ StyledTitle >
< Dialog.Close > Close </ Dialog.Close >
</ StyledContent >
</ Dialog.Portal >
</ Dialog.Root >
);
}
Tailwind CSS
Radix UI works excellently with Tailwind CSS:
import * as Accordion from '@radix-ui/react-accordion' ;
export function AccordionDemo () {
return (
< Accordion.Root
type = "single"
collapsible
className = "w-full max-w-md mx-auto"
>
< Accordion.Item value = "item-1" className = "border-b" >
< Accordion.Header >
< Accordion.Trigger className = "flex w-full items-center justify-between py-4 text-left font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180" >
What is Radix UI?
< svg
className = "h-4 w-4 transition-transform duration-200"
xmlns = "http://www.w3.org/2000/svg"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
>
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M19 9l-7 7-7-7" />
</ svg >
</ Accordion.Trigger >
</ Accordion.Header >
< Accordion.Content className = "overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down" >
< div className = "pb-4 pt-0" >
Radix UI is a collection of accessible, unstyled components for building high-quality design systems.
</ div >
</ Accordion.Content >
</ Accordion.Item >
</ Accordion.Root >
);
}
Use Tailwind’s data-* attribute selectors to style different component states: data-[state=open], data-[state=checked], etc.
Data Attributes
Radix components expose data attributes that you can use for styling:
State Attributes
Many components expose their state via data-state:
/* Switch states */
.switch-root [ data-state = 'checked' ] {
background-color : green ;
}
.switch-root [ data-state = 'unchecked' ] {
background-color : gray ;
}
/* Dialog states */
.dialog-content [ data-state = 'open' ] {
animation : slideIn 200 ms ;
}
.dialog-content [ data-state = 'closed' ] {
animation : slideOut 200 ms ;
}
/* Accordion states */
.accordion-trigger [ data-state = 'open' ] {
/* Styles for open state */
}
.accordion-trigger [ data-state = 'closed' ] {
/* Styles for closed state */
}
Disabled State
Components expose a data-disabled attribute when disabled:
.button [ data-disabled ] {
opacity : 0.5 ;
cursor : not-allowed ;
}
Orientation
Some components like Slider and Toolbar expose orientation:
.slider-root [ data-orientation = 'horizontal' ] {
width : 200 px ;
height : 20 px ;
}
.slider-root [ data-orientation = 'vertical' ] {
width : 20 px ;
height : 200 px ;
}
Side and Align
Positioned components like Tooltip and Popover expose side and alignment:
.tooltip-content [ data-side = 'top' ] {
/* Styles for top-positioned tooltip */
}
.tooltip-content [ data-side = 'bottom' ] {
/* Styles for bottom-positioned tooltip */
}
.popover-content [ data-align = 'start' ] {
/* Styles for start-aligned popover */
}
Animating with Data Attributes
You can create smooth animations using data attributes:
/* Accordion animation */
.accordion-content {
overflow : hidden ;
}
.accordion-content [ data-state = 'open' ] {
animation : slideDown 300 ms ease-out ;
}
.accordion-content [ data-state = 'closed' ] {
animation : slideUp 300 ms 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 ;
}
}
Some components provide CSS variables (like --radix-accordion-content-height) that you can use in your animations.
CSS Variables
Radix components expose CSS variables for dynamic values:
/* Collapsible content height */
.collapsible-content [ data-state = 'open' ] {
height : var ( --radix-collapsible-content-height );
}
/* Collapsible content width */
.collapsible-content [ data-state = 'open' ] {
width : var ( --radix-collapsible-content-width );
}
/* Navigation menu viewport */
.navigation-menu-viewport {
width : var ( --radix-navigation-menu-viewport-width );
height : var ( --radix-navigation-menu-viewport-height );
}
Styling Best Practices
Start with layout and structure
First, establish the basic layout and positioning using CSS or your preferred framework.
Add interactive states
Use data attributes to style hover, focus, active, and disabled states.
Implement animations
Add smooth transitions and animations for state changes using data attributes.
Ensure accessibility
Test with keyboard navigation and screen readers. Make sure focus indicators are visible.
Make it responsive
Use media queries or responsive utilities to adapt to different screen sizes.
Common Patterns
Focus Visible
Style focus states for keyboard navigation:
.button:focus-visible {
outline : 2 px solid blue ;
outline-offset : 2 px ;
}
Hover and Active States
.button:hover {
background-color : #2a3333 ;
}
.button:active {
transform : scale ( 0.98 );
}
Responsive Design
.dialog-content {
width : 90 vw ;
max-width : 450 px ;
}
@media ( max-width : 640 px ) {
.dialog-content {
width : 95 vw ;
padding : 20 px ;
}
}
Examples by Framework
import * as Dialog from '@radix-ui/react-dialog' ;
export function DialogDemo () {
return (
< Dialog.Root >
< Dialog.Trigger className = "bg-black text-white px-4 py-2 rounded hover:bg-gray-800" >
Open Dialog
</ Dialog.Trigger >
< Dialog.Portal >
< Dialog.Overlay className = "fixed inset-0 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out" />
< Dialog.Content className = "fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg p-6 shadow-xl max-w-md w-[90vw]" >
< Dialog.Title className = "text-xl font-semibold mb-2" >
Edit Profile
</ Dialog.Title >
< Dialog.Description className = "text-gray-600 mb-4" >
Make changes to your profile here.
</ Dialog.Description >
< Dialog.Close className = "absolute top-4 right-4 text-gray-400 hover:text-gray-600" >
×
</ Dialog.Close >
</ Dialog.Content >
</ Dialog.Portal >
</ Dialog.Root >
);
}
import * as Switch from '@radix-ui/react-switch' ;
import styles from './switch.module.css' ;
export function SwitchDemo () {
return (
< Switch.Root className = { styles . root } >
< Switch.Thumb className = { styles . thumb } />
</ Switch.Root >
);
}
.root {
width : 42 px ;
height : 25 px ;
background-color : var ( --gray-400 );
border-radius : 9999 px ;
position : relative ;
}
.root [ data-state = 'checked' ] {
background-color : var ( --green-500 );
}
.thumb {
display : block ;
width : 21 px ;
height : 21 px ;
background-color : white ;
border-radius : 9999 px ;
transition : transform 100 ms ;
}
.thumb [ data-state = 'checked' ] {
transform : translateX ( 19 px );
}
Design System Integration
Radix UI Primitives work great as the foundation for design systems:
Create styled wrappers - Wrap Radix components with your design system’s styling
Use design tokens - Apply your design tokens (colors, spacing, typography) to Radix components
Document patterns - Create a pattern library showing how to use styled components
Share across teams - Distribute your styled components as a package
When creating wrapper components, make sure to forward all props and refs correctly to maintain accessibility features.
Next Steps
Composition Learn about the composable API design
Accessibility Understand accessibility features
Components Browse all available components
Examples See Radix Themes for pre-styled examples