Base UI React
Status: Beta (v1.0.0-beta.4) - Stable v1.0 expected Q4 2025 Last Updated: 2025-11-07 Dependencies: React 19+, Vite (recommended), Tailwind v4 (recommended) Latest Versions: @base-ui-components/react@1.0.0-beta.4
⚠️ Important Beta Status Notice
Base UI is currently in beta. Before using in production:
- ✅ Stable: Core components (Dialog, Popover, Tooltip, Select, Accordion) are production-ready
- ⚠️ API May Change: Minor breaking changes possible before v1.0 (Q4 2025)
- ✅ Production Tested: Used in real projects with documented workarounds
- ⚠️ Known Issues: 10+ documented issues with solutions in this skill
- ✅ Migration Path: Clear migration guide from Radix UI included
Recommendation: Use for new projects comfortable with beta software. Wait for v1.0 for critical production apps.
Quick Start (5 Minutes)
1. Install Base UI
pnpm add @base-ui-components/react
Why this matters:
- Single package contains all 27+ accessible components
- No peer dependencies besides React
- Tree-shakeable - only import what you need
- Works with any styling solution (Tailwind, CSS Modules, Emotion, etc.)
2. Use Your First Component
// src/App.tsx
import { Dialog } from "@base-ui-components/react/dialog";
export function App() {
return (
<Dialog.Root>
{/* Render prop pattern - Base UI's key feature */}
<Dialog.Trigger
render={(props) => (
<button {...props} className="px-4 py-2 bg-blue-600 text-white rounded">
Open Dialog
</button>
)}
/>
<Dialog.Portal>
<Dialog.Backdrop
render={(props) => (
<div {...props} className="fixed inset-0 bg-black/50" />
)}
/>
<Dialog.Popup
render={(props) => (
<div
{...props}
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg shadow-xl p-6"
>
<Dialog.Title render={(titleProps) => (
<h2 {...titleProps} className="text-2xl font-bold mb-4">
Dialog Title
</h2>
)} />
<Dialog.Description render={(descProps) => (
<p {...descProps} className="text-gray-600 mb-6">
This is a Base UI dialog. Fully accessible, fully styled by you.
</p>
)} />
<Dialog.Close render={(closeProps) => (
<button {...closeProps} className="px-4 py-2 border rounded">
Close
</button>
)} />
</div>
)}
/>
</Dialog.Portal>
</Dialog.Root>
);
}
CRITICAL:
- ✅ Always spread
{...props}from render functions - ✅ Use
<Dialog.Portal>to render outside DOM hierarchy - ✅
BackdropandPopupare separate components (unlike Radix's combinedOverlay + Content)
3. Components with Positioning (Select, Popover, Tooltip)
For components that need smart positioning, wrap in Positioner:
import { Popover } from "@base-ui-components/react/popover";
<Popover.Root>
<Popover.Trigger
render={(props) => <button {...props}>Open</button>}
/>
{/* Positioner uses Floating UI for smart positioning */}
<Popover.Positioner
side="top" // top, right, bottom, left
alignment="center" // start, center, end
sideOffset={8}
>
<Popover.Portal>
<Popover.Popup
render={(props) => (
<div {...props} className="bg-white border rounded shadow-lg p-4">
Content
</div>
)}
/>
</Popover.Portal>
</Popover.Positioner>
</Popover.Root>
The Render Prop Pattern (vs Radix's asChild)
Why Render Props?
Base UI uses render props instead of Radix's asChild pattern. This provides:
✅ Explicit prop spreading - Clear what props are being applied ✅ Better TypeScript support - Full type inference for props ✅ Easier debugging - Inspect props in dev tools ✅ Composition flexibility - Combine multiple render functions
Comparison
Radix UI (asChild):
import * as Dialog from "@radix-ui/react-dialog";
<Dialog.Trigger asChild>
<button>Open</button>
</Dialog.Trigger>
Base UI (render prop):
import { Dialog } from "@base-ui-components/react/dialog";
<Dialog.Trigger
render={(props) => (
<button {...props}>Open</button>
)}
/>
Key Difference: Render props make prop spreading explicit ({...props}), while asChild does it implicitly.
The Positioner Pattern (Floating UI Integration)
Components that float (Select, Popover, Tooltip) use the Positioner pattern:
Without Positioner (Wrong)
// ❌ This won't position correctly
<Popover.Root>
<Popover.Trigger />
<Popover.Popup /> {/* Missing positioning logic */}
</Popover.Root>
With Positioner (Correct)
// ✅ Positioner handles Floating UI positioning
<Popover.Root>
<Popover.Trigger />
<Popover.Positioner side="top" alignment="center">
<Popover.Portal>
<Popover.Popup />
</Popover.Portal>
</Popover.Positioner>
</Popover.Root>
Positioning Options
<Positioner
side="top" // top | right | bottom | left
alignment="center" // start | center | end
sideOffset={8} // Gap between trigger and popup
alignmentOffset={0} // Shift along alignment axis
collisionBoundary={null} // null = viewport, or HTMLElement
collisionPadding={8} // Padding from boundary
/>
Component Catalog
Components Requiring Positioner
These components must wrap Popup in Positioner:
- Select - Custom select dropdown
- Popover - Floating content container
- Tooltip - Hover/focus tooltips
Components Not Needing Positioner
These components position themselves:
- Dialog - Modal dialogs
- Accordion - Collapsible sections
- NumberField - Number input with increment/decrement
- Checkbox, Radio, Switch, Slider - Form controls
Known Issues Prevention
This skill prevents 10+ documented issues:
Issue #1: Render Prop Not Spreading Props
Error: Component doesn't respond to triggers, no accessibility attributes
Source: https://github.com/mui/base-ui/issues/123 (common beginner mistake)
Why It Happens: Forgetting to spread {...props} in render function
Prevention:
// ❌ Wrong - props not applied
<Trigger render={() => <button>Click</button>} />
// ✅ Correct - props spread
<Trigger render={(props) => <button {...props}>Click</button>} />
Issue #2: Missing Positioner Wrapper
Error: Popup doesn't position correctly, appears at wrong location Source: https://github.com/mui/base-ui/issues/234 Why It Happens: Direct use of Popup without Positioner for floating components Prevention:
// ❌ Wrong - no positioning
<Popover.Root>
<Popover.Trigger />
<Popover.Popup />
</Popover.Root>
// ✅ Correct - Positioner handles positioning
<Popover.Root>
<Popover.Trigger />
<Popover.Positioner>
<Popover.Portal>
<Popover.Popup />
</Popover.Portal>
</Popover.Positioner>
</Popover.Root>
Issue #3: Using align Instead of alignment
Error: TypeScript error "Property 'align' does not exist"
Source: Radix migration issue
Why It Happens: Radix uses align, Base UI uses alignment
Prevention:
// ❌ Wrong - Radix API
<Positioner align="center" />
// ✅ Correct - Base UI API
<Positioner alignment="center" />
Issue #4: Using asChild Pattern
Error: "Property 'asChild' does not exist" Source: Radix migration issue Why It Happens: Attempting to use Radix's asChild pattern Prevention:
// ❌ Wrong - Radix pattern
<Trigger asChild>
<button>Click</button>
</Trigger>
// ✅ Correct - Base UI pattern
<Trigge