Custom renderers

Replace any component's renderer with the components prop, and how an Astryx adapter is planned to plug in the same way.

@polyxd/react ships one adapter: Radix primitives (the same ones shadcn/ui uses) styled only by design-system token variables. You can replace the renderer for any of the 24 components with your own, one component at a time.

The components prop#

Pass a map from component name to a React component. Each renderer receives the document node as node.

import { PolyxdSurface, Render, useBindings, type Node } from "@polyxd/react";

function MyStatus({ node }: { node: Node }) {
  const b = useBindings();
  const urgent = node.kind === "error";
  return (
    <div role={urgent ? "alert" : "status"} className={`my-banner my-banner-${node.kind}`}>
      <strong>{b.text(node.title)}</strong>
      {node.message !== undefined && <p>{b.text(node.message)}</p>}
      {node.action && <Render id={node.action} />}
    </div>
  );
}

<PolyxdSurface document={doc} theme="material3" components={{ Status: MyStatus }} />;

Everything you don't override keeps the default renderer. The default map is exported as registry if you want to wrap an existing renderer rather than replace it.

Helpers for renderers#

Export What it gives you
useBindings() Binding helpers for the current scope: value(v) resolves a literal or binding; text(v, format?) resolves and formats it as text; write(binding, value) writes an input's value back to host data; pointer(binding) gives the absolute JSON Pointer; context(c) resolves an action context
useSurface() The surface: the document, byId, current data, dispatch(action, scope, sourceId) for sending actions, locale, resolveMedia, and the portal element dialogs should render into
Render <Render id="child" /> renders another component by id, in the current scope. Use it for children and referenced components
formatValue(value, format, locale) The same Intl-based formatting the default renderer uses
getPointer, setPointer JSON Pointer read and immutable write

Use dispatch for every action rather than calling your own handlers, so that ui.dismiss, ui.back and ui.next behave correctly and every capability event reaches the host's onAction.

What a custom renderer must keep#

A custom renderer takes over the rendering rules and accessibility guarantees for its component. Keep what the component definition requires: the role, the accessible name, live-region behaviour, target sizes, and the rendering rules (for example, Status errors are assertive, and a Confirm starts focus on the least destructive option).

Use token variables (var(--pxd-color-status-danger-emphasis) and so on) rather than raw values, so your renderer still follows whichever pack is loaded. Then run the verifier over the examples: its rendered and agent checks apply to any renderer, and they are how the default adapter's own bugs were found.

Astryx (planned)#

Astryx is Meta's open-source React design system. Decision 0002 (docs/decisions/0002-astryx.md) looked at how it relates to Polyxd:

The components prop is the seam this adapter will use: an adapter is a full map of renderers passed the same way. The adapter itself is not built. Any Astryx-based work would pin a specific Astryx version (it is pre-1.0 and changes often) and be described as "compatible with Astryx", not as an official Astryx pack.

Polyxd is an early preview. Found something unclear? It will get better with your feedback.