Styling

Every part renders exactly one element and forwards className and style, so any styling approach works. bedrock.css is optional and is a demonstration, not a dependency.

State selectors

State is native. This is a deliberate divergence from Radix, which uses data-state="open" | "closed".

you want bedrock Radix
dialog is open dialog:open [data-state="open"]
popover is open :popover-open [data-state="open"]
checkbox is checked :checked [data-state="checked"]
the overlay ::backdrop a separate Overlay element

Why: data-state has to be written by JavaScript, which means it is a mirror that can lag or desync, and it cannot be set at all before hydration. A closed <dialog> matches :not(:open) from the moment the parser sees it.

The cost is real and worth naming: you cannot select an ancestor on a descendant's open state, and :has() is the only way back, which is why data-* attributes are still emitted for part identification (data-bedrock-dialog, data-bedrock-dialog-trigger, …) even though state is not.

Enter and exit transitions

No forceMount, no presence wrapper, no unmount timing in JavaScript. Three things do the work:

[data-bedrock-dialog] {
  opacity: 0;
  scale: 0.96;
  transition:
    opacity 180ms,
    scale 180ms,
    display 180ms allow-discrete, /* keeps it displayed while exiting */
    overlay 180ms allow-discrete; /* keeps it in the top layer while exiting */
}

[data-bedrock-dialog]:open {
  opacity: 1;
  scale: 1;
}

/* the state it animates *from* on entry */
@starting-style {
  [data-bedrock-dialog]:open {
    opacity: 0;
    scale: 0.96;
  }
}

overlay is the one people miss. Without it the element leaves the top layer the instant it closes, so the exit transition plays underneath whatever was above it — which then gets blamed on z-index.

The backdrop

[data-bedrock-dialog]::backdrop {
  background: rgb(0 0 0 / 0.4);
}

It is a pseudo-element, so it cannot be a React node, cannot take a className, and cannot have children. If you need a clickable or animated overlay with content in it, that is a real limitation — see gaps.

Backdrop transitions need their own @starting-style block; a pseudo-element does not inherit the host's.

Tailwind

Tailwind 4 handles all of this with variants and arbitrary selectors:

<Dialog.Content
  className="
    opacity-0 scale-96 transition-all duration-180
    transition-discrete
    open:opacity-100 open:scale-100
    starting:open:opacity-0 starting:open:scale-96
    backdrop:bg-black/40 backdrop:transition-opacity
  "
/>

open:, starting:, backdrop: and transition-discrete are all built in. Tailwind 3 needs arbitrary variants: [&:open]:, [&::backdrop]:.

Reduced motion

bedrock.css collapses its durations under prefers-reduced-motion: reduce rather than removing the transitions, so allow-discrete still sequences the display change correctly.