Skip to main content
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:
1

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>;
2

Extend with custom props

interface MyDialogContentProps extends DialogContentProps {
  /** Custom prop for styling */
  variant?: 'default' | 'large';
  /** Custom prop for behavior */
  closeOnOverlayClick?: boolean;
}
3

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