Release 4.0.0
Intent UI v4 introduces clearer overlay composition, new sheet positions, and explicit theme token names. Here's what changed and how to migrate from v3.
Introduction
Intent UI v4 brings a more consistent way to compose overlays and menus. The trigger wrapper manages the interaction, a button opens it, and the component itself renders the content.
This release includes breaking changes. If you're moving from v3, update both your imports and JSX structure, along with your theme tokens and custom styles.
Component changes
Removed components
Drawer and Modal have been removed in v4, along with their component files, documentation pages, examples, and registry entries. Update existing imports and CLI installation commands to use their replacements.
| Removed | Replacement | Migration |
|---|---|---|
Drawer (@/components/ui/drawer, @intentui/drawer) | Sheet | Use SheetTrigger with a Button and Sheet. Choose the opening edge with position, and use SheetHeader, SheetBody, SheetFooter, and SheetClose for composition. |
Modal (@/components/ui/modal, @intentui/modal) | Dialog | Use DialogTrigger with a Button and Dialog. Rename ModalHeader, ModalTitle, ModalDescription, ModalBody, ModalFooter, and ModalClose to their Dialog equivalents. |
Dialog now includes its backdrop and overlay surface. If you previously used the Intent UI Dialog as a content primitive inside another overlay, remove that wrapper or import the content-only Dialog from react-aria-components/Dialog.
PopoverTrigger now wraps a Button and Popover as shown in the Popover documentation. It is an alias of DialogTrigger.
A consistent composition pattern
In v3, names like Modal and Menu referred to the wrapper, while ModalContent and MenuContent rendered the content. In v4, the wrapper uses the Trigger suffix, and the content uses the component name directly.
| Purpose | v3 | v4 |
|---|---|---|
| Dialog wrapper | Modal | DialogTrigger |
| Dialog content | ModalContent | Dialog |
| Sheet wrapper | Sheet | SheetTrigger |
| Sheet content | SheetContent | Sheet |
| Menu wrapper | Menu | MenuTrigger |
| Menu content | MenuContent | Menu |
| Tooltip wrapper | Tooltip | TooltipTrigger |
| Tooltip content | TooltipContent | Tooltip |
| Preview wrapper | Preview | PreviewTrigger |
| Preview content | PreviewContent | Preview |
| Popover wrapper | Popover | PopoverTrigger |
| Popover content | PopoverContent | Popover |
| Context menu wrapper | ContextMenu | MenuTrigger trigger="contextMenu" |
| Context menu content | ContextMenuContent | Menu |
The old ModalTrigger, SheetTrigger, PopoverTrigger, MenuTrigger, and TooltipTrigger button components are no longer used as buttons. Use Button inside the new trigger wrapper for dialogs, sheets, popovers, menus, and tooltips. Move button props such as intent, className, and isDisabled to that button.
Composing overlay content
The Intent UI Dialog now includes its backdrop and modal surface. Use it directly for centered dialogs. Sheet and Popover provide their own overlay surfaces and share the DialogHeader, DialogTitle, DialogDescription, DialogBody, and DialogFooter building blocks.
Do not place the Intent UI Dialog inside a Sheet or Popover. For custom overlays that need only the accessible content primitive, import Dialog from react-aria-components/Dialog.
Dialog
DialogTrigger now wraps the opening button and Dialog. The Intent UI Modal component has been removed; import Dialog from @/components/ui/dialog. Props that previously belonged to ModalContent, such as size, role, isDismissable, and closeButton, now belong to Dialog.
Before (v3)
import {
Modal,
ModalContent,
ModalHeader,
ModalTitle,
ModalTrigger,
} from "@/components/ui/modal"
<Modal>
<ModalTrigger>Rename project</ModalTrigger>
<ModalContent size="lg">
<ModalHeader>
<ModalTitle>Rename project</ModalTitle>
</ModalHeader>
</ModalContent>
</Modal>After (v4)
import { Button } from "react-aria-components/Button"
import {
Dialog,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
<DialogTrigger>
<Button>Rename project</Button>
<Dialog size="lg">
<DialogHeader>
<DialogTitle>Rename project</DialogTitle>
</DialogHeader>
</Dialog>
</DialogTrigger>Rename ModalHeader, ModalBody, ModalFooter, and ModalClose to DialogHeader, DialogBody, DialogFooter, and DialogClose. The render prop for accessing close still works inside Dialog.
For a controlled dialog without a trigger wrapper, replace ModalContent with Dialog and keep your state props on it:
const [isOpen, setIsOpen] = useState(false)
<Dialog isOpen={isOpen} onOpenChange={setIsOpen}>
<DialogHeader>
<DialogTitle>Rename project</DialogTitle>
</DialogHeader>
</Dialog>If you're controlling an overlay through its trigger wrapper, keep isOpen and onOpenChange on the new wrapper instead.
Read the Dialog documentation for complete examples.
Sheet
Sheets follow the same structure: SheetTrigger wraps a Button and Sheet. The side prop is now called position.
Before (v3)
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
<Sheet>
<SheetTrigger>Edit settings</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Settings</SheetTitle>
</SheetHeader>
</SheetContent>
</Sheet>After (v4)
import { Button } from "react-aria-components/Button"
import {
Sheet,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
<SheetTrigger>
<Button>Edit settings</Button>
<Sheet position="right">
<SheetHeader>
<SheetTitle>Settings</SheetTitle>
</SheetHeader>
</Sheet>
</SheetTrigger>Alongside top, bottom, left, and right, position now supports start, end, and center. The default is still right.
startopens on the left in LTR layouts and on the right in RTL layouts.endopens on the right in LTR layouts and on the left in RTL layouts.centerdisplays the sheet in the center with a zoom animation.
Use start or end when the sheet should follow the locale's reading direction. isFloat is still available, and sheets now fade out when closing.
Read the Sheet documentation for position and controlled examples.
Popover
PopoverTrigger wraps the opening button and Popover. Move state props such as isOpen, defaultOpen, and onOpenChange to PopoverTrigger, and move content props to Popover. Update the imports from @/components/ui/popover along with the JSX.
Popover: v3 → v4
// Before
<Popover>
<Button>Details</Button>
<PopoverContent>
<PopoverHeader>
<PopoverTitle>Project details</PopoverTitle>
</PopoverHeader>
</PopoverContent>
</Popover>
// After
import { Button } from "react-aria-components/Button"
import { Popover, PopoverHeader, PopoverTitle, PopoverTrigger } from "@/components/ui/popover"
<PopoverTrigger>
<Button>Details</Button>
<Popover>
<PopoverHeader>
<PopoverTitle>Project details</PopoverTitle>
</PopoverHeader>
</Popover>
</PopoverTrigger>Read the Popover documentation for full examples.
Menu
MenuTrigger now manages the menu interaction, and Menu renders the menu and its popover. Replace the old trigger button with Button.
Before (v3)
import { Menu, MenuContent, MenuItem, MenuTrigger } from "@/components/ui/menu"
<Menu>
<MenuTrigger>Actions</MenuTrigger>
<MenuContent placement="bottom start">
<MenuItem id="edit">Edit</MenuItem>
<MenuItem id="delete" intent="danger">Delete</MenuItem>
</MenuContent>
</Menu>After (v4)
import { Button } from "react-aria-components/Button"
import { Menu, MenuItem, MenuTrigger } from "@/components/ui/menu"
<MenuTrigger>
<Button>Actions</Button>
<Menu placement="bottom start">
<MenuItem id="edit">Edit</MenuItem>
<MenuItem id="delete" intent="danger">Delete</MenuItem>
</Menu>
</MenuTrigger>Move content props such as items, selectionMode, selectedKeys, and onAction from MenuContent to Menu. Popover customization remains available through the popover prop. MenuItem, MenuSection, and MenuSubMenu keep their names.
For custom menu styling and TypeScript imports, menuContentStyles becomes menuStyles, and MenuContentProps becomes MenuProps. The local button type MenuTriggerProps has been removed; use ButtonProps for your button or infer wrapper props with React.ComponentProps<typeof MenuTrigger>.
Read the Menu documentation for selection, sections, and submenus.
Context menu
Context menu uses MenuTrigger with trigger="contextMenu". Import the components from @/components/ui/menu.
Context menu: v3 → v4
// Before
<ContextMenu>
<Button>Right-click here</Button>
<ContextMenuContent>
<ContextMenuItem id="edit">Edit</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
// After
import { Menu, MenuItem, MenuTrigger } from "@/components/ui/menu"
<MenuTrigger trigger="contextMenu">
<Button>Right-click here</Button>
<Menu>
<MenuItem id="edit">Edit</MenuItem>
</Menu>
</MenuTrigger>Read the Context Menu documentation for full examples.
Tooltip
TooltipTrigger now wraps a focusable button and Tooltip, which renders the content. The old TooltipContent export has been removed, and TooltipTrigger no longer renders a button.
Before (v3)
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
<Tooltip delay={0}>
<TooltipTrigger>Duplicate</TooltipTrigger>
<TooltipContent placement="top" inverse>
Duplicate project
</TooltipContent>
</Tooltip>After (v4)
import { Button } from "react-aria-components/Button"
import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip"
<TooltipTrigger delay={0}>
<Button>Duplicate</Button>
<Tooltip placement="top" inverse>
Duplicate project
</Tooltip>
</TooltipTrigger>Move timing and state props such as delay, closeDelay, isOpen, defaultOpen, and onOpenChange from the old Tooltip wrapper to TooltipTrigger. Move button props to Button, and content props such as placement, offset, arrow, inverse, and className from TooltipContent to Tooltip.
For TypeScript imports, rename TooltipContentProps to TooltipProps. The old wrapper's TooltipProps type now describes the content; infer wrapper props with React.ComponentProps<typeof TooltipTrigger>.
Read the Tooltip documentation for placement, appearance, and delay examples.
Preview
PreviewTrigger now wraps a focusable link or button and Preview, which renders the popover content. Rename the old Preview wrapper to PreviewTrigger and PreviewContent to Preview.
import { Link } from "@/components/ui/link"
import { Preview, PreviewTrigger } from "@/components/ui/preview"
// Before (v3)
<Preview delay={0}>
<Link href="#">View issue</Link>
<PreviewContent>
<p>Issue details</p>
</PreviewContent>
</Preview>
// After (v4)
<PreviewTrigger delay={0}>
<Link href="#">View issue</Link>
<Preview>
<p>Issue details</p>
</Preview>
</PreviewTrigger>Keep delay, closeDelay, and isDisabled on PreviewTrigger. Move content props such as placement, arrow, and className from PreviewContent to Preview. Custom triggers can still use React Aria's Focusable component.
Read the Preview documentation for complete examples.
Theme and styling
Card spacing
Card now uses --card-spacing instead of --gutter for its padding and gaps. Rename custom spacing overrides on Card and styles that read the card's spacing.
// Before (v3)
<Card className="[--gutter:--spacing(4)]" />
// After (v4)
<Card className="[--card-spacing:--spacing(4)]" />The default remains --spacing(6). Table still uses --gutter; when a table sits inside a card, set its gutter explicitly to the card spacing.
<Card>
<CardHeader title="Users" />
<CardContent>
<Table bleed className="[--gutter:var(--card-spacing)]" aria-label="Users">
{/* Table header and body */}
</Table>
</CardContent>
</Card>See Card for spacing customization.
Explicit theme token names
Theme tokens now use background and foreground instead of the abbreviated bg and fg names. Update your CSS variables, Tailwind theme mappings, and utility classes together, including dark mode overrides.
| v3 | v4 |
|---|---|
--bg | --background |
--fg | --foreground |
--color-bg | --color-background |
--color-fg | --color-foreground |
--card-fg | --card-foreground |
--primary-fg | --primary-foreground |
--primary-subtle-fg | --primary-subtle-foreground |
--secondary-fg | --secondary-foreground |
--accent-fg | --accent-foreground |
--muted-fg | --muted-foreground |
--overlay-fg | --overlay-foreground |
--navbar-fg | --navbar-foreground |
--sidebar-fg | --sidebar-foreground |
--sidebar-primary-fg | --sidebar-primary-foreground |
--sidebar-accent-fg | --sidebar-accent-foreground |
bg-bg | bg-background |
text-fg | text-foreground |
text-muted-fg | text-muted-foreground |
text-primary-fg | text-primary-foreground |
The same -fg to -foreground rename applies to the other semantic foreground tokens, including card, secondary, accent, status colors, navbar, and sidebar. Tailwind mappings such as --color-primary-fg also become --color-primary-foreground.
CSS variables and Tailwind mappings
Rename both the variable definitions and every var(...) reference. Keep your existing color values when migrating a customized theme. The following is a minimal example; apply the same changes to the rest of your theme and its .dark overrides.
Before (v3)
:root {
--bg: oklch(1 0 0);
--fg: oklch(0.21 0.006 285.885);
--primary-fg: oklch(1 0 0);
}
@theme inline {
--color-bg: var(--bg);
--color-fg: var(--fg);
--color-primary-fg: var(--primary-fg);
}
body {
background-color: var(--bg);
color: var(--fg);
}After (v4)
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(1 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary-foreground: var(--primary-foreground);
}
body {
background-color: var(--background);
color: var(--foreground);
}Status foreground tokens follow the same rule: --success-fg, --danger-fg, and --warning-fg become --success-foreground, --danger-foreground, and --warning-foreground. Their subtle variants also change, including --info-subtle-fg → --info-subtle-foreground.
Utility classes
The Tailwind mappings generate the new utility names. Update variants and opacity modifiers too: dark:bg-bg becomes dark:bg-background, and text-fg/80 becomes text-foreground/80.
// Before
<div className="bg-bg text-fg">
<p className="text-muted-fg">Project details</p>
</div>
// After
<div className="bg-background text-foreground">
<p className="text-muted-foreground">Project details</p>
</div>Only the token names change here. Tailwind utility prefixes such as bg- stay the same: bg-primary still uses the primary background token.
Upgrade with Agent Skills
Intent UI now includes official Agent Skills to help you develop components, use premium resources, and upgrade an existing application. Install them from the main repository:
npx skills add irsyadadl/intentuiChoose the skills you need in the CLI:
intentuihelps create, install, review, and debug components. It detects v3 or v4 from your installed source and follows the matching APIs and theme tokens.intentui-designhelps discover and integrate purchased Design Intent UI resources for v4. The authenticated@irsyadregistry includes public components and premium resources, so licensed users need only this registry.intentui-migratehelps audit and migrate v3 components and themes to v4. It can handle one feature or an explicitly requested application-wide upgrade, preserving local customizations and checking imports, dependencies, and behavior.
To plan an upgrade, ask your agent: "Audit my Intent UI v3 project for a v4 upgrade without modifying files." When you're ready, ask it to migrate a specific feature or the application. Ordinary component development does not trigger an automatic upgrade.
The migration skill covers the component and theme changes described above. Review its proposed changes and verify keyboard navigation, focus restoration, dismissal, and touch interactions after migrating.
See Working with AI for skill setup and registry integration.
Migration checklist
- Update your local component source and theme CSS together, preserving any customizations.
- Replace removed
Drawerimports and installations withSheet, and removedModalimports and installations withDialog. - Use
PopoverTriggeras the wrapper around aButtonandPopover, and move controlled state props to the wrapper. - Replace the old overlay wrappers with
DialogTrigger,SheetTrigger,PopoverTrigger,MenuTrigger, orTooltipTriggeras appropriate. Settrigger="contextMenu"for context menus. - Replace opening button components with
Buttoninside the trigger wrapper. - Rename
ModalContenttoDialogand import it from@/components/ui/dialog. RenameSheetContent,PopoverContent,MenuContent, andContextMenuContentto their component names. - Rename
SheetContentPropstoSheetPropsandPopoverContentPropstoPopoverPropsin custom wrappers. - Migrate tooltips to
TooltipTriggerwith aButtonandTooltip. RenameTooltipContentPropstoTooltipProps, and move timing and state props toTooltipTrigger. - Rename the
Previewwrapper toPreviewTriggerandPreviewContenttoPreview. Keep timing and disabled props on the wrapper. - Change
Sheet'ssideprop toposition. - Use the local
DialogTriggeras a wrapper around a button andDialog. Update custom overlay composition to avoid nesting overlay surfaces. - Update abbreviated theme tokens and utility classes in custom components and styles.
- Check controlled overlays, nested menus, keyboard navigation, focus restoration, and RTL sheet placement after migrating.
Since several existing names now have different roles, avoid a global search-and-replace. Update each wrapper, button, and content element together using the examples above.