
Base Ui React
- 147 installs
- 202 repo stars
- Updated August 4, 2026
- secondsky/claude-skills
Helps with frontend development tasks during AI-assisted development.
About
base-ui-react is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted coding.
- base-ui-react
- Frontend Development
- AI-coding skill
Base Ui React by the numbers
- 147 all-time installs (skills.sh)
- +10 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #957 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/secondsky/claude-skills --skill base-ui-reactAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 147 |
|---|---|
| repo stars | ★ 202 |
| Last updated | August 4, 2026 |
| Repository | secondsky/claude-skills ↗ |
What it does
Helps with frontend development tasks during AI-assisted development.
Files
Base UI React
Status: Beta (v1.0.0-beta.4) ⚠️ | Last Verified: 2025-11-18
---
What Is Base UI?
MUI's unstyled, accessible React component library:
- 27+ accessible components
- Render props pattern
- Full styling control
- Floating UI integration
- Alternative to Radix UI
Beta status: v1.0.0-beta.4 (stable v1.0 expected Q4 2025)
---
Quick Start
Install
bun add @base-ui-components/reactBasic Dialog
import * as Dialog from '@base-ui-components/react/dialog';
export function MyDialog() {
return (
<Dialog.Root>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Backdrop />
<Dialog.Popup>
<Dialog.Title>Title</Dialog.Title>
<Dialog.Description>Content</Dialog.Description>
<Dialog.Close>Close</Dialog.Close>
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>
);
}Basic Select
import * as Select from '@base-ui-components/react/select';
<Select.Root>
<Select.Trigger>
<Select.Value placeholder="Select" />
</Select.Trigger>
<Select.Portal>
<Select.Positioner>
<Select.Popup>
<Select.Option value="1">Option 1</Select.Option>
<Select.Option value="2">Option 2</Select.Option>
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>Load `references/setup-guide.md` for complete examples.
---
Core Components
Available (27+):
- Dialog
- Select
- Popover
- Tooltip
- Accordion
- NumberField
- Checkbox
- Switch
- Tabs
- Slider
- And more...
---
Critical Rules
Always Do ✅
1. Use Portal for popups (Dialog, Select, Popover) 2. Use Positioner for floating elements 3. Add Backdrop for modal dialogs 4. Style with Tailwind (or any CSS) 5. Use render props (not asChild like Radix) 6. Test accessibility (ARIA built-in) 7. Handle Portal edge cases (SSR, hydration) 8. Check beta docs for breaking changes 9. Use TypeScript for better DX 10. Test on target browsers
Never Do ❌
1. Never use asChild (use render function instead) 2. Never skip Portal for popups (positioning breaks) 3. Never skip Positioner (Floating UI won't work) 4. Never assume API stability (beta software) 5. Never skip accessibility testing 6. Never use with React <19 (requires React 19+) 7. Never skip Backdrop for modals 8. Never hardcode z-index (use Portal) 9. Never skip SSR testing (hydration issues) 10. Never assume Radix compatibility (different API)
---
With Tailwind
<Dialog.Popup className="rounded-lg bg-white p-6 shadow-xl dark:bg-gray-800">
<Dialog.Title className="text-xl font-bold text-gray-900 dark:text-white">
Dialog Title
</Dialog.Title>
<Dialog.Description className="mt-2 text-gray-600 dark:text-gray-300">
Dialog content here
</Dialog.Description>
<Dialog.Close className="mt-4 rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Close
</Dialog.Close>
</Dialog.Popup>---
Common Use Cases
Use Case 1: Modal Dialog
<Dialog.Root>
<Dialog.Trigger className="btn">Open Modal</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Backdrop className="fixed inset-0 bg-black/50" />
<Dialog.Popup className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white p-6">
<Dialog.Title>Confirm Action</Dialog.Title>
<Dialog.Description>Are you sure?</Dialog.Description>
<Dialog.Close>Cancel</Dialog.Close>
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>Use Case 2: Dropdown Select
<Select.Root>
<Select.Trigger className="flex items-center gap-2 rounded border px-4 py-2">
<Select.Value placeholder="Select option" />
</Select.Trigger>
<Select.Portal>
<Select.Positioner className="z-50">
<Select.Popup className="rounded border bg-white shadow-lg">
<Select.Option value="1" className="px-4 py-2 hover:bg-gray-100">
Option 1
</Select.Option>
<Select.Option value="2" className="px-4 py-2 hover:bg-gray-100">
Option 2
</Select.Option>
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>Use Case 3: Tooltip
import * as Tooltip from '@base-ui-components/react/tooltip';
<Tooltip.Root>
<Tooltip.Trigger>Hover me</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Positioner>
<Tooltip.Popup className="rounded bg-gray-900 px-2 py-1 text-sm text-white">
Tooltip text
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>---
Beta Considerations
Stable for production:
- Dialog
- Popover
- Tooltip
- Select
- Accordion
Use with caution:
- NumberField (API may change)
- Complex form components
Migration path:
- v1.0 stable expected Q4 2025
- Breaking changes will be documented
- Codemods likely provided
---
vs Radix UI
| Feature | Base UI | Radix UI |
|---|---|---|
| Pattern | Render props | asChild |
| Positioning | Floating UI built-in | Manual |
| Beta | Yes | Stable |
| Tree-shaking | Better | Good |
| Bundle size | Smaller | Larger |
When to use Base UI:
- Prefer render props
- Need built-in positioning
- Want smaller bundle
- Okay with beta
When to use Radix:
- Need stability
- Prefer asChild pattern
- Established ecosystem
---
Resources
References (references/):
example-reference.md- Detailed component examples and patternsmigration-from-radix.md- Complete migration guide from Radix UI (includes render prop pattern explanation)setup-guide.md- Installation and setup walkthrough
Templates (templates/):
Accordion.tsx- Accordion component with render propsDialog.tsx- Modal dialog exampleNumberField.tsx- Number input with validationPopover.tsx- Popover with positioningSelect.tsx- Select dropdownTooltip.tsx- Tooltip componentmigration-example.tsx- Radix to Base UI migration example
---
Official Documentation
- Base UI: <https://base-ui.mui.com/>
- Components: <https://base-ui.mui.com/components/>
- Migration Guide: <https://base-ui.mui.com/base-ui/getting-started/migration/>
---
Questions? Issues?
1. Check references/setup-guide.md for setup 2. Review beta status warnings 3. See official docs for latest updates 4. Consider Radix if need stability
[TODO: Example Template File]
[TODO: This directory contains files that will be used in the OUTPUT that Claude produces.]
[TODO: Examples:]
- Templates (.html, .tsx, .md)
- Images (.png, .svg)
- Fonts (.ttf, .woff)
- Boilerplate code
- Configuration file templates
[TODO: Delete this file and add your actual assets]
These files are NOT loaded into context. They are copied or used directly in the final output.
[TODO: Reference Document Name]
[TODO: This file contains reference documentation that Claude can load when needed.]
[TODO: Delete this file if you don't have reference documentation to provide.]
Purpose
[TODO: Explain what information this document contains]
When Claude Should Use This
[TODO: Describe specific scenarios where Claude should load this reference]
Content
[TODO: Add your reference content here - schemas, guides, specifications, etc.]
---
Note: This file is NOT loaded into context by default. Claude will only load it when:
- It determines the information is needed
- You explicitly ask Claude to reference it
- The SKILL.md instructions direct Claude to read it
Keep this file under 10k words for best performance.
Migration Guide: Radix UI → Base UI
Complete guide to migrating from Radix UI to Base UI.
---
Quick Reference
| Pattern | Radix UI | Base UI |
|---|---|---|
| Prop merging | asChild | render={(props) => ...} |
| Positioning | side, align | <Positioner side alignment> |
| Content component | Content | Popup |
| Overlay component | Overlay | Backdrop |
| Alignment prop | align | alignment |
| Portal | Automatic | Explicit <Portal> |
---
Step-by-Step Migration
Step 1: Update Dependencies
# Remove Radix
pnpm remove @radix-ui/react-dialog @radix-ui/react-popover @radix-ui/react-select
# Add Base UI
pnpm add @base-ui-components/reactStep 2: Update Imports
// Before (Radix)
import * as Dialog from "@radix-ui/react-dialog";
import * as Popover from "@radix-ui/react-popover";
import * as Select from "@radix-ui/react-select";
// After (Base UI)
import { Dialog } from "@base-ui-components/react/dialog";
import { Popover } from "@base-ui-components/react/popover";
import { Select } from "@base-ui-components/react/select";Step 3: Replace asChild with Render Props
// Before (Radix)
<Dialog.Trigger asChild>
<button className="btn">Open</button>
</Dialog.Trigger>
// After (Base UI)
<Dialog.Trigger
render={(props) => (
<button {...props} className="btn">
Open
</button>
)}
/>Step 4: Add Positioner for Popups
// Before (Radix Select)
<Select.Portal>
<Select.Content side="bottom" align="start">
<Select.Viewport>
{/* options */}
</Select.Viewport>
</Select.Content>
</Select.Portal>
// After (Base UI Select)
<Select.Positioner side="bottom" alignment="start">
<Select.Portal>
<Select.Popup render={(props) => <div {...props}>{/* options */}</div>} />
</Select.Portal>
</Select.Positioner>Step 5: Rename Components
Content→PopupOverlay→Backdropalign→alignmentViewport→ Remove (not needed)
---
Component-by-Component Guide
Dialog
Radix:
<Dialog.Root>
<Dialog.Trigger asChild>
<button>Open</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="overlay" />
<Dialog.Content className="content">
<Dialog.Title>Title</Dialog.Title>
<Dialog.Description>Description</Dialog.Description>
<Dialog.Close asChild>
<button>Close</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>Base UI:
<Dialog.Root>
<Dialog.Trigger render={(props) => <button {...props}>Open</button>} />
<Dialog.Portal>
<Dialog.Backdrop render={(props) => <div {...props} className="overlay" />} />
<Dialog.Popup render={(props) => (
<div {...props} className="content">
<Dialog.Title render={(p) => <h2 {...p}>Title</h2>} />
<Dialog.Description render={(p) => <p {...p}>Description</p>} />
<Dialog.Close render={(p) => <button {...p}>Close</button>} />
</div>
)} />
</Dialog.Portal>
</Dialog.Root>Popover
Radix:
<Popover.Root>
<Popover.Trigger asChild>
<button>Open</button>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content side="top" align="center">
<Popover.Arrow />
Content
<Popover.Close asChild>
<button>Close</button>
</Popover.Close>
</Popover.Content>
</Popover.Portal>
</Popover.Root>Base UI:
<Popover.Root>
<Popover.Trigger render={(props) => <button {...props}>Open</button>} />
<Popover.Positioner side="top" alignment="center">
<Popover.Portal>
<Popover.Popup render={(props) => (
<div {...props}>
Content
<Popover.Close render={(p) => <button {...p}>Close</button>} />
</div>
)} />
<Popover.Arrow render={(props) => <div {...props} />} />
</Popover.Portal>
</Popover.Positioner>
</Popover.Root>Select
Radix:
<Select.Root>
<Select.Trigger asChild>
<button>
<Select.Value placeholder="Select..." />
<Select.Icon>▼</Select.Icon>
</button>
</Select.Trigger>
<Select.Portal>
<Select.Content>
<Select.Viewport>
<Select.Item value="1" asChild>
<div>Option 1</div>
</Select.Item>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>Base UI:
<Select.Root>
<Select.Trigger render={(props) => (
<button {...props}>
<Select.Value render={(p) => <span {...p} placeholder="Select..." />} />
<Select.Icon render={(p) => <span {...p}>▼</span>} />
</button>
)} />
<Select.Positioner side="bottom" alignment="start">
<Select.Portal>
<Select.Popup render={(props) => (
<div {...props}>
<Select.Option value="1" render={(p) => <div {...p}>Option 1</div>} />
</div>
)} />
</Select.Portal>
</Select.Positioner>
</Select.Root>---
Common Migration Issues
Issue: Props not applied
// ❌ Wrong
<Trigger render={() => <button>Click</button>} />
// ✅ Correct
<Trigger render={(props) => <button {...props}>Click</button>} />Issue: Popup won't position
// ❌ Wrong (missing Positioner)
<Popover.Portal>
<Popover.Popup />
</Popover.Portal>
// ✅ Correct
<Popover.Positioner side="top">
<Popover.Portal>
<Popover.Popup />
</Popover.Portal>
</Popover.Positioner>Issue: TypeScript errors
// ❌ Wrong (Radix prop names)
<Positioner align="center" />
<Dialog.Content />
<Dialog.Overlay />
// ✅ Correct (Base UI prop names)
<Positioner alignment="center" />
<Dialog.Popup />
<Dialog.Backdrop />---
Automated Migration Script
Use the bundled script to automatically convert files:
./scripts/migrate-radix-component.sh src/components/Dialog.tsxThis will: 1. Replace asChild with render props 2. Add Positioner where needed 3. Rename Content → Popup, Overlay → Backdrop 4. Update align → alignment 5. Make Portal explicit
---
Testing After Migration
Checklist:
- [ ] All components render correctly
- [ ] Keyboard navigation works (Tab, Escape, Arrow keys)
- [ ] Screen reader announces elements properly
- [ ] Popups position correctly near viewport edges
- [ ] Styling is preserved
- [ ] TypeScript compiles without errors
- [ ] No console warnings/errors
Test with:
- Keyboard only (no mouse)
- Screen reader (NVDA, JAWS, VoiceOver)
- Different viewport sizes
- Dark mode (if applicable)
---
Migration Timeline Estimates
| Component Count | Estimated Time |
|---|---|
| 1-5 components | 1-2 hours |
| 6-10 components | 2-4 hours |
| 11-20 components | 4-8 hours |
| 20+ components | 1-2 days |
Note: Time includes testing and fixing edge cases.
Base UI React Complete Setup
Quick setup for Base UI (@base-ui-components/react) - MUI's unstyled components.
---
Installation
pnpm add @base-ui-components/reactBeta status: v1.0.0-beta.4 (stable v1.0 expected Q4 2025)
---
Basic Usage
Dialog Example
import * as Dialog from '@base-ui-components/react/dialog';
export function MyDialog() {
return (
<Dialog.Root>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Backdrop />
<Dialog.Popup>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog content here</Dialog.Description>
<Dialog.Close>Close</Dialog.Close>
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>
);
}Select Example
import * as Select from '@base-ui-components/react/select';
export function MySelect() {
return (
<Select.Root>
<Select.Trigger>
<Select.Value placeholder="Select option" />
</Select.Trigger>
<Select.Portal>
<Select.Positioner>
<Select.Popup>
<Select.Option value="1">Option 1</Select.Option>
<Select.Option value="2">Option 2</Select.Option>
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
);
}---
With Tailwind
<Dialog.Popup className="rounded-lg bg-white p-6 shadow-xl">
<Dialog.Title className="text-xl font-bold">Title</Dialog.Title>
<Dialog.Description className="mt-2 text-gray-600">
Content
</Dialog.Description>
</Dialog.Popup>---
Available Components
- Dialog
- Select
- Popover
- Tooltip
- Accordion
- NumberField
- Checkbox
- Switch
- Tabs
- And 20+ more
---
Official Documentation
- Base UI Docs: https://base-ui.mui.com/
- Components: https://base-ui.mui.com/components/
#!/bin/bash
# Check Base UI version and compare with latest
set -e
echo "🔍 Checking Base UI version..."
echo ""
# Check if in a Node.js project
if [ ! -f "package.json" ]; then
echo "❌ Error: package.json not found. Run this in a Node.js project directory."
exit 1
fi
# Check if Base UI is installed
if ! grep -q "@base-ui-components/react" package.json; then
echo "⚠️ Base UI not installed in this project."
echo ""
echo "To install:"
echo " pnpm add @base-ui-components/react"
exit 0
fi
# Get installed version
INSTALLED_VERSION=$(grep "@base-ui-components/react" package.json | sed 's/.*"@base-ui-components\/react": "\^*\([^"]*\)".*/\1/')
echo "📦 Installed version: @base-ui-components/react@$INSTALLED_VERSION"
echo ""
# Check npm for latest version
echo "🌐 Checking npm for latest version..."
LATEST_VERSION=$(npm view @base-ui-components/react version 2>/dev/null || echo "unknown")
if [ "$LATEST_VERSION" = "unknown" ]; then
echo "⚠️ Could not fetch latest version from npm"
exit 1
fi
echo "📦 Latest version: @base-ui-components/react@$LATEST_VERSION"
echo ""
# Compare versions
if [ "$INSTALLED_VERSION" = "$LATEST_VERSION" ]; then
echo "✅ You are on the latest version!"
else
echo "⚠️ Update available: $INSTALLED_VERSION → $LATEST_VERSION"
echo ""
echo "To update:"
echo " pnpm add @base-ui-components/react@latest"
echo ""
echo "⚠️ Beta Warning:"
echo " Base UI is currently in beta. Check release notes before updating:"
echo " https://github.com/mui/base-ui/releases"
fi
echo ""
echo "📚 Official Docs: https://base-ui.com"
echo "📝 GitHub: https://github.com/mui/base-ui"
#!/bin/bash
# [TODO: Script Name]
# [TODO: Brief description of what this script does]
# Example script structure - delete if not needed
set -e # Exit on error
# [TODO: Add your script logic here]
echo "Example script - replace or delete this file"
# Usage:
# ./scripts/example-script.sh [args]
#!/bin/bash
# Automated Radix UI → Base UI migration helper
# WARNING: This script modifies files. Commit your changes first!
set -e
if [ $# -eq 0 ]; then
echo "Usage: $0 <file.tsx>"
echo ""
echo "Example:"
echo " $0 src/components/Dialog.tsx"
echo ""
echo "⚠️ WARNING: This script modifies files. Commit your changes first!"
exit 1
fi
FILE="$1"
if [ ! -f "$FILE" ]; then
echo "❌ Error: File not found: $FILE"
exit 1
fi
echo "🔄 Migrating $FILE from Radix UI to Base UI..."
echo ""
# Create backup
BACKUP="${FILE}.radix-backup"
cp "$FILE" "$BACKUP"
echo "✅ Created backup: $BACKUP"
# Perform migrations
echo "🔧 Applying transformations..."
# 1. Update imports
sed -i 's/import \* as Dialog from "@radix-ui\/react-dialog"/import { Dialog } from "@base-ui-components\/react\/dialog"/' "$FILE"
sed -i 's/import \* as Popover from "@radix-ui\/react-popover"/import { Popover } from "@base-ui-components\/react\/popover"/' "$FILE"
sed -i 's/import \* as Select from "@radix-ui\/react-select"/import { Select } from "@base-ui-components\/react\/select"/' "$FILE"
sed -i 's/import \* as Tooltip from "@radix-ui\/react-tooltip"/import { Tooltip } from "@base-ui-components\/react\/tooltip"/' "$FILE"
sed -i 's/import \* as Accordion from "@radix-ui\/react-accordion"/import { Accordion } from "@base-ui-components\/react\/accordion"/' "$FILE"
# 2. Replace component names
sed -i 's/\.Content\b/.Popup/g' "$FILE"
sed -i 's/\.Overlay\b/.Backdrop/g' "$FILE"
# 3. Replace props
sed -i 's/\balign="/alignment="/g' "$FILE"
# 4. Add TODO markers for manual fixes
echo ""
echo "⚠️ Manual fixes required:"
echo ""
echo "1. Replace asChild with render props:"
echo " <Trigger asChild><button /></Trigger>"
echo " → <Trigger render={(props) => <button {...props} />} />"
echo ""
echo "2. Add Positioner wrapper for Select, Popover, Tooltip:"
echo " <Portal><Popup /></Portal>"
echo " → <Positioner side=\"top\"><Portal><Popup /></Portal></Positioner>"
echo ""
echo "3. Make Portal explicit if not already"
echo ""
echo "4. Ensure all render props spread {...props}"
echo ""
# Add TODO comment at top of file
sed -i '1i // TODO: Complete Base UI migration - check render props, Positioner wrappers, and Portal usage' "$FILE"
echo "✅ Automatic transformations complete!"
echo ""
echo "📝 Next steps:"
echo " 1. Review changes: git diff $FILE"
echo " 2. Manually fix asChild → render props"
echo " 3. Add Positioner wrappers where needed"
echo " 4. Test thoroughly"
echo " 5. Remove TODO comment when done"
echo ""
echo "📚 See references/migration-from-radix.md for complete guide"
echo ""
echo "To restore backup:"
echo " mv $BACKUP $FILE"
// Base UI Accordion Component
// @base-ui-components/react v1.0.0-beta.4
import * as React from "react";
import { Accordion } from "@base-ui-components/react/accordion";
/**
* Example: Collapsible Accordion with Base UI
*
* Key Features:
* - Single or multiple item expansion
* - Animated transitions
* - Keyboard navigation (arrow keys, Home/End)
* - Accessible (ARIA attributes)
*/
export function AccordionExample() {
return (
<Accordion.Root className="w-full max-w-2xl">
{/* Item 1 */}
<Accordion.Item value="item-1" className="border-b border-gray-200">
{/* Header */}
<Accordion.Header>
<Accordion.Trigger
render={(props) => (
<button
{...props}
className="flex w-full items-center justify-between py-4 px-2 text-left hover:bg-gray-50 dark:hover:bg-gray-900"
>
<span className="text-lg font-medium text-gray-900 dark:text-gray-100">
What is Base UI?
</span>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
className="transition-transform duration-200 data-[state=open]:rotate-180"
>
<path
d="M4 6L8 10L12 6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
</button>
)}
/>
</Accordion.Header>
{/* Panel */}
<Accordion.Panel
render={(props) => (
<div
{...props}
className="px-2 pb-4 text-gray-600 dark:text-gray-400"
>
Base UI is a library of unstyled React components built by MUI.
It provides accessible, customizable building blocks for your UI.
</div>
)}
/>
</Accordion.Item>
{/* Item 2 */}
<Accordion.Item value="item-2" className="border-b border-gray-200">
<Accordion.Header>
<Accordion.Trigger
render={(props) => (
<button
{...props}
className="flex w-full items-center justify-between py-4 px-2 text-left hover:bg-gray-50 dark:hover:bg-gray-900"
>
<span className="text-lg font-medium text-gray-900 dark:text-gray-100">
How does it differ from Radix UI?
</span>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
className="transition-transform duration-200 data-[state=open]:rotate-180"
>
<path
d="M4 6L8 10L12 6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
</button>
)}
/>
</Accordion.Header>
<Accordion.Panel
render={(props) => (
<div
{...props}
className="px-2 pb-4 text-gray-600 dark:text-gray-400"
>
Base UI uses render props instead of Radix's asChild pattern.
It also has the Positioner pattern for better popup positioning
with Floating UI.
</div>
)}
/>
</Accordion.Item>
{/* Item 3 */}
<Accordion.Item value="item-3" className="border-b border-gray-200">
<Accordion.Header>
<Accordion.Trigger
render={(props) => (
<button
{...props}
className="flex w-full items-center justify-between py-4 px-2 text-left hover:bg-gray-50 dark:hover:bg-gray-900"
>
<span className="text-lg font-medium text-gray-900 dark:text-gray-100">
Is it production ready?
</span>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
className="transition-transform duration-200 data-[state=open]:rotate-180"
>
<path
d="M4 6L8 10L12 6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
</button>
)}
/>
</Accordion.Header>
<Accordion.Panel
render={(props) => (
<div
{...props}
className="px-2 pb-4 text-gray-600 dark:text-gray-400"
>
Base UI is currently in beta (v1.0.0-beta.4). Stable v1.0 is
expected Q4 2025. Use with caution in production.
</div>
)}
/>
</Accordion.Item>
</Accordion.Root>
);
}
/**
* Multiple Open Items Example
*/
export function MultipleAccordionExample() {
return (
<Accordion.Root
multiple // Allow multiple items open at once
defaultValue={["item-1"]} // Start with item-1 open
className="w-full max-w-2xl"
>
<Accordion.Item value="item-1" className="border-b">
<Accordion.Header>
<Accordion.Trigger
render={(props) => (
<button
{...props}
className="flex w-full items-center justify-between py-4 px-2"
>
<span className="font-medium">Features</span>
<span className="text-sm text-gray-500">▼</span>
</button>
)}
/>
</Accordion.Header>
<Accordion.Panel
render={(props) => (
<div {...props} className="px-2 pb-4">
<ul className="list-disc pl-6 space-y-1">
<li>Unstyled components</li>
<li>Accessible by default</li>
<li>Render prop pattern</li>
</ul>
</div>
)}
/>
</Accordion.Item>
<Accordion.Item value="item-2" className="border-b">
<Accordion.Header>
<Accordion.Trigger
render={(props) => (
<button
{...props}
className="flex w-full items-center justify-between py-4 px-2"
>
<span className="font-medium">Installation</span>
<span className="text-sm text-gray-500">▼</span>
</button>
)}
/>
</Accordion.Header>
<Accordion.Panel
render={(props) => (
<div {...props} className="px-2 pb-4">
<code className="block bg-gray-100 p-2 rounded">
pnpm add @base-ui-components/react
</code>
</div>
)}
/>
</Accordion.Item>
</Accordion.Root>
);
}
/**
* Animated Accordion Example
*/
export function AnimatedAccordionExample() {
return (
<Accordion.Root className="w-full max-w-2xl">
<Accordion.Item value="item-1" className="border-b border-gray-200">
<Accordion.Header>
<Accordion.Trigger
render={(props) => (
<button
{...props}
className="flex w-full items-center justify-between py-4 px-2 text-left group"
>
<span className="text-lg font-medium group-hover:text-blue-600 transition-colors">
Smooth Animation
</span>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
className="transition-transform duration-300 ease-in-out data-[state=open]:rotate-180"
>
<path
d="M5 7.5L10 12.5L15 7.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
)}
/>
</Accordion.Header>
<Accordion.Panel
render={(props) => (
<div
{...props}
className="px-2 overflow-hidden transition-all duration-300 ease-in-out data-[state=open]:pb-4 data-[state=closed]:pb-0"
style={{
maxHeight: props["data-state"] === "open" ? "200px" : "0",
}}
>
<div className="text-gray-600">
This accordion uses CSS transitions for smooth open/close
animations. The content fades in and slides down.
</div>
</div>
)}
/>
</Accordion.Item>
</Accordion.Root>
);
}
/**
* Styled Accordion Example
*/
export function StyledAccordionExample() {
const items = [
{
value: "basics",
title: "Getting Started",
content: "Learn the basics of Base UI and how to set up your project.",
},
{
value: "components",
title: "Components",
content: "Explore all 27+ accessible components available in Base UI.",
},
{
value: "migration",
title: "Migration Guide",
content: "Migrate from Radix UI to Base UI with our comprehensive guide.",
},
];
return (
<Accordion.Root
defaultValue={["basics"]}
className="w-full max-w-2xl bg-white dark:bg-gray-900 rounded-lg shadow-lg"
>
{items.map((item, index) => (
<Accordion.Item
key={item.value}
value={item.value}
className={`${index !== items.length - 1 ? "border-b border-gray-200 dark:border-gray-700" : ""}`}
>
<Accordion.Header>
<Accordion.Trigger
render={(props) => (
<button
{...props}
className="flex w-full items-center justify-between p-6 text-left hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 flex items-center justify-center bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400 rounded-full font-semibold">
{index + 1}
</div>
<span className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{item.title}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500 data-[state=open]:hidden">
Expand
</span>
<span className="text-sm text-gray-500 data-[state=closed]:hidden">
Collapse
</span>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
className="transition-transform duration-200 data-[state=open]:rotate-180"
>
<path
d="M4 6L8 10L12 6"
stroke="currentColor"
strokeWidth="2"
/>
</svg>
</div>
</button>
)}
/>
</Accordion.Header>
<Accordion.Panel
render={(props) => (
<div
{...props}
className="px-6 pb-6 text-gray-600 dark:text-gray-400"
>
{item.content}
</div>
)}
/>
</Accordion.Item>
))}
</Accordion.Root>
);
}
/**
* Disabled Item Example
*/
export function DisabledAccordionExample() {
return (
<Accordion.Root className="w-full max-w-2xl">
<Accordion.Item value="item-1" className="border-b">
<Accordion.Header>
<Accordion.Trigger
render={(props) => (
<button {...props} className="w-full py-4 px-2 text-left">
Active Item
</button>
)}
/>
</Accordion.Header>
<Accordion.Panel
render={(props) => (
<div {...props} className="px-2 pb-4">
This item can be expanded
</div>
)}
/>
</Accordion.Item>
<Accordion.Item value="item-2" disabled className="border-b">
<Accordion.Header>
<Accordion.Trigger
render={(props) => (
<button
{...props}
className="w-full py-4 px-2 text-left opacity-50 cursor-not-allowed"
>
Disabled Item
</button>
)}
/>
</Accordion.Header>
<Accordion.Panel
render={(props) => (
<div {...props} className="px-2 pb-4">
This item cannot be expanded
</div>
)}
/>
</Accordion.Item>
</Accordion.Root>
);
}
/**
* Common Pitfalls:
*
* ❌ Missing value prop
* <Accordion.Item> {/* Won't work */}
* <Accordion.Trigger />
* </Accordion.Item>
*
* ✅ Provide unique value
* <Accordion.Item value="item-1">
* <Accordion.Trigger />
* </Accordion.Item>
*
* ❌ Animating height without max-height
* <Accordion.Panel className="transition-all" /> {/* Jumpy animation */}
*
* ✅ Use max-height or grid
* <Accordion.Panel
* style={{ maxHeight: open ? "200px" : "0" }}
* className="transition-all overflow-hidden"
* />
*
* ❌ Forgetting multiple prop
* <Accordion.Root> {/* Only one item can be open */}
*
* ✅ Allow multiple open
* <Accordion.Root multiple>
*/
// Base UI Dialog Component with Render Props Pattern
// @base-ui-components/react v1.0.0-beta.4
import * as React from "react";
import { Dialog } from "@base-ui-components/react/dialog";
/**
* Example: Modal Dialog with Base UI
*
* Key Differences from Radix:
* - Uses render prop pattern instead of asChild
* - Explicit trigger/backdrop/close props
* - No Portal by default (add manually if needed)
*/
export function DialogExample() {
const [open, setOpen] = React.useState(false);
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
{/* Trigger: Render prop returns button props */}
<Dialog.Trigger
render={(props) => (
<button
{...props}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
Open Dialog
</button>
)}
/>
{/* Backdrop: Optional, render prop for custom styling */}
<Dialog.Backdrop
render={(props) => (
<div
{...props}
className="fixed inset-0 bg-black/50 backdrop-blur-sm"
/>
)}
/>
{/* Portal: Manual portal if you need it */}
<Dialog.Portal>
{/* Popup: The actual dialog container */}
<Dialog.Popup
render={(props) => (
<div
{...props}
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white dark:bg-gray-900 rounded-lg shadow-xl p-6 w-full max-w-md"
>
{/* Title */}
<Dialog.Title
render={(props) => (
<h2
{...props}
className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100"
>
Dialog Title
</h2>
)}
/>
{/* Description */}
<Dialog.Description
render={(props) => (
<p
{...props}
className="text-gray-600 dark:text-gray-400 mb-6"
>
This is an example dialog built with Base UI using the
render prop pattern. All styling is custom.
</p>
)}
/>
{/* Content */}
<div className="mb-6">
<label className="block text-sm font-medium mb-2">
Example Input
</label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Type something..."
/>
</div>
{/* Actions */}
<div className="flex justify-end gap-3">
<Dialog.Close
render={(props) => (
<button
{...props}
className="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
>
Cancel
</button>
)}
/>
<button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
Confirm
</button>
</div>
</div>
)}
/>
</Dialog.Portal>
</Dialog.Root>
);
}
/**
* Alternative: Without Render Props (Direct Props)
*
* Base UI also supports direct className/style props if you don't need
* the flexibility of render props.
*/
export function DialogSimple() {
const [open, setOpen] = React.useState(false);
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger className="px-4 py-2 bg-blue-600 text-white rounded-md">
Open Simple Dialog
</Dialog.Trigger>
<Dialog.Backdrop className="fixed inset-0 bg-black/50" />
<Dialog.Portal>
<Dialog.Popup className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg shadow-xl p-6 max-w-md">
<Dialog.Title className="text-2xl font-semibold mb-4">
Simple Dialog
</Dialog.Title>
<Dialog.Description className="text-gray-600 mb-6">
You can use direct className props if you don't need render props.
</Dialog.Description>
<Dialog.Close className="px-4 py-2 border rounded-md">
Close
</Dialog.Close>
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>
);
}
/**
* Common Pitfalls:
*
* ❌ Don't use asChild (Radix pattern)
* <Dialog.Trigger asChild>
* <button>Open</button>
* </Dialog.Trigger>
*
* ✅ Use render prop
* <Dialog.Trigger render={(props) => <button {...props}>Open</button>} />
*
* ❌ Don't expect automatic Portal
* <Dialog.Content>...</Dialog.Content>
*
* ✅ Wrap in Portal manually
* <Dialog.Portal>
* <Dialog.Popup>...</Dialog.Popup>
* </Dialog.Portal>
*/
// Migration Example: Radix UI → Base UI
// Side-by-side comparison showing key differences
import * as React from "react";
/**
* ============================================================================
* DIALOG COMPONENT COMPARISON
* ============================================================================
*/
// ❌ RADIX UI (Old)
/*
import * as Dialog from "@radix-ui/react-dialog";
export function RadixDialog() {
return (
<Dialog.Root>
{/* asChild pattern - merge props into child *\/}
<Dialog.Trigger asChild>
<button className="btn">Open</button>
</Dialog.Trigger>
{/* Portal is automatic *\/}
<Dialog.Portal>
<Dialog.Overlay className="overlay" />
<Dialog.Content className="content">
<Dialog.Title>Title</Dialog.Title>
<Dialog.Description>Description</Dialog.Description>
<Dialog.Close asChild>
<button>Close</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
*/
// ✅ BASE UI (New)
import { Dialog } from "@base-ui-components/react/dialog";
export function BaseUIDialog() {
return (
<Dialog.Root>
{/* Render prop pattern - explicit props spreading */}
<Dialog.Trigger
render={(props) => (
<button {...props} className="btn">
Open
</button>
)}
/>
{/* Portal must be explicit */}
<Dialog.Portal>
<Dialog.Backdrop
render={(props) => <div {...props} className="overlay" />}
/>
<Dialog.Popup
render={(props) => (
<div {...props} className="content">
<Dialog.Title
render={(titleProps) => (
<h2 {...titleProps}>Title</h2>
)}
/>
<Dialog.Description
render={(descProps) => (
<p {...descProps}>Description</p>
)}
/>
<Dialog.Close
render={(closeProps) => (
<button {...closeProps}>Close</button>
)}
/>
</div>
)}
/>
</Dialog.Portal>
</Dialog.Root>
);
}
/**
* ============================================================================
* SELECT COMPONENT COMPARISON
* ============================================================================
*/
// ❌ RADIX UI (Old)
/*
import * as Select from "@radix-ui/react-select";
export function RadixSelect() {
return (
<Select.Root>
<Select.Trigger asChild>
<button>
<Select.Value placeholder="Select..." />
<Select.Icon>▼</Select.Icon>
</button>
</Select.Trigger>
{/* Portal is automatic *\/}
<Select.Portal>
<Select.Content>
<Select.Viewport>
<Select.Item value="1" asChild>
<div>Option 1</div>
</Select.Item>
<Select.Item value="2" asChild>
<div>Option 2</div>
</Select.Item>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
);
}
*/
// ✅ BASE UI (New)
import { Select } from "@base-ui-components/react/select";
export function BaseUISelect() {
return (
<Select.Root>
<Select.Trigger
render={(props) => (
<button {...props}>
<Select.Value
render={(valueProps) => (
<span {...valueProps} placeholder="Select..." />
)}
/>
<Select.Icon
render={(iconProps) => <span {...iconProps}>▼</span>}
/>
</button>
)}
/>
{/* Positioner is REQUIRED for positioning */}
<Select.Positioner side="bottom" alignment="start">
<Select.Portal>
<Select.Popup
render={(props) => (
<div {...props}>
<Select.Option
value="1"
render={(optionProps) => (
<div {...optionProps}>Option 1</div>
)}
/>
<Select.Option
value="2"
render={(optionProps) => (
<div {...optionProps}>Option 2</div>
)}
/>
</div>
)}
/>
</Select.Portal>
</Select.Positioner>
</Select.Root>
);
}
/**
* ============================================================================
* POPOVER COMPONENT COMPARISON
* ============================================================================
*/
// ❌ RADIX UI (Old)
/*
import * as Popover from "@radix-ui/react-popover";
export function RadixPopover() {
return (
<Popover.Root>
<Popover.Trigger asChild>
<button>Open</button>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content side="top" align="center">
<Popover.Arrow />
<p>Content</p>
<Popover.Close asChild>
<button>Close</button>
</Popover.Close>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
}
*/
// ✅ BASE UI (New)
import { Popover } from "@base-ui-components/react/popover";
export function BaseUIPopover() {
return (
<Popover.Root>
<Popover.Trigger
render={(props) => <button {...props}>Open</button>}
/>
{/* Positioner wraps Portal and handles positioning */}
<Popover.Positioner side="top" alignment="center">
<Popover.Portal>
<Popover.Popup
render={(props) => (
<div {...props}>
<p>Content</p>
<Popover.Close
render={(closeProps) => (
<button {...closeProps}>Close</button>
)}
/>
</div>
)}
/>
<Popover.Arrow
render={(props) => <div {...props} />}
/>
</Popover.Portal>
</Popover.Positioner>
</Popover.Root>
);
}
/**
* ============================================================================
* TOOLTIP COMPONENT COMPARISON
* ============================================================================
*/
// ❌ RADIX UI (Old)
/*
import * as Tooltip from "@radix-ui/react-tooltip";
export function RadixTooltip() {
return (
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button>Hover</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="top">
Tooltip
<Tooltip.Arrow />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}
*/
// ✅ BASE UI (New)
import { Tooltip } from "@base-ui-components/react/tooltip";
export function BaseUITooltip() {
return (
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger
render={(props) => <button {...props}>Hover</button>}
/>
<Tooltip.Positioner side="top" alignment="center">
<Tooltip.Portal>
<Tooltip.Popup
render={(props) => <div {...props}>Tooltip</div>}
/>
<Tooltip.Arrow
render={(props) => <div {...props} />}
/>
</Tooltip.Portal>
</Tooltip.Positioner>
</Tooltip.Root>
</Tooltip.Provider>
);
}
/**
* ============================================================================
* KEY MIGRATION PATTERNS
* ============================================================================
*/
/**
* Pattern 1: asChild → render prop
*
* RADIX:
* <Trigger asChild>
* <button>Click</button>
* </Trigger>
*
* BASE UI:
* <Trigger render={(props) => <button {...props}>Click</button>} />
*/
/**
* Pattern 2: Direct props → Positioner wrapper
*
* RADIX:
* <Content side="top" align="center">...</Content>
*
* BASE UI:
* <Positioner side="top" alignment="center">
* <Portal>
* <Popup>...</Popup>
* </Portal>
* </Positioner>
*/
/**
* Pattern 3: Content → Popup
*
* RADIX:
* <Dialog.Content>...</Dialog.Content>
*
* BASE UI:
* <Dialog.Popup render={(props) => <div {...props}>...</div>} />
*/
/**
* Pattern 4: Overlay → Backdrop
*
* RADIX:
* <Dialog.Overlay />
*
* BASE UI:
* <Dialog.Backdrop render={(props) => <div {...props} />} />
*/
/**
* Pattern 5: align → alignment
*
* RADIX:
* align="center"
*
* BASE UI:
* alignment="center"
*/
/**
* ============================================================================
* MIGRATION CHECKLIST
* ============================================================================
*
* [ ] Replace all asChild with render prop pattern
* [ ] Wrap Content/Popup in Positioner for popups (Select, Popover, Tooltip)
* [ ] Rename Content → Popup
* [ ] Rename Overlay → Backdrop
* [ ] Change align → alignment
* [ ] Make Portal explicit (not automatic)
* [ ] Update Arrow styling (now requires explicit render prop)
* [ ] Test keyboard navigation still works
* [ ] Test screen reader announcements
* [ ] Verify positioning near viewport edges
*/
/**
* ============================================================================
* COMMON GOTCHAS
* ============================================================================
*
* 1. Forgot Positioner wrapper
* ❌ <Popup /> won't position correctly
* ✅ <Positioner><Portal><Popup /></Portal></Positioner>
*
* 2. Used asChild instead of render
* ❌ <Trigger asChild><button /></Trigger>
* ✅ <Trigger render={(props) => <button {...props} />} />
*
* 3. Used align instead of alignment
* ❌ <Positioner align="center" />
* ✅ <Positioner alignment="center" />
*
* 4. Expected automatic Portal
* ❌ <Popup /> (renders in place)
* ✅ <Portal><Popup /></Portal>
*
* 5. Didn't spread props from render
* ❌ <Trigger render={() => <button>Click</button>} />
* ✅ <Trigger render={(props) => <button {...props}>Click</button>} />
*/
// Base UI NumberField Component
// @base-ui-components/react v1.0.0-beta.4
import * as React from "react";
import { NumberField } from "@base-ui-components/react/number-field";
/**
* Example: Number Input with Increment/Decrement
*
* Key Features:
* - Built-in increment/decrement buttons
* - Min/max validation
* - Step control
* - Keyboard shortcuts (arrow keys, page up/down)
* - Scroll wheel support
*/
export function NumberFieldExample() {
const [value, setValue] = React.useState<number>(0);
return (
<NumberField.Root
value={value}
onValueChange={setValue}
min={0}
max={100}
step={1}
>
<div className="flex items-center gap-2">
{/* Decrement button */}
<NumberField.Decrement
render={(props) => (
<button
{...props}
className="w-8 h-8 flex items-center justify-center bg-gray-200 dark:bg-gray-800 rounded-md hover:bg-gray-300 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M4 8H12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
</button>
)}
/>
{/* Input field */}
<NumberField.Input
render={(props) => (
<input
{...props}
className="w-20 px-3 py-2 text-center bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
)}
/>
{/* Increment button */}
<NumberField.Increment
render={(props) => (
<button
{...props}
className="w-8 h-8 flex items-center justify-center bg-gray-200 dark:bg-gray-800 rounded-md hover:bg-gray-300 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M8 4V12M4 8H12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
</button>
)}
/>
</div>
</NumberField.Root>
);
}
/**
* With Label and Description
*/
export function NumberFieldWithLabelExample() {
const [quantity, setQuantity] = React.useState(1);
return (
<NumberField.Root
value={quantity}
onValueChange={setQuantity}
min={1}
max={99}
step={1}
>
<div className="space-y-2">
{/* Label */}
<NumberField.Label
render={(props) => (
<label
{...props}
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
Quantity
</label>
)}
/>
{/* Input group */}
<div className="flex items-center gap-2">
<NumberField.Decrement
render={(props) => (
<button
{...props}
className="w-8 h-8 flex items-center justify-center bg-gray-200 rounded-md hover:bg-gray-300 disabled:opacity-50"
>
−
</button>
)}
/>
<NumberField.Input
render={(props) => (
<input
{...props}
className="w-20 px-3 py-2 text-center border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
/>
)}
/>
<NumberField.Increment
render={(props) => (
<button
{...props}
className="w-8 h-8 flex items-center justify-center bg-gray-200 rounded-md hover:bg-gray-300 disabled:opacity-50"
>
+
</button>
)}
/>
</div>
{/* Description */}
<NumberField.Description
render={(props) => (
<p {...props} className="text-sm text-gray-600 dark:text-gray-400">
Select quantity between 1 and 99
</p>
)}
/>
</div>
</NumberField.Root>
);
}
/**
* Decimal Numbers Example
*/
export function DecimalNumberFieldExample() {
const [price, setPrice] = React.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-md hover:bg-gray-300"
>
−
</button>
)}
/>
<NumberField.Input
render={(props) => (
<input
{...props}
className="w-32 px-3 py-2 text-center border rounded-md focus:ring-2 focus:ring-blue-500"
/>
)}
/>
<NumberField.Increment
render={(props) => (
<button
{...props}
className="w-8 h-8 bg-gray-200 rounded-md hover:bg-gray-300"
>
+
</button>
)}
/>
</div>
</div>
</NumberField.Root>
);
}
/**
* Percentage Example
*/
export function PercentageNumberFieldExample() {
const [percentage, setPercentage] = React.useState(50);
return (
<NumberField.Root
value={percentage}
onValueChange={setPercentage}
min={0}
max={100}
step={5}
formatOptions={{
style: "percent",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}}
>
<div className="space-y-2">
<NumberField.Label
render={(props) => (
<label {...props} className="block text-sm font-medium">
Completion
</label>
)}
/>
<div className="flex items-center gap-2">
<NumberField.Decrement
render={(props) => (
<button
{...props}
className="w-8 h-8 bg-blue-100 text-blue-600 rounded-md hover:bg-blue-200"
>
−
</button>
)}
/>
<NumberField.Input
render={(props) => (
<input
{...props}
className="w-24 px-3 py-2 text-center border border-blue-300 rounded-md focus:ring-2 focus:ring-blue-500"
/>
)}
/>
<NumberField.Increment
render={(props) => (
<button
{...props}
className="w-8 h-8 bg-blue-100 text-blue-600 rounded-md hover:bg-blue-200"
>
+
</button>
)}
/>
</div>
{/* Visual progress bar */}
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all"
style={{ width: `${percentage}%` }}
/>
</div>
</div>
</NumberField.Root>
);
}
/**
* Large Step Increments
*/
export function LargeStepNumberFieldExample() {
const [value, setValue] = React.useState(1000);
return (
<NumberField.Root
value={value}
onValueChange={setValue}
min={0}
max={10000}
step={100} // Large steps for quick adjustments
largeStep={1000} // Page Up/Down uses this
>
<div className="space-y-2">
<NumberField.Label
render={(props) => (
<label {...props} className="block text-sm font-medium">
Budget
</label>
)}
/>
<div className="flex items-center gap-2">
<NumberField.Decrement
render={(props) => (
<button
{...props}
className="w-8 h-8 bg-gray-200 rounded-md hover:bg-gray-300"
>
−
</button>
)}
/>
<NumberField.Input
render={(props) => (
<input
{...props}
className="w-32 px-3 py-2 text-center border rounded-md focus:ring-2 focus:ring-blue-500"
/>
)}
/>
<NumberField.Increment
render={(props) => (
<button
{...props}
className="w-8 h-8 bg-gray-200 rounded-md hover:bg-gray-300"
>
+
</button>
)}
/>
</div>
<p className="text-xs text-gray-500">
Tip: Use Page Up/Down for ±1000, Arrow keys for ±100
</p>
</div>
</NumberField.Root>
);
}
/**
* Disabled State Example
*/
export function DisabledNumberFieldExample() {
return (
<NumberField.Root value={42} disabled>
<div className="space-y-2">
<NumberField.Label
render={(props) => (
<label {...props} className="block text-sm font-medium text-gray-400">
Locked Value
</label>
)}
/>
<div className="flex items-center gap-2">
<NumberField.Decrement
render={(props) => (
<button
{...props}
className="w-8 h-8 bg-gray-100 text-gray-400 rounded-md cursor-not-allowed"
>
−
</button>
)}
/>
<NumberField.Input
render={(props) => (
<input
{...props}
className="w-20 px-3 py-2 text-center bg-gray-100 border border-gray-200 rounded-md cursor-not-allowed"
/>
)}
/>
<NumberField.Increment
render={(props) => (
<button
{...props}
className="w-8 h-8 bg-gray-100 text-gray-400 rounded-md cursor-not-allowed"
>
+
</button>
)}
/>
</div>
</div>
</NumberField.Root>
);
}
/**
* Common Pitfalls:
*
* ❌ Using type="number" directly
* <input type="number" /> {/* No increment/decrement, poor accessibility */}
*
* ✅ Use NumberField
* <NumberField.Root>
* <NumberField.Input />
* <NumberField.Increment />
* <NumberField.Decrement />
* </NumberField.Root>
*
* ❌ Forgetting min/max validation
* <NumberField.Root value={value} /> {/* Can go negative/unlimited */}
*
* ✅ Set boundaries
* <NumberField.Root value={value} min={0} max={100} />
*
* ❌ Wrong step for decimals
* <NumberField.Root step={1} /> {/* Can't enter 9.99 */}
*
* ✅ Match step to precision
* <NumberField.Root step={0.01} /> {/* Allows 2 decimal places */}
*/
// Base UI Popover Component with Floating UI Integration
// @base-ui-components/react v1.0.0-beta.4
import * as React from "react";
import { Popover } from "@base-ui-components/react/popover";
/**
* Example: Popover with Base UI
*
* Key Features:
* - Positioner pattern with Floating UI
* - Anchor positioning (side, alignment, offset)
* - Click or hover triggers
* - Arrow support
*/
export function PopoverExample() {
const [open, setOpen] = React.useState(false);
return (
<Popover.Root open={open} onOpenChange={setOpen}>
{/* Trigger */}
<Popover.Trigger
render={(props) => (
<button
{...props}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
Open Popover
</button>
)}
/>
{/* Positioner with Floating UI options */}
<Popover.Positioner
side="top"
alignment="center"
sideOffset={8}
render={(props) => (
<div {...props} className="z-50">
<Popover.Portal>
{/* Popup */}
<Popover.Popup
render={(popupProps) => (
<div
{...popupProps}
className="bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-md shadow-lg p-4 w-64"
>
{/* Title */}
<Popover.Title
render={(titleProps) => (
<h3
{...titleProps}
className="text-lg font-semibold mb-2 text-gray-900 dark:text-gray-100"
>
Popover Title
</h3>
)}
/>
{/* Description */}
<Popover.Description
render={(descProps) => (
<p
{...descProps}
className="text-sm text-gray-600 dark:text-gray-400 mb-4"
>
This is a popover with content. It positions itself
automatically using Floating UI.
</p>
)}
/>
{/* Close button */}
<Popover.Close
render={(closeProps) => (
<button
{...closeProps}
className="px-3 py-1 bg-gray-200 dark:bg-gray-800 rounded-md text-sm hover:bg-gray-300 dark:hover:bg-gray-700"
>
Close
</button>
)}
/>
</div>
)}
/>
{/* Arrow */}
<Popover.Arrow
render={(arrowProps) => (
<div
{...arrowProps}
className="relative w-3 h-3 rotate-45 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700"
/>
)}
/>
</Popover.Portal>
</div>
)}
/>
</Popover.Root>
);
}
/**
* Hover Trigger Example
*/
export function HoverPopoverExample() {
const [open, setOpen] = React.useState(false);
return (
<Popover.Root open={open} onOpenChange={setOpen}>
<Popover.Trigger
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
render={(props) => (
<button
{...props}
className="px-4 py-2 border border-gray-300 rounded-md hover:border-gray-400"
>
Hover me
</button>
)}
/>
<Popover.Positioner
side="top"
alignment="center"
sideOffset={4}
>
<Popover.Portal>
<Popover.Popup
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
render={(props) => (
<div
{...props}
className="bg-gray-900 text-white text-sm px-3 py-2 rounded-md shadow-lg"
>
This is a tooltip-style popover
</div>
)}
/>
</Popover.Portal>
</Popover.Positioner>
</Popover.Root>
);
}
/**
* Positioning Options Example
*/
export function PositioningExample() {
const [side, setSide] = React.useState<"top" | "right" | "bottom" | "left">("top");
const [alignment, setAlignment] = React.useState<"start" | "center" | "end">("center");
return (
<div className="space-y-4">
{/* Controls */}
<div className="flex gap-4">
<div>
<label className="block text-sm font-medium mb-1">Side</label>
<select
value={side}
onChange={(e) => setSide(e.target.value as typeof side)}
className="border rounded-md px-3 py-1"
>
<option value="top">Top</option>
<option value="right">Right</option>
<option value="bottom">Bottom</option>
<option value="left">Left</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Alignment</label>
<select
value={alignment}
onChange={(e) => setAlignment(e.target.value as typeof alignment)}
className="border rounded-md px-3 py-1"
>
<option value="start">Start</option>
<option value="center">Center</option>
<option value="end">End</option>
</select>
</div>
</div>
{/* Popover */}
<Popover.Root>
<Popover.Trigger
render={(props) => (
<button
{...props}
className="px-4 py-2 bg-blue-600 text-white rounded-md"
>
Open Popover ({side} - {alignment})
</button>
)}
/>
<Popover.Positioner
side={side}
alignment={alignment}
sideOffset={8}
alignmentOffset={0}
>
<Popover.Portal>
<Popover.Popup
render={(props) => (
<div
{...props}
className="bg-white border rounded-md shadow-lg p-4 w-64"
>
<p className="text-sm">
Position: <strong>{side}</strong>
<br />
Alignment: <strong>{alignment}</strong>
</p>
</div>
)}
/>
<Popover.Arrow
render={(props) => (
<div {...props} className="w-3 h-3 rotate-45 bg-white border" />
)}
/>
</Popover.Portal>
</Popover.Positioner>
</Popover.Root>
</div>
);
}
/**
* Collision Boundary Example
* Prevents popover from overflowing viewport
*/
export function CollisionBoundaryExample() {
return (
<div className="flex justify-end p-4">
<Popover.Root>
<Popover.Trigger
render={(props) => (
<button
{...props}
className="px-4 py-2 bg-blue-600 text-white rounded-md"
>
Open (near edge)
</button>
)}
/>
<Popover.Positioner
side="right"
alignment="start"
sideOffset={8}
collisionBoundary={null} // null = viewport (default)
collisionPadding={8}
>
<Popover.Portal>
<Popover.Popup
render={(props) => (
<div
{...props}
className="bg-white border rounded-md shadow-lg p-4 w-64"
>
<p className="text-sm">
This popover will automatically flip to stay in viewport.
</p>
</div>
)}
/>
</Popover.Portal>
</Popover.Positioner>
</Popover.Root>
</div>
);
}
/**
* Common Pitfalls:
*
* ❌ Missing Positioner
* <Popover.Root>
* <Popover.Trigger />
* <Popover.Popup /> {/* Won't position correctly */}
* </Popover.Root>
*
* ✅ Wrap in Positioner
* <Popover.Root>
* <Popover.Trigger />
* <Popover.Positioner>
* <Popover.Portal>
* <Popover.Popup />
* </Popover.Portal>
* </Popover.Positioner>
* </Popover.Root>
*
* ❌ Arrow without proper styling
* <Popover.Arrow /> {/* Invisible */}
*
* ✅ Style arrow correctly
* <Popover.Arrow
* render={(props) => (
* <div {...props} className="w-3 h-3 rotate-45 bg-white border" />
* )}
* />
*/
// Base UI Select Component with Positioner Pattern
// @base-ui-components/react v1.0.0-beta.4
import * as React from "react";
import { Select } from "@base-ui-components/react/select";
/**
* Example: Custom Select with Base UI
*
* Key Features:
* - Positioner pattern for popup positioning (Floating UI)
* - Render props for full styling control
* - Accessible keyboard navigation
* - Multi-select support
*/
interface Option {
value: string;
label: string;
}
const options: Option[] = [
{ value: "react", label: "React" },
{ value: "vue", label: "Vue" },
{ value: "angular", label: "Angular" },
{ value: "svelte", label: "Svelte" },
{ value: "solid", label: "Solid" },
];
export function SelectExample() {
const [value, setValue] = React.useState<string>("react");
return (
<Select.Root value={value} onValueChange={setValue}>
{/* Trigger Button */}
<Select.Trigger
render={(props) => (
<button
{...props}
className="flex items-center justify-between w-64 px-4 py-2 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-md hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<Select.Value
render={(valueProps) => (
<span {...valueProps} className="text-gray-900 dark:text-gray-100">
{options.find((opt) => opt.value === value)?.label || "Select..."}
</span>
)}
/>
<Select.Icon
render={(iconProps) => (
<svg
{...iconProps}
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
className="text-gray-500"
>
<path
d="M4 6L8 10L12 6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
)}
/>
</button>
)}
/>
{/* Positioner: Handles popup positioning with Floating UI */}
<Select.Positioner
render={(props) => (
<div {...props} className="z-50">
{/* Portal for rendering outside DOM hierarchy */}
<Select.Portal>
{/* Popup: The actual dropdown */}
<Select.Popup
render={(popupProps) => (
<div
{...popupProps}
className="mt-1 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-md shadow-lg overflow-hidden"
>
{options.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 dark:hover:bg-gray-800 data-[highlighted]:bg-blue-50 dark:data-[highlighted]:bg-blue-900/20 data-[selected]:bg-blue-600 data-[selected]:text-white"
>
{option.label}
</div>
)}
/>
))}
</div>
)}
/>
</Select.Portal>
</div>
)}
/>
</Select.Root>
);
}
/**
* Multi-Select Example
*/
export function MultiSelectExample() {
const [values, setValues] = React.useState<string[]>(["react", "vue"]);
return (
<Select.Root
value={values}
onValueChange={setValues}
multiple
>
<Select.Trigger
render={(props) => (
<button
{...props}
className="flex items-center justify-between w-64 px-4 py-2 bg-white border border-gray-300 rounded-md"
>
<Select.Value
render={(valueProps) => (
<span {...valueProps}>
{values.length > 0
? `${values.length} selected`
: "Select multiple..."}
</span>
)}
/>
</button>
)}
/>
<Select.Positioner>
<Select.Portal>
<Select.Popup
render={(props) => (
<div {...props} className="mt-1 bg-white border rounded-md shadow-lg">
{options.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 flex items-center gap-2"
>
{/* Checkbox indicator */}
<span className="w-4 h-4 border border-gray-300 rounded data-[selected]:bg-blue-600 data-[selected]:border-blue-600" />
{option.label}
</div>
)}
/>
))}
</div>
)}
/>
</Select.Portal>
</Select.Positioner>
</Select.Root>
);
}
/**
* Searchable Select Example
*/
export function SearchableSelectExample() {
const [value, setValue] = React.useState<string>("");
const [search, setSearch] = React.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="flex items-center justify-between w-64 px-4 py-2 bg-white border rounded-md"
>
<Select.Value
render={(valueProps) => (
<span {...valueProps}>
{options.find((opt) => opt.value === value)?.label || "Search..."}
</span>
)}
/>
</button>
)}
/>
<Select.Positioner>
<Select.Portal>
<Select.Popup
render={(props) => (
<div {...props} className="mt-1 bg-white border rounded-md shadow-lg">
{/* Search input */}
<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-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Filtered options */}
<div className="max-h-60 overflow-y-auto">
{filtered.length > 0 ? (
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 className="px-4 py-2 text-gray-500">No results found</div>
)}
</div>
</div>
)}
/>
</Select.Portal>
</Select.Positioner>
</Select.Root>
);
}
/**
* Common Pitfalls:
*
* ❌ Missing Positioner (popup won't position correctly)
* <Select.Root>
* <Select.Trigger />
* <Select.Popup /> {/* Wrong! */}
* </Select.Root>
*
* ✅ Wrap Popup in Positioner
* <Select.Root>
* <Select.Trigger />
* <Select.Positioner>
* <Select.Portal>
* <Select.Popup />
* </Select.Portal>
* </Select.Positioner>
* </Select.Root>
*
* ❌ Using empty string value (breaks accessibility)
* <Select.Option value="">Any</Select.Option>
*
* ✅ Use sentinel value
* <Select.Option value="__any__">Any</Select.Option>
*/
// Base UI Tooltip Component
// @base-ui-components/react v1.0.0-beta.4
import * as React from "react";
import { Tooltip } from "@base-ui-components/react/tooltip";
/**
* Example: Accessible Tooltip with Base UI
*
* Key Features:
* - Hover and focus triggers
* - Keyboard accessible
* - Positioner for smart positioning
* - Delay controls
*/
export function TooltipExample() {
return (
<Tooltip.Root>
{/* Trigger */}
<Tooltip.Trigger
render={(props) => (
<button
{...props}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
Hover me
</button>
)}
/>
{/* Positioner */}
<Tooltip.Positioner
side="top"
alignment="center"
sideOffset={4}
render={(props) => (
<div {...props} className="z-50">
<Tooltip.Portal>
{/* Popup */}
<Tooltip.Popup
render={(popupProps) => (
<div
{...popupProps}
className="bg-gray-900 text-white text-sm px-3 py-2 rounded-md shadow-lg max-w-xs"
>
This is a tooltip with helpful information
</div>
)}
/>
{/* Arrow */}
<Tooltip.Arrow
render={(arrowProps) => (
<div
{...arrowProps}
className="w-2 h-2 rotate-45 bg-gray-900"
/>
)}
/>
</Tooltip.Portal>
</div>
)}
/>
</Tooltip.Root>
);
}
/**
* Delayed Tooltip Example
*/
export function DelayedTooltipExample() {
return (
<Tooltip.Root
delay={500} // 500ms delay before showing
closeDelay={0} // Hide immediately
>
<Tooltip.Trigger
render={(props) => (
<button
{...props}
className="px-4 py-2 border border-gray-300 rounded-md hover:border-gray-400"
>
Hover me (500ms delay)
</button>
)}
/>
<Tooltip.Positioner side="top" alignment="center" sideOffset={4}>
<Tooltip.Portal>
<Tooltip.Popup
render={(props) => (
<div
{...props}
className="bg-gray-900 text-white text-sm px-3 py-2 rounded-md"
>
Delayed tooltip
</div>
)}
/>
</Tooltip.Portal>
</Tooltip.Positioner>
</Tooltip.Root>
);
}
/**
* Rich Content Tooltip Example
*/
export function RichTooltipExample() {
return (
<Tooltip.Root>
<Tooltip.Trigger
render={(props) => (
<button
{...props}
className="inline-flex items-center gap-1 text-blue-600 hover:underline"
>
What's this?
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
className="text-gray-400"
>
<circle cx="8" cy="8" r="7" stroke="currentColor" />
<text
x="8"
y="11"
textAnchor="middle"
fontSize="10"
fill="currentColor"
>
?
</text>
</svg>
</button>
)}
/>
<Tooltip.Positioner side="right" alignment="start" sideOffset={8}>
<Tooltip.Portal>
<Tooltip.Popup
render={(props) => (
<div
{...props}
className="bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-md shadow-lg p-4 w-64"
>
<h4 className="font-semibold mb-2 text-gray-900 dark:text-gray-100">
Helpful Information
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
This tooltip contains rich content including headings,
paragraphs, and links.
</p>
<a
href="#"
className="text-sm text-blue-600 hover:underline"
onClick={(e) => e.preventDefault()}
>
Learn more →
</a>
</div>
)}
/>
<Tooltip.Arrow
render={(props) => (
<div
{...props}
className="w-3 h-3 rotate-45 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700"
/>
)}
/>
</Tooltip.Portal>
</Tooltip.Positioner>
</Tooltip.Root>
);
}
/**
* Disabled State Example
*/
export function DisabledTooltipExample() {
return (
<div className="space-x-4">
<Tooltip.Root>
<Tooltip.Trigger
render={(props) => (
<button
{...props}
disabled
className="px-4 py-2 bg-gray-300 text-gray-500 rounded-md cursor-not-allowed"
>
Disabled button
</button>
)}
/>
<Tooltip.Positioner side="top">
<Tooltip.Portal>
<Tooltip.Popup
render={(props) => (
<div
{...props}
className="bg-gray-900 text-white text-sm px-3 py-2 rounded-md"
>
This button is disabled
</div>
)}
/>
</Tooltip.Portal>
</Tooltip.Positioner>
</Tooltip.Root>
</div>
);
}
/**
* Multiple Tooltips with Provider
* Share delay settings across multiple tooltips
*/
export function TooltipProviderExample() {
return (
<Tooltip.Provider delay={300} closeDelay={100}>
<div className="flex gap-4">
<Tooltip.Root>
<Tooltip.Trigger
render={(props) => (
<button {...props} className="px-4 py-2 border rounded-md">
First
</button>
)}
/>
<Tooltip.Positioner side="top">
<Tooltip.Portal>
<Tooltip.Popup
render={(props) => (
<div {...props} className="bg-gray-900 text-white px-3 py-2 rounded-md">
First tooltip
</div>
)}
/>
</Tooltip.Portal>
</Tooltip.Positioner>
</Tooltip.Root>
<Tooltip.Root>
<Tooltip.Trigger
render={(props) => (
<button {...props} className="px-4 py-2 border rounded-md">
Second
</button>
)}
/>
<Tooltip.Positioner side="top">
<Tooltip.Portal>
<Tooltip.Popup
render={(props) => (
<div {...props} className="bg-gray-900 text-white px-3 py-2 rounded-md">
Second tooltip
</div>
)}
/>
</Tooltip.Portal>
</Tooltip.Positioner>
</Tooltip.Root>
</div>
</Tooltip.Provider>
);
}
/**
* Positioning Variations
*/
export function TooltipPositionsExample() {
const positions: Array<{
side: "top" | "right" | "bottom" | "left";
alignment: "start" | "center" | "end";
}> = [
{ side: "top", alignment: "center" },
{ side: "right", alignment: "center" },
{ side: "bottom", alignment: "center" },
{ side: "left", alignment: "center" },
];
return (
<div className="grid grid-cols-2 gap-4">
{positions.map(({ side, alignment }) => (
<Tooltip.Root key={`${side}-${alignment}`}>
<Tooltip.Trigger
render={(props) => (
<button
{...props}
className="px-4 py-2 border border-gray-300 rounded-md hover:border-gray-400"
>
{side}
</button>
)}
/>
<Tooltip.Positioner side={side} alignment={alignment} sideOffset={4}>
<Tooltip.Portal>
<Tooltip.Popup
render={(props) => (
<div
{...props}
className="bg-gray-900 text-white text-sm px-3 py-2 rounded-md"
>
Tooltip on {side}
</div>
)}
/>
<Tooltip.Arrow
render={(props) => (
<div {...props} className="w-2 h-2 rotate-45 bg-gray-900" />
)}
/>
</Tooltip.Portal>
</Tooltip.Positioner>
</Tooltip.Root>
))}
</div>
);
}
/**
* Common Pitfalls:
*
* ❌ Not keyboard accessible
* <div onMouseEnter={show}>...</div> {/* No keyboard support */}
*
* ✅ Use Tooltip.Trigger
* <Tooltip.Trigger render={(props) => <button {...props}>...</button>} />
*
* ❌ Missing Positioner
* <Tooltip.Popup /> {/* Won't position correctly */}
*
* ✅ Wrap in Positioner
* <Tooltip.Positioner>
* <Tooltip.Portal>
* <Tooltip.Popup />
* </Tooltip.Portal>
* </Tooltip.Positioner>
*
* ❌ Tooltip on disabled button doesn't show
* <Tooltip.Trigger disabled /> {/* Pointer events disabled */}
*
* ✅ Wrap disabled element
* <Tooltip.Trigger>
* <span className="inline-block">
* <button disabled>...</button>
* </span>
* </Tooltip.Trigger>
*/