07 Oct

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.

RemovedReplacementMigration
Drawer (@/components/ui/drawer, @intentui/drawer)SheetUse 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)DialogUse 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.

Purposev3v4
Dialog wrapperModalDialogTrigger
Dialog contentModalContentDialog
Sheet wrapperSheetSheetTrigger
Sheet contentSheetContentSheet
Menu wrapperMenuMenuTrigger
Menu contentMenuContentMenu
Tooltip wrapperTooltipTooltipTrigger
Tooltip contentTooltipContentTooltip
Preview wrapperPreviewPreviewTrigger
Preview contentPreviewContentPreview
Popover wrapperPopoverPopoverTrigger
Popover contentPopoverContentPopover
Context menu wrapperContextMenuMenuTrigger trigger="contextMenu"
Context menu contentContextMenuContentMenu

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.

  • start opens on the left in LTR layouts and on the right in RTL layouts.
  • end opens on the right in LTR layouts and on the left in RTL layouts.
  • center displays 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.

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.

v3v4
--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-bgbg-background
text-fgtext-foreground
text-muted-fgtext-muted-foreground
text-primary-fgtext-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/intentui

Choose the skills you need in the CLI:

  • intentui helps create, install, review, and debug components. It detects v3 or v4 from your installed source and follows the matching APIs and theme tokens.
  • intentui-design helps discover and integrate purchased Design Intent UI resources for v4. The authenticated @irsyad registry includes public components and premium resources, so licensed users need only this registry.
  • intentui-migrate helps 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 Drawer imports and installations with Sheet, and removed Modal imports and installations with Dialog.
  • Use PopoverTrigger as the wrapper around a Button and Popover, and move controlled state props to the wrapper.
  • Replace the old overlay wrappers with DialogTrigger, SheetTrigger, PopoverTrigger, MenuTrigger, or TooltipTrigger as appropriate. Set trigger="contextMenu" for context menus.
  • Replace opening button components with Button inside the trigger wrapper.
  • Rename ModalContent to Dialog and import it from @/components/ui/dialog. Rename SheetContent, PopoverContent, MenuContent, and ContextMenuContent to their component names.
  • Rename SheetContentProps to SheetProps and PopoverContentProps to PopoverProps in custom wrappers.
  • Migrate tooltips to TooltipTrigger with a Button and Tooltip. Rename TooltipContentProps to TooltipProps, and move timing and state props to TooltipTrigger.
  • Rename the Preview wrapper to PreviewTrigger and PreviewContent to Preview. Keep timing and disabled props on the wrapper.
  • Change Sheet's side prop to position.
  • Use the local DialogTrigger as a wrapper around a button and Dialog. 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.