Radix UI Primitives are written in TypeScript and provide comprehensive type definitions. All components export their prop types, making it easy to extend and compose them in a type-safe manner.
Type Exports
Each component package exports TypeScript types for all its parts:
import * as Dialog from '@radix-ui/react-dialog';
import type {
DialogProps,
DialogTriggerProps,
DialogContentProps,
DialogTitleProps,
DialogDescriptionProps,
DialogCloseProps,
} from '@radix-ui/react-dialog';
Component Type Inference
Radix components use React.forwardRef with proper typing. You can infer element types using React’s utility types:
import * as Switch from '@radix-ui/react-switch';
import { ComponentPropsWithoutRef, ElementRef } from 'react';
// Get the element type
type SwitchElement = ElementRef<typeof Switch.Root>;
// Result: HTMLButtonElement
// Get the props type
type SwitchProps = ComponentPropsWithoutRef<typeof Switch.Root>;
// Result: SwitchProps including all button props
Extending Component Props
You can extend Radix component props to create your own typed components:
import * as Dialog from '@radix-ui/react-dialog';
import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react';
interface MyDialogProps extends ComponentPropsWithoutRef<typeof Dialog.Content> {
title: string;
description?: string;
children: React.ReactNode;
}
const MyDialog = forwardRef<
ElementRef<typeof Dialog.Content>,
MyDialogProps
>(({ title, description, children, ...props }, ref) => (
<Dialog.Portal>
<Dialog.Overlay className="overlay" />
<Dialog.Content ref={ref} {...props}>
<Dialog.Title>{title}</Dialog.Title>
{description && <Dialog.Description>{description}</Dialog.Description>}
{children}
<Dialog.Close>Close</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
));
MyDialog.displayName = 'MyDialog';
Generic Props
Some components like Accordion have discriminated union types for their props:
import * as Accordion from '@radix-ui/react-accordion';
// Single mode - value is a string
function SingleAccordion() {
return (
<Accordion.Root
type="single"
collapsible
defaultValue="item-1" // string
onValueChange={(value) => {
// value is string
console.log(value);
}}
>
{/* items */}
</Accordion.Root>
);
}
// Multiple mode - value is string[]
function MultipleAccordion() {
return (
<Accordion.Root
type="multiple"
defaultValue={["item-1", "item-2"]} // string[]
onValueChange={(value) => {
// value is string[]
console.log(value);
}}
>
{/* items */}
</Accordion.Root>
);
}
The type prop determines the types of value, defaultValue, and the onValueChange callback parameter.
Controlled vs Uncontrolled Types
Radix components support both controlled and uncontrolled usage with appropriate types:
import * as Collapsible from '@radix-ui/react-collapsible';
import { useState } from 'react';
// Uncontrolled
function UncontrolledCollapsible() {
return (
<Collapsible.Root defaultOpen={false}>
{/* content */}
</Collapsible.Root>
);
}
// Controlled
function ControlledCollapsible() {
const [open, setOpen] = useState(false);
return (
<Collapsible.Root
open={open}
onOpenChange={setOpen}
>
{/* content */}
</Collapsible.Root>
);
}
Ref Types
All Radix components properly forward refs with correct types:
import * as Dialog from '@radix-ui/react-dialog';
import { useRef } from 'react';
function MyComponent() {
// Type is inferred as React.RefObject<HTMLDivElement>
const contentRef = useRef<React.ElementRef<typeof Dialog.Content>>(null);
return (
<Dialog.Root>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Content ref={contentRef}>
Content
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
Primitive Component Types
Radix uses a Primitive component system. Each primitive extends native HTML element props:
import { Primitive } from '@radix-ui/react-primitive';
import { ComponentPropsWithoutRef } from 'react';
// These are equivalent:
type ButtonProps = ComponentPropsWithoutRef<typeof Primitive.button>;
type ButtonProps2 = React.ButtonHTMLAttributes<HTMLButtonElement>;
Type-Safe Event Handlers
Event handlers maintain proper types:
import * as Switch from '@radix-ui/react-switch';
function MySwitch() {
return (
<Switch.Root
onCheckedChange={(checked) => {
// checked is boolean
console.log(checked);
}}
onClick={(event) => {
// event is React.MouseEvent<HTMLButtonElement>
console.log(event.currentTarget);
}}
>
<Switch.Thumb />
</Switch.Root>
);
}
Creating Wrapper Libraries
Build type-safe wrapper components:
Define base component types
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react';
type DialogContentElement = ElementRef<typeof DialogPrimitive.Content>;
type DialogContentProps = ComponentPropsWithoutRef<typeof DialogPrimitive.Content>;
Extend with custom props
interface MyDialogContentProps extends DialogContentProps {
/** Custom prop for styling */
variant?: 'default' | 'large';
/** Custom prop for behavior */
closeOnOverlayClick?: boolean;
}
Create typed component
const DialogContent = forwardRef<DialogContentElement, MyDialogContentProps>(
({ variant = 'default', closeOnOverlayClick = true, ...props }, ref) => {
return (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay />
<DialogPrimitive.Content
ref={ref}
onInteractOutside={(e) => {
if (!closeOnOverlayClick) e.preventDefault();
}}
className={`dialog-content dialog-content--${variant}`}
{...props}
/>
</DialogPrimitive.Portal>
);
}
);
DialogContent.displayName = 'DialogContent';
AsChild Prop Type
The asChild prop allows component composition with proper typing:
import * as Dialog from '@radix-ui/react-dialog';
import { motion } from 'framer-motion';
function AnimatedDialog() {
return (
<Dialog.Root>
<Dialog.Trigger asChild>
<button className="custom-button">
Open
</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Content asChild>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
Content
</motion.div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
When using asChild, the Radix component merges its props with the child component’s props.
Common Type Patterns
Discriminated Unions
import type { AccordionSingleProps, AccordionMultipleProps } from '@radix-ui/react-accordion';
// Type is automatically discriminated by the 'type' property
type AccordionProps = AccordionSingleProps | AccordionMultipleProps;
function handleAccordion(props: AccordionProps) {
if (props.type === 'single') {
// props.value is string | undefined
// props.onValueChange is (value: string) => void
} else {
// props.value is string[] | undefined
// props.onValueChange is (value: string[]) => void
}
}
Polymorphic Components
import { forwardRef } from 'react';
import { Primitive } from '@radix-ui/react-primitive';
type ButtonProps<T extends React.ElementType = 'button'> = {
as?: T;
} & React.ComponentPropsWithoutRef<T>;
const Button = forwardRef(
<T extends React.ElementType = 'button'>(
{ as, ...props }: ButtonProps<T>,
ref: React.Ref<Element>
) => {
const Component = as || 'button';
return <Component ref={ref} {...props} />;
}
);
Troubleshooting
Type Errors with Refs
If you get type errors with refs, ensure you’re using the correct type:
import * as Dialog from '@radix-ui/react-dialog';
import { ElementRef, useRef } from 'react';
// ✅ Correct
const ref = useRef<ElementRef<typeof Dialog.Content>>(null);
// ❌ Incorrect
const ref = useRef<HTMLDivElement>(null);
Type Inference Issues
If TypeScript can’t infer types properly, explicitly annotate them:
import * as Accordion from '@radix-ui/react-accordion';
const MyAccordion = () => {
return (
<Accordion.Root<'single'> type="single" collapsible>
{/* content */}
</Accordion.Root>
);
};
Strict Mode
Radix Primitives work with TypeScript’s strict mode enabled:
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}
Type Utilities
Useful type utilities when working with Radix:
import { ComponentPropsWithoutRef, ElementRef, ComponentProps } from 'react';
import * as Dialog from '@radix-ui/react-dialog';
// Extract element type
type DialogElement = ElementRef<typeof Dialog.Content>;
// Extract props without ref
type DialogProps = ComponentPropsWithoutRef<typeof Dialog.Content>;
// Extract props with ref
type DialogPropsWithRef = ComponentProps<typeof Dialog.Content>;
// Extract specific prop types
type OnOpenChange = NonNullable<DialogProps['onOpenChange']>;
// Result: (open: boolean) => void