Command Palette

Search for a command to run...

Styling

How className and render interact when you override a component's styles.

Every component merges the classes you give it with its own using tailwind-merge, so a conflicting utility replaces the component's rather than sitting alongside it and leaving the winner to stylesheet order.

Overriding with className

<Badge className="rounded-full" />

rounded-full replaces the component's rounded-sm. Non-conflicting classes are kept from both sides, so you only have to name what you're changing.

Overriding through render

render swaps the underlying element or composes the component with another one. A class you put on the render element behaves exactly like one passed via className — it wins:

<Badge render={<a className="rounded-full" />} />

These two spellings are interchangeable for styling purposes. Pick whichever reads better; render when you also need to change the element, className when you don't.

A render given as a function can't be inspected, so its classes are concatenated rather than merged. If you need an override to win, put it on the component's className instead.

Composing two components

When the render element is itself a design-system component, it merges what it is handed, so the outer component's classes reach it as ordinary input:

<InputGroup.Input render={<Input.Text />} />

Here InputGroup.Input contributes layout and Input.Text contributes the control's own styling. Container components own their frame and reset it on whatever they wrap — see Input Group — so you size the group rather than the control inside it.

Overriding size on a component that changes size

A spacing override applies to every state of a component, including the ones you weren't looking at when you wrote it. That is easy to miss, because the state you were solving for is the one in front of you.

The question to ask isn't did my override change what I was looking at — it's did it start applying somewhere I never looked.

// Written so a two-line value doesn't tower. Also applies to the one-line
// placeholder state, which now measures 33px next to 36px siblings.
<Select.Trigger className="h-auto py-1.5" />

h-auto removes the fixed frame, and py-1.5 is smaller than the padding it replaced, so the collapsed state shrinks. Nothing looks wrong in isolation — you notice it because the control no longer lines up with the input beside it.

Pair the override with a min-* companion, so the frame holds when the content is small and still grows when it isn't:

<Select.Trigger className="h-auto min-h-9 py-1.5" />

The same shape applies to width (min-w-0 on a flex child that must be allowed to shrink but not collapse). InputGroup does this in-package with has-[>textarea]:h-auto: the group keeps its control height until it actually wraps a textarea, rather than dropping the frame for every case.

This is checked, not just documented. The visual harness renders each size-varying component's collapsed and expanded states in a row with reference siblings and asserts they share a collapsed height — because a control that is 33px tall is only obviously wrong next to one that is 36px.

What a component can tell about your render

Styling isn't the only thing inferred across this seam. Tabs.Tab reads the render element to decide whether it's handing Base UI a real <button>, which determines the keyboard and ARIA semantics it applies — so a router-link tab gets anchor semantics without you asking:

<Tabs.Tab value="homes" render={<Link to="/homes" />}>
  Homes
</Tabs.Tab>

Both inferences read the same thing and stop at the same wall, so it's worth knowing what is and isn't visible to a component:

You passVisible?
nothingyes — the component knows its own default element
render={<a />}, render={<button />}yes — a literal tag
render={<Link />}, render={<Button />}no — a component's type says nothing about what it renders
render={(props) => …}no — a function can't be read at all

The last two rows are why an override that depends on the element has to be stated rather than guessed. For styling that means classes concatenate instead of merging.

nativeButton is about semantics the element doesn't already have

Button and Popover.Trigger infer nativeButton from what you hand them. The prop reads backwards easily: it does not mean "is this literally a <button> tag". It means should Base UI synthesize button semantics onto this elementrole="button", focusability, Space to activate.

Those come apart exactly where the element has semantics of its own:

You passInferredWhy
render={<div />}falsea div has none — without this it isn't focusable and ignores Space
render={<a href />}truealready a link; role="button" would announce navigation as a button
render={<button />}truealready a button
render={<Anything />}trueunreadable, and the usual reason to swap the element is to navigate

The anchor row is the one that bites. Passing nativeButton={false} alongside a link looks like the careful thing to do and is actively worse than leaving it off: a control that navigates stops being announced as a navigation.

If you're rendering something inert that really should act like a button, pass nativeButton={false} explicitly. It always wins over the inference.

Tabs.Tab keeps its own rule, because a tab is announced as role="tab" whatever it renders — the link-vs-button question never reaches the user there, and nativeButton only decides keyboard handling.

Where the item inset lives

Every item-list popup insets its items by the same 4px (p-component-xs), and it always lives on the innermost element the component renders no matter what you compose into it — which is also the element that scrolls:

  • Menu, Context Menu — the popup itself. Items go directly inside it, and it is its own scroll container.
  • Select, Command — the list inside the popup, because the popup has siblings that must reach its edges (Select's scroll arrows, Command's input).

Two things follow. Items never need a group or any other wrapper to be spaced correctly — grouping expresses grouping, not layout. And separators bleed to the popup's edges on their own (-mx-component-xs cancels the inset), so you never compensate for the inset when composing items and separators.

Popups with non-item content

A popup that mixes items with something else — a calendar, a header, a form — opts out with p-0 and re-applies the inset per section:

<Menu.Content className="p-0">
  <div className="flex">
    <div className="p-component-xs">{/* menu items */}</div>
    <div className="p-component-xs">{/* anything else */}</div>
  </div>
</Menu.Content>