-
+
)
diff --git a/src/components/ds/overlay-audit/BuilderGuideDialogSpecimen.tsx b/src/components/ds/overlay-audit/BuilderGuideDialogSpecimen.tsx
index b3e9d507e..50ed9d6d8 100644
--- a/src/components/ds/overlay-audit/BuilderGuideDialogSpecimen.tsx
+++ b/src/components/ds/overlay-audit/BuilderGuideDialogSpecimen.tsx
@@ -1,4 +1,4 @@
-import * as DialogPrimitive from '@radix-ui/react-dialog'
+import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
import { ArrowSquareOutIcon, XIcon } from '@phosphor-icons/react'
import { Button } from '~/components/ds/ui'
import type { SpecimenProps } from './types'
@@ -9,10 +9,13 @@ import type { SpecimenProps } from './types'
*
* The strongest specimen in the set and the best starting point for the DS
* primitive: semantic tokens throughout (`border-border-default`,
- * `bg-background-surface`, `text-text-primary`), real enter/exit animation via
- * Radix data-state, a fixed header over an independently scrolling body, and a
- * responsive posture that is full-bleed on mobile and a right-edge sheet from
- * `sm` up.
+ * `bg-background-surface`, `text-text-primary`), a fixed header over an
+ * independently scrolling body, and a responsive posture that is full-bleed on
+ * mobile and a right-edge sheet from `sm` up.
+ *
+ * It animated nothing, though — the `animate-in` / `slide-in-from-right`
+ * classes it carried come from tailwindcss-animate, which is not installed.
+ * Dropped rather than translated: motion is the DS primitive's job now.
*/
export function BuilderGuideDialogSpecimen({
open,
@@ -21,8 +24,8 @@ export function BuilderGuideDialogSpecimen({
return (
-
-
+
+
Builder guide
@@ -32,17 +35,19 @@ export function BuilderGuideDialogSpecimen({
Plain text
-
-
-
+
+
+
+ }
+ />
@@ -64,7 +69,7 @@ export function BuilderGuideDialogSpecimen({
))}
-
+
)
diff --git a/src/components/ds/overlay-audit/CartDrawerSpecimen.tsx b/src/components/ds/overlay-audit/CartDrawerSpecimen.tsx
index 3f1813134..46cf01130 100644
--- a/src/components/ds/overlay-audit/CartDrawerSpecimen.tsx
+++ b/src/components/ds/overlay-audit/CartDrawerSpecimen.tsx
@@ -1,4 +1,4 @@
-import * as Dialog from '@radix-ui/react-dialog'
+import { Dialog } from '@base-ui/react/dialog'
import { ShoppingCartIcon, XIcon } from '@phosphor-icons/react'
import { twMerge } from 'tailwind-merge'
import type { SpecimenProps } from './types'
@@ -19,8 +19,8 @@ export function CartDrawerSpecimen({ open, onOpenChange }: SpecimenProps) {
return (
-
-
+
-
+
)
diff --git a/src/components/ds/overlay-audit/ExampleDeployDialogSpecimen.tsx b/src/components/ds/overlay-audit/ExampleDeployDialogSpecimen.tsx
index 300bbf191..29c0eb686 100644
--- a/src/components/ds/overlay-audit/ExampleDeployDialogSpecimen.tsx
+++ b/src/components/ds/overlay-audit/ExampleDeployDialogSpecimen.tsx
@@ -22,7 +22,7 @@ const STEPS: Array = [
*
* Note the shell is byte-identical to StarterDeployDialogSpecimen: same
* `fixed inset-0 z-50`, same button-as-backdrop, same
- * `max-w-md ... rounded-xl shadow-2xl border` panel. Neither uses Radix, so
+ * `max-w-md ... rounded-xl shadow-2xl border` panel. Neither used Base UI, so
* neither traps focus, restores focus on close, or closes on Escape.
*
* The step switcher is an audit affordance, not part of the original — it
diff --git a/src/components/ds/overlay-audit/LibrariesOverlaySpecimen.tsx b/src/components/ds/overlay-audit/LibrariesOverlaySpecimen.tsx
index cfd1e3ade..1dab501b0 100644
--- a/src/components/ds/overlay-audit/LibrariesOverlaySpecimen.tsx
+++ b/src/components/ds/overlay-audit/LibrariesOverlaySpecimen.tsx
@@ -1,4 +1,4 @@
-import * as DialogPrimitive from '@radix-ui/react-dialog'
+import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
import { XIcon } from '@phosphor-icons/react'
import type { SpecimenProps } from './types'
@@ -18,8 +18,8 @@ export function LibrariesOverlaySpecimen({
return (
-
-
+
+
Libraries
@@ -57,7 +57,7 @@ export function LibrariesOverlaySpecimen({
))}
-
+
)
diff --git a/src/components/ds/overlay-audit/LoginModalSpecimen.tsx b/src/components/ds/overlay-audit/LoginModalSpecimen.tsx
index 0ba27230f..9fbd508d3 100644
--- a/src/components/ds/overlay-audit/LoginModalSpecimen.tsx
+++ b/src/components/ds/overlay-audit/LoginModalSpecimen.tsx
@@ -1,4 +1,4 @@
-import * as DialogPrimitive from '@radix-ui/react-dialog'
+import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
import { XIcon } from '@phosphor-icons/react/X'
import type { SpecimenProps } from './types'
@@ -13,8 +13,8 @@ export function LoginModalSpecimen({ open, onOpenChange }: SpecimenProps) {
return (
-
-
+
+
Sign in to continue
@@ -45,7 +45,7 @@ export function LoginModalSpecimen({ open, onOpenChange }: SpecimenProps) {
Continue with Google
-
+
)
diff --git a/src/components/ds/overlay-audit/NpmStatsDialogSpecimen.tsx b/src/components/ds/overlay-audit/NpmStatsDialogSpecimen.tsx
index d41585da9..3170ebb74 100644
--- a/src/components/ds/overlay-audit/NpmStatsDialogSpecimen.tsx
+++ b/src/components/ds/overlay-audit/NpmStatsDialogSpecimen.tsx
@@ -1,19 +1,19 @@
-import * as DialogPrimitive from '@radix-ui/react-dialog'
+import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
import { XIcon } from '@phosphor-icons/react'
import type { SpecimenProps } from './types'
/**
* AUDIT SPECIMEN — verbatim shell of the inline dialog in
* `src/routes/stats/npm/index.tsx` (~line 1084). A fourth independent
- * re-declaration of the centered Radix panel, defined inside a route file
+ * re-declaration of the centered Base UI panel, defined inside a route file
* rather than a component.
*/
export function NpmStatsDialogSpecimen({ open, onOpenChange }: SpecimenProps) {
return (
-
-
+
+
Add up to five npm packages to chart their download trends together.
-
+
)
diff --git a/src/components/ds/overlay-audit/ProductDrawerSpecimen.tsx b/src/components/ds/overlay-audit/ProductDrawerSpecimen.tsx
index e56be6664..ae8ea80de 100644
--- a/src/components/ds/overlay-audit/ProductDrawerSpecimen.tsx
+++ b/src/components/ds/overlay-audit/ProductDrawerSpecimen.tsx
@@ -7,7 +7,7 @@ import type { SpecimenProps } from './types'
* AUDIT SPECIMEN — verbatim shell of `src/components/shop/ProductDrawer.tsx`.
*
* The only bottom-sheet posture on the site, and hand-rolled rather than
- * Radix: the Escape handler below is copied from the original because there is
+ * Base UI: the Escape handler below is copied from the original because there is
* no primitive supplying it. Uses three separate z-tiers (`60` scrim, `70`
* sheet, `71` side arrows) and is the one overlay already wired to the motion
* tokens (`--motion-duration-fast`, `--motion-ease-standard`).
@@ -60,7 +60,8 @@ export function ProductDrawerSpecimen({ open, onOpenChange }: SpecimenProps) {
Classic Tee
A bottom-anchored sheet. Note there is no focus trap and no
- focus restoration on close — both would come free from Radix.
+ focus restoration on close — both come free from the DS Drawer
+ it was migrated onto.
-
+
)
diff --git a/src/components/ds/overlay-audit/specimen-meta.ts b/src/components/ds/overlay-audit/specimen-meta.ts
index fa3ab110f..93ff5fd58 100644
--- a/src/components/ds/overlay-audit/specimen-meta.ts
+++ b/src/components/ds/overlay-audit/specimen-meta.ts
@@ -11,7 +11,7 @@ export type Posture =
| 'top-anchored'
| 'full-bleed'
-export type Base = 'radix' | 'hand-rolled'
+export type Base = 'base-ui' | 'hand-rolled'
export type TokenLayer = 'semantic' | 'raw-tailwind' | 'shop-scope'
@@ -26,7 +26,7 @@ export type SpecimenMeta = {
zIndex: string
overlay: string
width: string
- /** Radix supplies these for free; hand-rolled dialogs must implement them. */
+ /** Base UI supplies these for free; hand-rolled dialogs must implement them. */
focusTrap: boolean
focusRestore: boolean
escape: boolean
@@ -42,7 +42,7 @@ export const SPECIMENS: Array = [
source: 'src/components/LoginModal.tsx',
sourceLines: 73,
posture: 'centered',
- base: 'radix',
+ base: 'base-ui',
tokens: 'raw-tailwind',
zIndex: '999 / 1000',
overlay: 'bg-black/60 backdrop-blur-sm',
@@ -61,7 +61,7 @@ export const SPECIMENS: Array = [
source: 'src/components/AvatarCropModal.tsx',
sourceLines: 176,
posture: 'centered',
- base: 'radix',
+ base: 'base-ui',
tokens: 'raw-tailwind',
zIndex: '999 / 1000',
overlay: 'bg-black/60 backdrop-blur-sm',
@@ -80,7 +80,7 @@ export const SPECIMENS: Array = [
source: 'src/routes/stats/npm/index.tsx',
sourceLines: 40,
posture: 'centered',
- base: 'radix',
+ base: 'base-ui',
tokens: 'raw-tailwind',
zIndex: '999 / 1000',
overlay: 'bg-black/60 backdrop-blur-sm',
@@ -156,7 +156,7 @@ export const SPECIMENS: Array = [
source: 'src/components/charts/BuilderGuideDialog.tsx',
sourceLines: 150,
posture: 'edge-sheet',
- base: 'radix',
+ base: 'base-ui',
tokens: 'semantic',
zIndex: '999 / 1000',
overlay: 'bg-black/45 backdrop-blur-[1px]',
@@ -175,7 +175,7 @@ export const SPECIMENS: Array = [
source: 'src/components/shop/CartDrawer.tsx',
sourceLines: 257,
posture: 'anchored-panel',
- base: 'radix',
+ base: 'base-ui',
tokens: 'shop-scope',
zIndex: '100',
overlay: 'bg-black/40 (no blur)',
@@ -205,7 +205,7 @@ export const SPECIMENS: Array = [
scrollLock: false,
animated: true,
notes:
- 'MIGRATED — now Drawer side="bottom" fit. Was the last hand-rolled overlay on the site; Radix now supplies the focus trap, focus restoration, Escape and scroll lock it never had. Its prev/next arrows moved from viewport-fixed on a third z-tier to panel-absolute, because Radix traps focus inside the panel and viewport-level siblings would have been unreachable by keyboard.',
+ 'MIGRATED — now Drawer side="bottom" fit. Was the last hand-rolled overlay on the site; Base UI now supplies the focus trap, focus restoration, Escape and scroll lock it never had. Its prev/next arrows moved from viewport-fixed on a third z-tier to panel-absolute, because focus is trapped inside the panel and viewport-level siblings would have been unreachable by keyboard.',
},
{
id: 'libraries-overlay',
@@ -213,7 +213,7 @@ export const SPECIMENS: Array = [
source: 'src/components/LibrariesOverlay.tsx',
sourceLines: 57,
posture: 'full-bleed',
- base: 'radix',
+ base: 'base-ui',
tokens: 'semantic',
zIndex: '110 / 111 / 112',
overlay: 'bespoke .libraries-overlay-glass',
@@ -224,7 +224,7 @@ export const SPECIMENS: Array = [
scrollLock: true,
animated: true,
notes:
- 'MIGRATED — now Takeover with scrim="glass". Its bespoke glass treatment became the DS\'s second scrim, which is the pair the audit asked for after finding seven. LibrariesBrowser also dropped its direct Radix import in favour of TakeoverTitle / TakeoverDescription.',
+ 'MIGRATED — now Takeover with scrim="glass". Its bespoke glass treatment became the DS\'s second scrim, which is the pair the audit asked for after finding seven. LibrariesBrowser also dropped its direct Base UI import in favour of TakeoverTitle / TakeoverDescription.',
},
{
id: 'search-modal',
@@ -232,7 +232,7 @@ export const SPECIMENS: Array = [
source: 'src/components/SearchModal.tsx',
sourceLines: 3766,
posture: 'top-anchored',
- base: 'radix',
+ base: 'base-ui',
tokens: 'raw-tailwind',
zIndex: '999 / 1000',
overlay: 'bg-black/60 → xl:bg-black/30',
@@ -243,7 +243,7 @@ export const SPECIMENS: Array = [
scrollLock: true,
animated: true,
notes:
- 'TOKENS ADOPTED, POSTURE NOT EXTRACTED. Now on bg-scrim and the --z-scrim / --z-overlay / --z-above-overlay tiers, which closes the last bespoke values in the audit. The shell itself stays: it is already Radix, already accessible, and already has real data-state animation, so extracting a CommandPalette buys no correctness — and it would need five caller-specific escape hatches (forceMount on Portal/Overlay/Content to keep InstantSearch state alive; animation on an inner panel because Content is a full-bleed hit area on mobile; the top-anchored responsive posture; a conditional sm:bottom-4; and a scrim that lightens at xl). One caller is below the extraction threshold. Revisit if a second command palette appears.',
+ 'TOKENS ADOPTED, POSTURE NOT EXTRACTED. Now on bg-scrim and the --z-scrim / --z-overlay / --z-above-overlay tiers, which closes the last bespoke values in the audit. The shell itself stays: it is already on the dialog primitive, already accessible, and already animates, so extracting a CommandPalette buys no correctness — and it would need four caller-specific escape hatches (animation on an inner panel because the popup is a full-bleed hit area on mobile; the top-anchored responsive posture; a conditional sm:bottom-4; and a scrim that lightens at xl). One caller is below the extraction threshold. Revisit if a second command palette appears.',
},
]
@@ -271,9 +271,9 @@ export const DIVERGENCE = [
},
{
property: 'Base',
- values: ['Radix (7)', 'hand-rolled (4)'],
+ values: ['Base UI (7)', 'hand-rolled (4)'],
verdict:
- 'The 4 hand-rolled ones accounted for every a11y failure in the audit. All four are now migrated — every overlay on the site is Radix-backed.',
+ 'The 4 hand-rolled ones accounted for every a11y failure in the audit. All four are now migrated — every overlay on the site is Base UI-backed.',
},
{
property: 'Token layer',
diff --git a/src/components/ds/ui/Dialog.tsx b/src/components/ds/ui/Dialog.tsx
index 88e86077f..5e523c2d6 100644
--- a/src/components/ds/ui/Dialog.tsx
+++ b/src/components/ds/ui/Dialog.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
-import * as DialogPrimitive from '@radix-ui/react-dialog'
+import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
import { CircleNotchIcon } from '@phosphor-icons/react/CircleNotch'
import { XIcon } from '@phosphor-icons/react/X'
import { twMerge } from 'tailwind-merge'
@@ -7,7 +7,7 @@ import { twMerge } from 'tailwind-merge'
/**
* Centered modal dialog.
*
- * Always Radix-backed, which is the whole point: focus trapping, focus
+ * Always Base UI-backed, which is the whole point: focus trapping, focus
* restoration on close, Escape-to-dismiss and scroll lock come from the
* primitive rather than from each call site remembering to implement them.
* The overlay audit at /ds/overlays found that every accessibility failure on
@@ -43,27 +43,21 @@ type DialogContentProps = {
children: React.ReactNode
size?: DialogSize
className?: string
- /** Escape hatch for content that manages its own dismissal (e.g. a wizard mid-submit). */
- onInteractOutside?: DialogPrimitive.DialogContentProps['onInteractOutside']
}
export const DialogContent = React.forwardRef<
HTMLDivElement,
DialogContentProps
->(function DialogContent(
- { children, size = 'sm', className, onInteractOutside },
- ref,
-) {
+>(function DialogContent({ children, size = 'sm', className }, ref) {
return (
-
-
{children}
-
+
)
})
@@ -92,8 +86,8 @@ export const DialogContent = React.forwardRef<
type DialogHeaderProps = {
title: React.ReactNode
/**
- * Rendered under the title. Radix warns when a dialog has no description, so
- * when this is omitted the description is still emitted, visually hidden.
+ * Rendered under the title. A dialog without one is an accessibility gap,
+ * so when this is omitted the description is still emitted, visually hidden.
*/
description?: React.ReactNode
/** Extra controls placed left of the close button. */
diff --git a/src/components/ds/ui/Drawer.tsx b/src/components/ds/ui/Drawer.tsx
index d3502682d..d9ffc561b 100644
--- a/src/components/ds/ui/Drawer.tsx
+++ b/src/components/ds/ui/Drawer.tsx
@@ -1,12 +1,12 @@
import * as React from 'react'
-import * as DialogPrimitive from '@radix-ui/react-dialog'
+import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
import { XIcon } from '@phosphor-icons/react/X'
import { twMerge } from 'tailwind-merge'
/**
* Edge-anchored panel — the drawer / side sheet.
*
- * Same Radix foundation as Dialog (focus trap, focus restoration, Escape,
+ * Same Base UI foundation as Dialog (focus trap, focus restoration, Escape,
* scroll lock) and the same three-region layout, but anchored to an edge
* instead of centred. `side` covers right, left and bottom: they are one
* mechanism — a surface arriving from off-screen — differing only in axis.
@@ -119,7 +119,6 @@ type DrawerContentProps = {
*/
fit?: boolean
className?: string
- onInteractOutside?: DialogPrimitive.DialogContentProps['onInteractOutside']
}
export const DrawerContent = React.forwardRef<
@@ -133,21 +132,19 @@ export const DrawerContent = React.forwardRef<
anchor = 'viewport',
fit = false,
className,
- onInteractOutside,
},
ref,
) {
return (
-
-
{children}
-
+
)
})
diff --git a/src/components/ds/ui/Takeover.tsx b/src/components/ds/ui/Takeover.tsx
index 0adddc8ae..478d013eb 100644
--- a/src/components/ds/ui/Takeover.tsx
+++ b/src/components/ds/ui/Takeover.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
-import * as DialogPrimitive from '@radix-ui/react-dialog'
+import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
import { XIcon } from '@phosphor-icons/react/X'
import { twMerge } from 'tailwind-merge'
@@ -50,7 +50,6 @@ type TakeoverContentProps = {
*/
dismissOnBackdropClick?: boolean
className?: string
- onInteractOutside?: DialogPrimitive.DialogContentProps['onInteractOutside']
}
export const TakeoverContent = React.forwardRef<
@@ -64,25 +63,23 @@ export const TakeoverContent = React.forwardRef<
closeLabel = 'Close',
dismissOnBackdropClick = true,
className,
- onInteractOutside,
},
ref,
) {
- // The content fills the viewport, so Radix never sees a click as "outside".
+ // The content fills the viewport, so a click is never "outside" it.
// Backdrop dismissal therefore routes through the close button we already
// render, rather than a second dismissal path that could drift from it.
const closeRef = React.useRef(null)
return (
-
- {
@@ -112,7 +109,7 @@ export const TakeoverContent = React.forwardRef<
{children}
-
+
)
})
diff --git a/src/components/ds/ui/index.tsx b/src/components/ds/ui/index.tsx
index b3725f4f6..ae9c34e0a 100644
--- a/src/components/ds/ui/index.tsx
+++ b/src/components/ds/ui/index.tsx
@@ -1,6 +1,6 @@
import * as React from 'react'
import { twMerge } from 'tailwind-merge'
-import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
+import { Menu } from '@base-ui/react/menu'
import { Link } from '@tanstack/react-router'
import { CaretDownIcon } from '@phosphor-icons/react/CaretDown'
import { CircleNotchIcon } from '@phosphor-icons/react/CircleNotch'
@@ -713,26 +713,20 @@ export function Dropdown({
modal?: boolean
}) {
return (
-
+
{children}
-
+
)
}
export function DropdownTrigger({
- children,
+ render,
className,
- asChild = true,
}: {
- children: React.ReactNode
+ render: React.ReactElement
className?: string
- asChild?: boolean
}) {
- return (
-
- {children}
-
- )
+ return
}
export function DropdownContent({
@@ -762,29 +756,33 @@ export function DropdownContent({
}) {
const scrollable = maxHeight !== undefined
return (
-
-
+
- {children}
-
-
+
+ {children}
+
+
+
)
}
@@ -792,30 +790,34 @@ export function DropdownItem({
children,
className,
onSelect,
- asChild,
+ render,
}: {
- children: React.ReactNode
+ children?: React.ReactNode
className?: string
onSelect?: () => void
- asChild?: boolean
+ render?: React.ReactElement
}) {
+ const itemClassName = twMerge(
+ 'flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm text-text-secondary outline-none transition-colors hover:bg-background-subtle hover:text-text-primary data-highlighted:bg-background-subtle data-highlighted:text-text-primary',
+ className,
+ )
+
+ if (render) {
+ return (
+
+ )
+ }
+
return (
-
+
{children}
-
+
)
}
export function DropdownSeparator({ className }: { className?: string }) {
return (
-
-
-
-
+
+ On this page
+
+
+ }
+ />
{headings.map((heading) => (
-
-
-
-
-
+
+
+
+ }
+ />
))}
diff --git a/src/components/examples/ExampleWorkbench.client.tsx b/src/components/examples/ExampleWorkbench.client.tsx
index 2d0054cf5..619113ea7 100644
--- a/src/components/examples/ExampleWorkbench.client.tsx
+++ b/src/components/examples/ExampleWorkbench.client.tsx
@@ -3474,7 +3474,7 @@ export function ExampleWorkbench({
if (!open) setBuilderArrangeTabId(undefined)
}}
>
- {tabButton}
+
-
-
-
+
+
+
+ }
+ />
-
-
-
+
+
+
+ }
+ />
{openExternalUrl ? (
-
-
-
- Open in new tab
-
-
+
+
+ Open in new tab
+
+ }
+ />
) : null}
{captureScreenshot ? (
void takeScreenshot()}>
diff --git a/src/components/npm-stats/BaselineSection.tsx b/src/components/npm-stats/BaselineSection.tsx
index b2132ce3e..c2010d371 100644
--- a/src/components/npm-stats/BaselineSection.tsx
+++ b/src/components/npm-stats/BaselineSection.tsx
@@ -8,12 +8,7 @@ import {
EyeIcon,
EyeSlashIcon,
} from '@phosphor-icons/react'
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from '@radix-ui/react-dropdown-menu'
+import { Menu } from '@base-ui/react/menu'
import { twMerge } from 'tailwind-merge'
import { Tooltip } from '~/components/Tooltip'
import {
@@ -154,89 +149,88 @@ export function BaselineSection({
)
const presetsMenu = (
-
+
-
-
-
-
-
-
)}
diff --git a/src/components/shop/ProductDrawer.tsx b/src/components/shop/ProductDrawer.tsx
index 6c7716d42..6525c3bbb 100644
--- a/src/components/shop/ProductDrawer.tsx
+++ b/src/components/shop/ProductDrawer.tsx
@@ -228,7 +228,7 @@ export function ProductDrawer({
{allHandles.length > 1 ? (
<>
- {/* Anchored to the panel, not the viewport. Radix traps focus
+ {/* Anchored to the panel, not the viewport. Base UI traps focus
inside the panel, so viewport-level siblings would be
unreachable by keyboard — and it retires the third z-tier the
overlay audit flagged. */}
diff --git a/src/routes/ds.buttons.tsx b/src/routes/ds.buttons.tsx
index d90248302..db838fe48 100644
--- a/src/routes/ds.buttons.tsx
+++ b/src/routes/ds.buttons.tsx
@@ -351,11 +351,13 @@ function ButtonsPage() {
code={`
-
-
-
+
+
+
+ }
+ />
Deploy to productionDeploy a preview
@@ -368,15 +370,17 @@ function ButtonsPage() {
-
-
-
+
+
+
+ }
+ />
Deploy to productionDeploy a preview
diff --git a/src/routes/ds.cards.tsx b/src/routes/ds.cards.tsx
index 142362079..8238aea5e 100644
--- a/src/routes/ds.cards.tsx
+++ b/src/routes/ds.cards.tsx
@@ -158,7 +158,7 @@ function CardsPage() {
diff --git a/src/routes/ds.dialog.tsx b/src/routes/ds.dialog.tsx
index 4a1172972..9ea6897cf 100644
--- a/src/routes/ds.dialog.tsx
+++ b/src/routes/ds.dialog.tsx
@@ -27,7 +27,7 @@ export const Route = createFileRoute('/ds/dialog')({
meta: seo({
title: 'Dialog | TanStack Design System',
description:
- 'The centered modal dialog — Radix-backed, on the semantic token layer.',
+ 'The centered modal dialog — Base UI-backed, on the semantic token layer.',
}),
}),
})
@@ -116,7 +116,7 @@ function DialogPage() {
return (
…
-
-
-
+ Cancel
+ }
+ />
@@ -155,11 +157,13 @@ function DialogPage() {
-
-
-
+
+ Cancel
+
+ }
+ />
@@ -196,9 +200,7 @@ function DialogPage() {
-
-
-
+ Close} />
@@ -235,11 +237,13 @@ function DialogPage() {
))}
-
-
-
+
+ Decline
+
+ }
+ />
@@ -255,9 +259,11 @@ function DialogPage() {
code={`
-
-
-
+ Cancel
+ }
+ />
`}
@@ -272,11 +278,13 @@ function DialogPage() {
description="Are you sure you want to remove this user from the role? This cannot be undone."
/>
-
-
-
+
+ Cancel
+
+ }
+ />
@@ -440,7 +448,7 @@ function DialogPage() {
diff --git a/src/routes/ds.drawer.tsx b/src/routes/ds.drawer.tsx
index 0b6f3c80e..ce346bb4a 100644
--- a/src/routes/ds.drawer.tsx
+++ b/src/routes/ds.drawer.tsx
@@ -22,7 +22,7 @@ export const Route = createFileRoute('/ds/drawer')({
meta: seo({
title: 'Drawer | TanStack Design System',
description:
- 'The edge-anchored panel — right, left and bottom, on one Radix foundation.',
+ 'The edge-anchored panel — right, left and bottom, on one Base UI foundation.',
}),
}),
})
@@ -41,7 +41,7 @@ function DrawerPage() {
return (
-
-
-
+ Primary action}
+ />
@@ -318,9 +318,7 @@ function DrawerPage() {
Subtotal$56.00
{[
'The panel slides only — no fade. The scrim fades.',
- 'Exit animations play: Radix Presence waits for animationend before unmounting.',
+ 'Exit animations play: Base UI waits for the animation to finish before unmounting.',
'prefers-reduced-motion removes the movement but keeps the state change.',
'Bottom is centred with auto margins rather than a translate, so transform stays free for the slide — Tailwind v4 compiles -translate-x-1/2 to the independent translate property, which composes with transform rather than being replaced by it.',
].map((line) => (
diff --git a/src/routes/ds.dropdown.tsx b/src/routes/ds.dropdown.tsx
index b82d39f2b..4ae60dcd9 100644
--- a/src/routes/ds.dropdown.tsx
+++ b/src/routes/ds.dropdown.tsx
@@ -21,7 +21,7 @@ export const Route = createFileRoute('/ds/dropdown')({
head: () => ({
meta: seo({
title: 'Dropdown | TanStack Design System',
- description: 'The Radix-powered Dropdown menu.',
+ description: 'The Base UI-powered Dropdown menu.',
}),
}),
})
@@ -50,7 +50,7 @@ function DropdownPage() {
return (
-
-
-
+ Account }
+ />
Profile Settings
@@ -70,11 +70,13 @@ function DropdownPage() {
{/* Style (weight) — dropdown with a live preview per weight */}
-
-
-
+
+
+ {weight}
+
+
+ }
+ />
{WEIGHTS.map((w) => (
setWeight(w)}>
@@ -119,13 +121,15 @@ function IconographyPage() {
{/* Size — dropdown with a scaling size glyph per option */}
-
-
-
+
+
+ {size}px
+
+
+ }
+ />
{SIZES.map((s) => (
setSize(s)}>
diff --git a/src/routes/ds.overlays.tsx b/src/routes/ds.overlays.tsx
index c27a91afd..a027316e5 100644
--- a/src/routes/ds.overlays.tsx
+++ b/src/routes/ds.overlays.tsx
@@ -109,7 +109,7 @@ function OverlayAuditPage() {
{s.name}
{s.base === 'hand-rolled' ? (
raw
@@ -126,7 +126,7 @@ function OverlayAuditPage() {
diff --git a/src/routes/ds.tabs.tsx b/src/routes/ds.tabs.tsx
index 943be1208..c498788e8 100644
--- a/src/routes/ds.tabs.tsx
+++ b/src/routes/ds.tabs.tsx
@@ -79,13 +79,15 @@ function SizeSelector({
const current = TAB_SIZES.find((s) => s.value === value) ?? TAB_SIZES[1]
return (
-
-
-
+
+ {current.label}
+ {current.px}px
+
+
+ }
+ />
{TAB_SIZES.map((s) => (
onChange(s.value)}>
diff --git a/src/routes/ds.takeover.tsx b/src/routes/ds.takeover.tsx
index 2de71a5e9..1b02e0613 100644
--- a/src/routes/ds.takeover.tsx
+++ b/src/routes/ds.takeover.tsx
@@ -143,7 +143,7 @@ function TakeoverPage() {
'No panel and no header bar. TakeoverTitle / TakeoverDescription go wherever the content wants them — visible or sr-only.',
'The content element is the scroll container, so a takeover never nests a second scrollbar.',
'Clicking the content’s own empty space dismisses. It routes through the same close button rather than a second dismissal path that could drift from it.',
- 'onInteractOutside is forwarded, for content that portals menus of its own and must not close when they are clicked.',
+ 'Dismissal is the caller’s to veto: content that portals menus of its own cancels the outside press from the root’s onOpenChange, so clicking one does not close the takeover.',
'The surface fades rather than slides: nothing is arriving from an edge, the whole surface is being replaced.',
].map((line) => (
diff --git a/src/styles/app.css b/src/styles/app.css
index 10926a505..bcf91b2aa 100644
--- a/src/styles/app.css
+++ b/src/styles/app.css
@@ -179,13 +179,14 @@ html.theme-switching *::after {
}
/* ----------------------------------------------------------------- Dialog --
- Enter/exit motion for the DS Dialog, driven off Radix's data-state rather
+ Enter/exit motion for the DS Dialog, driven off Base UI's data-open /
+ data-closed rather
than utility classes. Written as real keyframes on purpose: the
`animate-in` / `fade-in-0` / `zoom-in-95` classes used elsewhere in this
codebase come from tailwindcss-animate, which is NOT installed — they match
no CSS rule and animate nothing. Timing reuses the existing motion tokens.
- Radix's Presence waits for animationend before unmounting, so the closed
+ Base UI waits for the animation to finish before unmounting, so the closed
state genuinely plays instead of being cut off.
The panel keyframes animate `transform` only. Tailwind v4's
@@ -231,19 +232,19 @@ html.theme-switching *::after {
}
}
-[data-ds-dialog-scrim][data-state='open'] {
+[data-ds-dialog-scrim][data-open] {
animation: dialog-scrim-in var(--motion-duration-fast)
var(--motion-ease-standard) both;
}
-[data-ds-dialog-scrim][data-state='closed'] {
+[data-ds-dialog-scrim][data-closed] {
animation: dialog-scrim-out var(--motion-duration-fast)
var(--motion-ease-standard) both;
}
-[data-ds-dialog-panel][data-state='open'] {
+[data-ds-dialog-panel][data-open] {
animation: dialog-panel-in var(--motion-duration-fast)
var(--motion-ease-standard) both;
}
-[data-ds-dialog-panel][data-state='closed'] {
+[data-ds-dialog-panel][data-closed] {
animation: dialog-panel-out var(--motion-duration-fast)
var(--motion-ease-standard) both;
}
@@ -262,8 +263,8 @@ html.theme-switching *::after {
`transform` carries scale/offset only, never a centring translate: Tailwind
v4 compiles `-translate-x-1/2` to the independent `translate` property,
which composes with `transform` rather than being replaced by it. That also
- leaves Radix's --radix-*-content-transform-origin free to govern where a
- popover grows from.
+ leaves Base UI's --transform-origin free to govern where a popover grows
+ from.
-------------------------------------------------------------------------- */
@keyframes ds-pop-in {
from {
@@ -286,17 +287,14 @@ html.theme-switching *::after {
}
}
-/* Radix-driven transient surfaces: tooltips, dropdown menus.
- Tooltip does not use `open` — it reports `delayed-open` when it appears
- after the hover delay and `instant-open` inside the skip-delay window, so
- all three have to be matched or tooltips silently never animate. */
-[data-ds-pop][data-state='open'],
-[data-ds-pop][data-state='delayed-open'],
-[data-ds-pop][data-state='instant-open'] {
+/* Transient surfaces: tooltips, dropdown menus. Base UI reports a single
+ `data-open` regardless of whether the tooltip appeared after the hover delay
+ or instantly inside the skip-delay window, so one selector covers both. */
+[data-ds-pop][data-open] {
animation: ds-pop-in var(--motion-duration-fast) var(--motion-ease-standard)
both;
}
-[data-ds-pop][data-state='closed'] {
+[data-ds-pop][data-closed] {
animation: ds-pop-out var(--motion-duration-fast) var(--motion-ease-standard)
both;
}
@@ -375,13 +373,13 @@ html.dark [data-ds-takeover-scrim='glass'] {
}
}
-[data-ds-takeover-scrim][data-state='open'],
-[data-ds-takeover-panel][data-state='open'] {
+[data-ds-takeover-scrim][data-open],
+[data-ds-takeover-panel][data-open] {
animation: takeover-in var(--motion-duration-fast) var(--motion-ease-standard)
both;
}
-[data-ds-takeover-scrim][data-state='closed'],
-[data-ds-takeover-panel][data-state='closed'] {
+[data-ds-takeover-scrim][data-closed],
+[data-ds-takeover-panel][data-closed] {
animation: takeover-out var(--motion-duration-fast)
var(--motion-ease-standard) both;
}
@@ -455,27 +453,27 @@ html.dark [data-ds-takeover-scrim='glass'] {
}
}
-[data-ds-drawer-panel][data-side='right'][data-state='open'] {
+[data-ds-drawer-panel][data-side='right'][data-open] {
animation: drawer-right-in var(--motion-duration-sheet)
var(--motion-ease-sheet) both;
}
-[data-ds-drawer-panel][data-side='right'][data-state='closed'] {
+[data-ds-drawer-panel][data-side='right'][data-closed] {
animation: drawer-right-out var(--motion-duration-sheet)
var(--motion-ease-sheet) both;
}
-[data-ds-drawer-panel][data-side='left'][data-state='open'] {
+[data-ds-drawer-panel][data-side='left'][data-open] {
animation: drawer-left-in var(--motion-duration-sheet)
var(--motion-ease-sheet) both;
}
-[data-ds-drawer-panel][data-side='left'][data-state='closed'] {
+[data-ds-drawer-panel][data-side='left'][data-closed] {
animation: drawer-left-out var(--motion-duration-sheet)
var(--motion-ease-sheet) both;
}
-[data-ds-drawer-panel][data-side='bottom'][data-state='open'] {
+[data-ds-drawer-panel][data-side='bottom'][data-open] {
animation: drawer-bottom-in var(--motion-duration-sheet)
var(--motion-ease-sheet) both;
}
-[data-ds-drawer-panel][data-side='bottom'][data-state='closed'] {
+[data-ds-drawer-panel][data-side='bottom'][data-closed] {
animation: drawer-bottom-out var(--motion-duration-sheet)
var(--motion-ease-sheet) both;
}
@@ -504,11 +502,11 @@ html.dark [data-ds-takeover-scrim='glass'] {
}
}
-[data-ds-drawer-scrim][data-state='open'] {
+[data-ds-drawer-scrim][data-open] {
animation: dialog-scrim-in var(--motion-duration-sheet)
var(--motion-ease-sheet) both;
}
-[data-ds-drawer-scrim][data-state='closed'] {
+[data-ds-drawer-scrim][data-closed] {
animation: dialog-scrim-out var(--motion-duration-sheet)
var(--motion-ease-sheet) both;
}
@@ -2749,7 +2747,7 @@ html:not(.dark) .library-landing-graphic [style*='drop-shadow'] {
animation: dropdown-in 150ms ease-out;
}
-.dropdown-content[data-state='closed'] {
+.dropdown-content[data-closed] {
animation: dropdown-out 100ms ease-in;
}
@@ -2794,11 +2792,11 @@ html:not(.dark) .library-landing-graphic [style*='drop-shadow'] {
}
}
-.search-modal-overlay[data-state='open'] {
+.search-modal-overlay[data-open] {
animation: search-modal-overlay-in 140ms ease-out both;
}
-.search-modal-overlay[data-state='closed'] {
+.search-modal-overlay[data-closed] {
animation: search-modal-overlay-out 120ms ease-in both;
}
@@ -2806,11 +2804,11 @@ html:not(.dark) .library-landing-graphic [style*='drop-shadow'] {
transform-origin: top center;
}
-.search-modal-content[data-state='open'] .search-modal-panel-transition {
+.search-modal-content[data-open] .search-modal-panel-transition {
animation: search-modal-panel-in 140ms ease-out both;
}
-.search-modal-content[data-state='closed'] .search-modal-panel-transition {
+.search-modal-content[data-closed] .search-modal-panel-transition {
animation: search-modal-panel-out 120ms ease-in both;
}
@@ -2862,19 +2860,19 @@ html:not(.dark) .library-landing-graphic [style*='drop-shadow'] {
}
}
-.cart-panel[data-state='open'] {
+.cart-panel[data-open] {
animation: cart-panel-in 200ms ease-out;
}
-.cart-panel[data-state='closed'] {
+.cart-panel[data-closed] {
animation: cart-panel-out 150ms ease-in;
}
-.cart-overlay[data-state='open'] {
+.cart-overlay[data-open] {
animation: cart-overlay-in 200ms ease-out;
}
-.cart-overlay[data-state='closed'] {
+.cart-overlay[data-closed] {
animation: cart-overlay-out 150ms ease-in;
}
diff --git a/src/ui/Tooltip.tsx b/src/ui/Tooltip.tsx
index 5d759704e..017223a63 100644
--- a/src/ui/Tooltip.tsx
+++ b/src/ui/Tooltip.tsx
@@ -1,9 +1,9 @@
import * as React from 'react'
-import * as TooltipPrimitive from '@radix-ui/react-tooltip'
+import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip'
import { twMerge } from 'tailwind-merge'
interface TooltipProps {
- children: React.ReactNode
+ children: React.ReactElement
content: React.ReactNode
side?: 'top' | 'right' | 'bottom' | 'left'
align?: 'start' | 'center' | 'end'
@@ -24,28 +24,25 @@ export function Tooltip({
}
return (
-
-
- {children}
-
-
+
+
+
+
{content}
-
-
-
-
-
+
+
+
+
+
)
}