Composition
Swap elements, compose with your router, merge styles.
Appica UI components ship fully styled, but you often need them to be something
else - a navigation link that's actually your router's Link, a tooltip trigger
that's actually your own button. The render prop, inherited from
Base UI, makes that possible without losing the
component's styling or behavior.
For the related case of a plain link that should merely look like a button, use the exported variant helpers instead - see Links that look like a button.
Rendering as a different element
Pass an element to render and the component adopts it as its output, merging in
its own props, classes, and event handlers. For instance, render a Badge as an
<a> so a status chip becomes a real link:
Don't render links as buttons
This is safe because Badge adds no interactive semantics. Button is different - it enforces button
semantics (role, keyboard handling), so rendering an <a> through it produces a link masquerading as a
button, which misleads assistive tech. To make a link merely look like a button, style the <a> directly -
see Links that look like a button.
Rendering as a component
render also accepts a component element - ideal for framework routers. A
NavigationLink stays a link while delegating navigation to your router's Link:
The same pattern composes the interactive parts of larger components. For instance, make a tooltip's trigger your own button:
Links that look like a button
Sometimes you don't want to change a component's element - you want a plain link
(or other element) to simply wear a component's styling. Don't reach for render
here: rendering an <a> through Button forces button semantics onto a link.
Instead, apply the exported buttonVariants classes directly to the element that
already has the right semantics:
Why not just render the link as a Button?
Links and buttons have different, non-interchangeable semantics: a link navigates, a button performs an
action. The render prop is for adopting a different element while keeping the component's role - not for
repainting a link as a button. Styling the <a> with buttonVariants keeps link semantics intact and gives
you the identical look.
buttonVariants isn't the only one - several components export their variant
recipe for exactly this purpose:
Each takes the same variant props as its component and returns a className string,
so they drop into any element and compose with your own classes through cn or a
template literal.
Where to put className
This is the one rule worth memorizing. When composing a render-prop wrapper:
- Visual overrides (
className,style) go on the wrapper. - Structural/behavioral props (
href,type,variant,size, rendering as a different element) go on the JSX insiderender.
Why this matters
Putting className on the inner JSX can cause a React hydration mismatch in Server Components: the styled element
is built on the server, serialized across the boundary, then cloned on the client, and the final class strings can
disagree. Keeping className on the wrapper avoids it - and class precedence still works, since the wrapper's classes
are merged last and win on conflicts.
If you specifically need a class on the inner element, extract the trigger into
its own 'use client' component rather than promoting the whole page to client.
Class merging
Overrides are merged with tailwind-merge, so conflicting utilities are de-duplicated and the last one wins. You don't need to fight specificity:
Reaching the elements around a popup
A popup is more than the box you see. Floating components (Select, Popover,
Tooltip, menus) render a portal, a positioner and the popup itself; modal ones
(Dialog, AlertDialog, Drawer) render a portal, a backdrop, a viewport and the
popup. *Content is the popup, so its className, style and render land
there. The rest are reached through escape-hatch props:
Each accepts what the element it targets accepts, and className merges with the
component's own rather than replacing it:
You rarely need positionerProps for positioning: side, align, sideOffset,
alignOffset, anchor and the collision props are already flat props on
*Content. The escape hatch is for the long tail (style, render, a class).
Its most common use is theming a popup opened from a scoped dark region - see Dark Mode.
Portals and the container prop
Every popup renders into <body>, which is what lets it escape an ancestor's
overflow: hidden and any stacking context that would trap it. container
retargets that portal. It takes an element, a ShadowRoot, or a ref to either:
Reach for it when the default is genuinely wrong:
- Shadow DOM. A popup in
<body>sits outside your shadow root, so the styles defined inside it don't apply. Pointingcontainerat the root brings the popup back under them. - An iframe. A popup has to live in the same document as the trigger.
- A stacking context you own, when a popup must sit under something else in the page's paint order.
Not for theming, and not a general escape hatch
Aiming container at some panel in your layout brings back everything portaling avoids: the panel's overflow: hidden clips the popup again, and z-50 starts competing inside that panel's stacking context instead of the
page's.
It's worse for Dialog, AlertDialog and Drawer, whose backdrop and viewport are position: fixed. Any ancestor with
transform, filter, backdrop-filter, contain or will-change becomes their containing block, and a
full-screen overlay collapses to that ancestor's box. Those properties are common - the library's own surfaces use
backdrop-blur and Motion writes transforms - so this fails more often than it works.
To theme a popup, put the class on the positioner or viewport instead.