Loading...
Loading...
Production-tested setup for Base UI (@base-ui-components/react) - MUI's unstyled component library that provides accessible, customizable React components using render props pattern. This skill should be used when building accessible UIs with full styling control, migrating from Radix UI, or needing components with Floating UI integration for smart positioning. Use when: Setting up Base UI in Vite + React projects, migrating from Radix UI to Base UI, implementing accessible components (Dialog, Select, Popover, Tooltip, NumberField, Accordion), encountering positioning issues with popups, needing render prop API instead of asChild pattern, building with Tailwind v4 + shadcn/ui, or deploying to Cloudflare Workers. ⚠️ BETA STATUS: Base UI is v1.0.0-beta.4. Stable v1.0 expected Q4 2025. This skill provides workarounds for known beta issues and guidance on API stability. Keywords: base-ui, @base-ui-components/react, mui base ui, unstyled components, accessible components, render props, radix alternative, radix migration, floating-ui, positioner pattern, headless ui, accessible dialog, accessible select, accessible popover, accessible tooltip, accessible accordion, number field, react components, tailwind components, vite react, cloudflare workers ui, beta components, component library
npx skill4agent add jackspace/claudeskillz base-ui-reactpnpm add @base-ui-components/react// 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>
);
}{...props}<Dialog.Portal>BackdropPopupOverlay + ContentPositionerimport { 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>import * as Dialog from "@radix-ui/react-dialog";
<Dialog.Trigger asChild>
<button>Open</button>
</Dialog.Trigger>import { Dialog } from "@base-ui-components/react/dialog";
<Dialog.Trigger
render={(props) => (
<button {...props}>Open</button>
)}
/>{...props}// ❌ This won't position correctly
<Popover.Root>
<Popover.Trigger />
<Popover.Popup /> {/* Missing positioning logic */}
</Popover.Root>// ✅ 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><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
/>PopupPositioner{...props}// ❌ Wrong - props not applied
<Trigger render={() => <button>Click</button>} />
// ✅ Correct - props spread
<Trigger render={(props) => <button {...props}>Click</button>} />// ❌ 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>alignalignment// ❌ Wrong - Radix API
<Positioner align="center" />
// ✅ Correct - Base UI API
<Positioner alignment="center" />// ❌ Wrong - Radix pattern
<Trigger asChild>
<button>Click</button>
</Trigger>
// ✅ Correct - Base UI pattern
<Trigger render={(props) => <button {...props}>Click</button>} />// ❌ Wrong - no Portal
<Dialog.Root>
<Dialog.Trigger />
<Dialog.Popup /> {/* Renders in place */}
</Dialog.Root>
// ✅ Correct - explicit Portal
<Dialog.Root>
<Dialog.Trigger />
<Dialog.Portal>
<Dialog.Popup />
</Dialog.Portal>
</Dialog.Root>// ❌ Wrong - invisible arrow
<Popover.Arrow />
// ✅ Correct - styled arrow
<Popover.Arrow
render={(props) => (
<div {...props} className="w-3 h-3 rotate-45 bg-white border" />
)}
/>ContentPopup// ❌ Wrong - Radix naming
<Dialog.Content>...</Dialog.Content>
// ✅ Correct - Base UI naming
<Dialog.Popup>...</Dialog.Popup>OverlayBackdrop// ❌ Wrong - Radix naming
<Dialog.Overlay />
// ✅ Correct - Base UI naming
<Dialog.Backdrop />// ❌ Wrong - tooltip won't show
<Tooltip.Root>
<Tooltip.Trigger render={(props) => <button {...props} disabled />} />
</Tooltip.Root>
// ✅ Correct - wrap in span
<Tooltip.Root>
<Tooltip.Trigger render={(props) => (
<span {...props}>
<button disabled />
</span>
)} />
</Tooltip.Root>// ❌ Wrong - empty string
<Select.Option value="">Any</Select.Option>
// ✅ Correct - sentinel value
<Select.Option value="__any__">Any</Select.Option><button {...props}>{...props}import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": "/src",
},
},
// Base UI works with any Vite setup - no special config needed
});{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}import { Dialog } from "@base-ui-components/react/dialog";
import { useState } from "react";
export function FormDialog() {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log("Submitted:", name);
setOpen(false);
};
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger
render={(props) => (
<button {...props} className="px-4 py-2 bg-blue-600 text-white rounded">
Open Form
</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 w-full max-w-md"
>
<Dialog.Title
render={(titleProps) => (
<h2 {...titleProps} className="text-2xl font-bold mb-4">
Enter Your Name
</h2>
)}
/>
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 border rounded mb-4"
autoFocus
/>
<div className="flex justify-end gap-2">
<Dialog.Close
render={(closeProps) => (
<button {...closeProps} type="button" className="px-4 py-2 border rounded">
Cancel
</button>
)}
/>
<button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded">
Submit
</button>
</div>
</form>
</div>
)}
/>
</Dialog.Portal>
</Dialog.Root>
);
}import { Select } from "@base-ui-components/react/select";
import { useState } from "react";
const options = [
{ value: "react", label: "React" },
{ value: "vue", label: "Vue" },
{ value: "angular", label: "Angular" },
];
export function SearchableSelect() {
const [value, setValue] = useState("");
const [search, setSearch] = useState("");
const filtered = options.filter((opt) =>
opt.label.toLowerCase().includes(search.toLowerCase())
);
return (
<Select.Root value={value} onValueChange={setValue}>
<Select.Trigger
render={(props) => (
<button {...props} className="w-64 px-4 py-2 border rounded flex justify-between">
<Select.Value
render={(valueProps) => (
<span {...valueProps}>
{options.find((opt) => opt.value === value)?.label || "Select..."}
</span>
)}
/>
<span>▼</span>
</button>
)}
/>
<Select.Positioner side="bottom" alignment="start">
<Select.Portal>
<Select.Popup
render={(props) => (
<div {...props} className="w-64 bg-white border rounded shadow-lg">
<div className="p-2 border-b">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
className="w-full px-3 py-2 border rounded"
/>
</div>
<div className="max-h-60 overflow-y-auto">
{filtered.map((option) => (
<Select.Option
key={option.value}
value={option.value}
render={(optionProps) => (
<div
{...optionProps}
className="px-4 py-2 cursor-pointer hover:bg-gray-100 data-[selected]:bg-blue-600 data-[selected]:text-white"
>
{option.label}
</div>
)}
/>
))}
</div>
</div>
)}
/>
</Select.Portal>
</Select.Positioner>
</Select.Root>
);
}import { NumberField } from "@base-ui-components/react/number-field";
import { useState } from "react";
export function CurrencyInput() {
const [price, setPrice] = useState(9.99);
return (
<NumberField.Root
value={price}
onValueChange={setPrice}
min={0}
max={999.99}
step={0.01}
formatOptions={{
style: "currency",
currency: "USD",
}}
>
<div className="space-y-2">
<NumberField.Label
render={(props) => (
<label {...props} className="block text-sm font-medium">
Price
</label>
)}
/>
<div className="flex items-center gap-2">
<NumberField.Decrement
render={(props) => (
<button {...props} className="w-8 h-8 bg-gray-200 rounded">
−
</button>
)}
/>
<NumberField.Input
render={(props) => (
<input
{...props}
className="w-32 px-3 py-2 text-center border rounded"
/>
)}
/>
<NumberField.Increment
render={(props) => (
<button {...props} className="w-8 h-8 bg-gray-200 rounded">
+
</button>
)}
/>
</div>
</div>
</NumberField.Root>
);
}templates/Dialog.tsxtemplates/Select.tsxtemplates/Popover.tsxtemplates/Tooltip.tsxtemplates/NumberField.tsxtemplates/Accordion.tsxtemplates/migration-example.tsx# Copy Dialog template to your project
cp templates/Dialog.tsx src/components/Dialog.tsxreferences/component-comparison.mdreferences/migration-from-radix.mdreferences/render-prop-deep-dive.mdreferences/known-issues.mdreferences/beta-to-stable.mdreferences/floating-ui-integration.mdscripts/migrate-radix-component.shscripts/check-base-ui-version.sh# Check for Base UI updates
./scripts/check-base-ui-version.sh
# Migrate Radix component
./scripts/migrate-radix-component.sh src/components/Dialog.tsx// Radix
<Trigger asChild><button /></Trigger>
// Base UI
<Trigger render={(props) => <button {...props} />} />// Radix
<Content side="top" />
// Base UI
<Positioner side="top">
<Portal><Popup /></Portal>
</Positioner>ContentPopupOverlayBackdropalignalignment// Radix (automatic)
<Portal><Content /></Portal>
// Base UI (explicit)
<Portal><Popup /></Portal>templates/migration-example.tsx// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import cloudflare from "@cloudflare/vite-plugin";
export default defineConfig({
plugins: [react(), cloudflare()],
build: {
outDir: "dist",
},
});<Dialog.Popup
render={(props) => (
<div {...props} className="bg-white rounded-lg shadow-xl p-6">
Content
</div>
)}
/>import styles from "./Dialog.module.css";
<Dialog.Popup
render={(props) => (
<div {...props} className={styles.popup}>
Content
</div>
)}
/>import styled from "@emotion/styled";
const StyledPopup = styled.div`
background: white;
border-radius: 8px;
padding: 24px;
`;
<Dialog.Popup
render={(props) => (
<StyledPopup {...props}>
Content
</StyledPopup>
)}
/>{...props}@base-ui-components/react@1.0.0-beta.4react@19.2.0+react-dom@19.2.0+@tailwindcss/vite@4.1.14vite@6.0.0{
"dependencies": {
"@base-ui-components/react": "^1.0.0-beta.4",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^5.0.0",
"vite": "^6.0.0"
}
}{...props}// ❌ Wrong
<Trigger render={() => <button>Click</button>} />
// ✅ Correct
<Trigger render={(props) => <button {...props}>Click</button>} />// ❌ Wrong
<Popover.Popup />
// ✅ Correct
<Popover.Positioner side="top">
<Popover.Portal>
<Popover.Popup />
</Popover.Portal>
</Popover.Positioner>alignmentalign// ❌ Wrong (Radix)
<Positioner align="center" />
// ✅ Correct (Base UI)
<Positioner alignment="center" />// ❌ Wrong
<Arrow />
// ✅ Correct
<Arrow render={(props) => (
<div {...props} className="w-3 h-3 rotate-45 bg-white border" />
)} />// ❌ Wrong
<Tooltip.Trigger render={(props) => <button {...props} disabled />} />
// ✅ Correct
<Tooltip.Trigger render={(props) => (
<span {...props}><button disabled /></span>
)} />@base-ui-components/react@1.0.0-beta.4{...props}PositionerPortalalignmentalignPopupContentBackdropOverlayArrowreferences/known-issues.mdreferences/migration-from-radix.md