Appica UIUI 1.1.0

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:

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:

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:

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 inside render.

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. Pointing container at 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.

© 2026 Appica UI. A free component library, crafted by the Appica team.