Skip to content
DomainKit
Esc
navigateopen⌘Jpreview
On this page

Composition

Compose DomainKit React parts with Base UI render props, built-in provider marks, host overrides, and CSS tokens that match your product.

Complete flows are conveniences over semantic parts. Every public part accepts Base UI’s render prop plus state-aware className and style values. Connection.Flow and Connection.ConnectTrigger are the styled recipes. Connection.Trigger only opens the dialog; its required children supply the label and render uses a host button.

<Connection.OAuthAction
  controller={controller}
  label="Connect"
  render={<MyButton variant="primary" />}
/>
<Connection.Root status={state._tag}>
  <HostCard>
    <Provider.Mark provider={snapshot.provider} />
    <p>Manages DNS for this domain.</p>
    <BaseDialog.Root>
      <Connection.Trigger render={<HostButton />}>
        Connect {snapshot.provider.name}
      </Connection.Trigger>
      <Connection.Dialog controller={controller} snapshot={snapshot} />
    </BaseDialog.Root>
  </HostCard>
</Connection.Root>

Theming

<DomainKit.Root
  colorScheme="inherit"
  icons={{ copy: <Copy />, copied: <Check />, download: <Download /> }}
  marks={{ internaldns: <InternalDnsMark /> }}
  messages={{ connectProvider: (name) => `Connect ${name}` }}
  theme={{ accent: "var(--brand-primary)", radius: "var(--radius-md)" }}
  transport={transport}
>
  {children}
</DomainKit.Root>

The opt-in stylesheet reads --domainkit-* variables. Theme props set those variables without replacing the host font, palette, or icon system.

Cloudflare and Vercel use bundled, theme-aware marks sourced from SVGL. Other recognized providers load a remote mark and fall back to the provider’s first letter when unavailable. Use marks when your product needs to replace a built-in mark or supply branding for a private provider.

Host notifications

DomainKit emits structured lifecycle events after user-initiated mutations. Keep notification UI in the host application and map the events to Sonner, another toast system, telemetry, or nothing at all:

<DomainKit.Root
  onEvent={(event) => {
    switch (event._tag) {
      case "ConnectionEstablished":
        toast.success(`${event.connection.provider.name} connected`);
        break;
      case "RecordsApplied":
        toast.success("DNS records added");
        break;
      case "RecordsCleaned":
        toast.success("DNS records removed");
        break;
      case "DomainDisconnected":
        toast.success("Domain disconnected");
        break;
    }
  }}
  transport={transport}
>
  {children}
</DomainKit.Root>

Partial apply and cleanup results have their own events, so hosts can use warning notifications instead of reporting partial work as success. Initial inspection and passive verification do not emit notification events.

The default mutation dialogs close immediately after a complete success. Partial results and failures remain in context because they require user attention; transient success feedback belongs to the host notification surface.

Records from parts

<Records.Root>
  <Records.Header>
    <Records.Row>
      <Records.Head scope="col">Type</Records.Head>
      <Records.Head scope="col">Name</Records.Head>
      <Records.Head scope="col">Value</Records.Head>
    </Records.Row>
  </Records.Header>
  <Records.Body>
    {records.map((record) => (
      <Records.Row key={record.id}>
        <Records.Cell>{record.type}</Records.Cell>
        <Records.Cell>
          <Records.CopyValue value={record.name} />
        </Records.Cell>
        <Records.Cell>
          <Records.CopyValue value={record.value} />
        </Records.Cell>
      </Records.Row>
    ))}
  </Records.Body>
</Records.Root>

Presentational record parts accept browser-safe records directly and do not require DomainKit.Root or a transport.

Effect Atom lifecycle models

Every stateful lifecycle exposes the atoms used by its packaged controller and flow:

const model = Provisioning.useModel(connection, records);
const state = useAtomValue(model.state);
const command = useAtomSet(model.command);

if (state._tag === "Review") {
  return (
    <Operations.Root lifecycle="provisioning">
      {state.plan.operations.map((operation) => (
        <Operations.Item key={operation.id} operation={operation} />
      ))}
      <button onClick={() => command(Provisioning.Command.Apply())}>Apply approved plan</button>
    </Operations.Root>
  );
}

The same model seam is available as Connection.useModel, Verification.useModel, and Cleanup.useModel. Operations.List is the complete neutral operation recipe; its exported parts support Base UI render elements, forwarded refs, state-aware classes, and host event handlers.

Was this page helpful?