---
title: Composition
description: Compose DomainKit React parts with Base UI render props, built-in provider marks, host overrides, and CSS tokens that match your product.
seo:
  title: Theme DomainKit React parts in 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.

```tsx
<Connection.OAuthAction
  controller={controller}
  label="Connect"
  render={<MyButton variant="primary" />}
/>
```

```tsx
<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

```tsx
<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:

```tsx
<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

```tsx
<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:

```tsx
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.

## Related docs

- [Choose an integration level](/docs/react/integration-levels)
- [React API reference](/docs/reference/react)
- [Implement the authenticated transport](/docs/react/transport)
