[]previous and next page

Radix concepts in ShadowJS

The parts render with renderToString and hydrate onto the same nodes.

For agentsUpdated
Radix / shadcn idiom ShadowJS
asChild and Slot The as prop on every part that renders an element: a tag name (as="a") or a component (as={Button}). The part spreads its merged props (attributes, handlers, ref, children) onto the target.
React.forwardRef props.ref is a callback ref. Wrappers forward it by spreading their props; parts combine it with their own through composeRefs.
className class, merged with mergeElementProps: both sources are kept.
Controlled and uncontrolled Pairs open/defaultOpen/onOpenChange, value/defaultValue/onValueChange, checked/defaultChecked/onCheckedChange, pressed/defaultPressed/onPressedChange. Controlled means the prop is not undefined. useControllableState builds the same contract.
data-state, data-disabled, data-orientation, data-side, data-align The same attributes with the same values, for Tailwind variants such as data-[state=open]:animate-in.
Portal with container Dialog.Portal, Popover.Portal, ... take container; the generic Portal from @shadow-js/web takes mount. Portaled content is not rendered on the server.
forceMount and Presence forceMount on Content, Overlay, Portal; usePresence keeps content mounted while an exit animation runs, and unmounts at once when nothing animates.
onEscapeKeyDown, onPointerDownOutside, onFocusOutside, onInteractOutside, onOpenAutoFocus, onCloseAutoFocus The same names on Content. Call event.preventDefault() to veto.
Primitive.div Polymorphic
useComposedRefs, composeEventHandlers composeRefs, composeEventHandlers
useId useBaseId and partId; ids are equal on server and client
Context per part (createContextScope) createPartContext("Dialog"): a missing root names itself in the error
DirectionProvider DirectionProvider, useDirection
VisuallyHidden VisuallyHidden
Collection, RovingFocusGroup createCollection, useRovingFocus

Server rendering#

The parts render with renderToString and hydrate onto the same nodes. Content inside a Portal is not rendered in place on the server. With a portals collector (see the SSR guide, "Portals on the server") a dialog open at render time is server-rendered as a block at the end of body; without one, as in this example, only the trigger and what is outside the portal are rendered.

src/ssr.tsxtsx
import { Dialog } from "@shadow-js/primitives";
import { renderToString } from "@shadow-js/web/server";

const html = renderToString(() => (
  <Dialog>
    <Dialog.Trigger>Open</Dialog.Trigger>
    <Dialog.Portal>
      <Dialog.Content>
        <Dialog.Title>Hello</Dialog.Title>
      </Dialog.Content>
    </Dialog.Portal>
  </Dialog>
));

console.log(`trigger rendered: ${html.includes("aria-haspopup=\"dialog\"")}`);
console.log(`dialog content on the server: ${html.includes("Hello")}`);
console.log(`closed on the server: ${html.includes("aria-expanded=\"false\"")}`);
Expected output
trigger rendered: true
dialog content on the server: false
closed on the server: true