This guide covers strategies for upgrading Radix UI Primitives and handling breaking changes between versions.
Version Strategy
Radix UI follows semantic versioning:
- Major versions (1.x → 2.x): Breaking changes that may require code updates
- Minor versions (1.1 → 1.2): New features, backward compatible
- Patch versions (1.1.1 → 1.1.2): Bug fixes, backward compatible
Radix Primitives is currently in v1.x and maintains a strong commitment to stability. Breaking changes are rare and well-documented.
General Upgrade Process
Check the changelog
Review the changelog for the component you’re upgrading:# View changelogs in the repository
https://github.com/radix-ui/primitives/blob/main/packages/react/[component]/CHANGELOG.md
Update dependencies
Update to the latest version:npm install @radix-ui/react-dialog@latest
# or update multiple packages
npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-popover
Run TypeScript checks
TypeScript will catch many breaking changes: Test your components
Run your test suite and manually test components:
Common Migration Patterns
Updating Multiple Packages
When upgrading, it’s often best to update all Radix packages together since they share internal dependencies:
# Update all @radix-ui packages to latest
npm update @radix-ui/*
# Or use a tool like npm-check-updates
npx npm-check-updates "/@radix-ui/" -u
npm install
Handling Breaking Changes
API Prop Changes
If a prop is renamed or removed:
// Before (v1.0)
<Dialog.Root open={open} onOpenChange={setOpen}>
{/* content */}
</Dialog.Root>
// After (hypothetical v2.0 with renamed prop)
<Dialog.Root isOpen={open} onIsOpenChange={setOpen}>
{/* content */}
</Dialog.Root>
Create a wrapper to ease migration:
import * as DialogPrimitive from '@radix-ui/react-dialog';
interface DialogProps {
open?: boolean; // Old API
onOpenChange?: (open: boolean) => void; // Old API
children: React.ReactNode;
}
// Adapter component for gradual migration
function Dialog({ open, onOpenChange, children }: DialogProps) {
return (
<DialogPrimitive.Root isOpen={open} onIsOpenChange={onOpenChange}>
{children}
</DialogPrimitive.Root>
);
}
Component Structure Changes
If component structure changes:
// Before: Content includes Overlay
<Dialog.Root>
<Dialog.Content>
{/* content */}
</Dialog.Content>
</Dialog.Root>
// After: Overlay is separate
<Dialog.Root>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
{/* content */}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
Type Changes
TypeScript types may change between versions:
import { ComponentPropsWithoutRef, ElementRef } from 'react';
import * as Dialog from '@radix-ui/react-dialog';
// Use utility types to adapt to changes
type DialogContentProps = ComponentPropsWithoutRef<typeof Dialog.Content>;
type DialogContentElement = ElementRef<typeof Dialog.Content>;
// Your wrapper component will automatically adapt
const MyDialogContent = React.forwardRef<
DialogContentElement,
DialogContentProps
>((props, ref) => <Dialog.Content ref={ref} {...props} />);
Migration Checklist
When upgrading major versions:
Read release notes
Check the release notes and changelog:
- Breaking changes
- Deprecated features
- New features
- Bug fixes
Update peer dependencies
Ensure React version compatibility:{
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
Search for deprecated usage
# Search your codebase for deprecated patterns
grep -r "deprecated-prop" src/
Update styles
Check if CSS selectors or data attributes have changed:/* Update data-* attribute selectors if needed */
[data-state="open"] { /* ... */ }
[data-radix-dialog-content] { /* ... */ }
Test accessibility
Ensure accessibility features still work:
- Keyboard navigation
- Screen reader announcements
- Focus management
- ARIA attributes
Backwards Compatibility
Create compatibility layers for gradual migration:
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react';
// V1 compatible wrapper
export const Dialog = DialogPrimitive.Root;
export const DialogTrigger = DialogPrimitive.Trigger;
// Automatically include Portal and Overlay for v1 behavior
export const DialogContent = forwardRef<
ElementRef<typeof DialogPrimitive.Content>,
ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>((props, ref) => (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className="dialog-overlay" />
<DialogPrimitive.Content ref={ref} {...props} />
</DialogPrimitive.Portal>
));
DialogContent.displayName = 'DialogContent';
Use this wrapper during migration:
// Import from your compatibility layer instead of directly from Radix
import { Dialog, DialogTrigger, DialogContent } from '@/components/compat/dialog';
function MyDialog() {
return (
<Dialog>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>
{/* Works with v1 code style */}
</DialogContent>
</Dialog>
);
}
Codemods
For large codebases, consider creating codemods to automate migrations:
// Example codemod using jscodeshift
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
// Find all Dialog.Content and wrap with Portal
root
.find(j.JSXElement, {
openingElement: {
name: { object: { name: 'Dialog' }, property: { name: 'Content' } },
},
})
.forEach((path) => {
// Transformation logic here
});
return root.toSource();
};
Handling Deprecation Warnings
Radix may mark features as deprecated before removing them:
import * as Dialog from '@radix-ui/react-dialog';
// If you see deprecation warnings in console
function MyDialog() {
return (
<Dialog.Root>
{/* ⚠️ Deprecated prop usage */}
<Dialog.Content deprecatedProp="value">
Content
</Dialog.Content>
</Dialog.Root>
);
}
// Fix by using the new API
function MyDialogFixed() {
return (
<Dialog.Root>
<Dialog.Content newProp="value">
Content
</Dialog.Content>
</Dialog.Root>
);
}
Address deprecation warnings as soon as possible. Deprecated features will be removed in the next major version.
Testing After Migration
Unit Tests
import { render, screen } from '@testing-library/react';
import { Dialog } from './dialog';
test('dialog opens and closes', () => {
render(
<Dialog>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Content>
<Dialog.Title>Title</Dialog.Title>
</Dialog.Content>
</Dialog>
);
// Test component behavior
const trigger = screen.getByText('Open');
expect(trigger).toBeInTheDocument();
});
Visual Regression Tests
Use tools like Playwright or Chromatic to catch visual changes:
import { test, expect } from '@playwright/test';
test('dialog appears correctly', async ({ page }) => {
await page.goto('/dialog-example');
await page.click('button:has-text("Open")');
// Visual snapshot
await expect(page).toHaveScreenshot('dialog-open.png');
});
Getting Help
If you encounter issues during migration:
Ask the community
Join discussions on GitHub Discussions or Discord
Report bugs
If you find a bug, report it with a minimal reproduction
Version-Specific Guides
Upgrading from v0.x to v1.x
Major changes in v1.0:
- Improved TypeScript types
- Better SSR support
- Refined API surface
- Enhanced accessibility
Staying Current
To stay informed about updates:
- Watch the GitHub repository
- Follow release notes
- Check the changelog regularly
- Subscribe to the Radix blog
# Enable GitHub notifications for releases only
# Go to: https://github.com/radix-ui/primitives
# Click "Watch" → "Custom" → "Releases"