Skip to content
DomainKit
Esc
navigateopen⌘Jpreview
On this page

Theme, messages, and marks

Restyle the flow through custom properties, replace any sentence in the message catalog, supply provider artwork, and swap the default icon set.

Branding stays in your app. DomainKit.Root takes everything the flow renders with, and the defaults are only defaults.

Theme

/** Every colour is a `--domainkit-*` custom property, so a theme is a plain object. */
export function BrandedSettings() {
  return (
    <DomainKit.Root
      colorScheme="inherit"
      theme={{
        accent: "var(--acme-brand)",
        accentContrast: "#ffffff",
        fontFamily: "var(--acme-font)",
        radius: "0.75rem",
      }}
      transport={transport}
    >
      <Domain.Flow domain="app.example.com" requirements={requirements} />
    </DomainKit.Root>
  );
}

Every value becomes a --domainkit-* custom property on the root element, so the stylesheet reads your values and you can also set them from CSS.

Key Property
accent, accentContrast --domainkit-accent, -contrast
danger, dangerContrast --domainkit-danger, -contrast
success --domainkit-success
background, fill, border The three surfaces
text, muted Foreground and secondary foreground
radius, shadow, backdrop Shape, elevation, dialog backdrop
fontFamily --domainkit-font-family

colorScheme is inherit, light, or dark. inherit follows whatever your page already does.

Messages

/** `Messages.Catalog` holds every user-visible string, including one sentence per failure reason. */
export function LocalisedSettings() {
  return (
    <DomainKit.Root
      messages={{
        approve: "Apply these DNS changes",
        decline: "Not right now",
        fieldLabel: (name) => (name === "accountId" ? "Cloudflare account ID" : name),
        reconnect: (reason) => `Your ${reason.provider} connection expired. Connect it again.`,
      }}
      transport={transport}
    >
      <Domain.Flow domain="app.example.com" requirements={requirements} />
    </DomainKit.Root>
  );
}

Messages.Catalog holds every user-visible string in eight groups: actions, progress, connection, plan review, records, receipts, verification, and one function per DomainKit.Error reason. The catalog is closed, so every key has a default in Messages.english; messages takes a Partial<Catalog> and merges over it, and a host replaces one key without restating the rest.

Nothing renders a tag. A reason becomes a sentence through the catalog, an operation becomes a sentence through operation, and a token field name becomes a label through fieldLabel, which humanises accountId into “Account id” until a host names it properly.

The verification group phrases what a failing check found: expectedValue(value), observedValues(values), and observedNothing. The only observer text that reaches a customer unwrapped is the detail the observer itself wrote.

notConnected is what the connection slot says in read-only, where a connect control would otherwise be.

Provider marks

/** Provider artwork is host-supplied and nothing is fetched at render time. */
export function BrandedProviders() {
  return (
    <DomainKit.Root
      marks={{
        cloudflare: <img alt="" height={20} src="/logos/cloudflare.svg" width={20} />,
        vercel: (provider) => <span aria-hidden="true">{provider.name.charAt(0)}</span>,
      }}
      transport={transport}
    >
      <Domain.Flow domain="app.example.com" requirements={requirements} />
    </DomainKit.Root>
  );
}
export function ProviderIdentity() {
  return <Provider.Mark provider={provider} />;
}

marks is keyed by provider id and takes a node or a function of the provider descriptor. Without an entry the mark is the provider’s own initial, drawn with the theme’s tokens. Nothing is fetched at render time, so a provider list costs no requests.

Icons

icons overrides defaultIcons by slot name. useIcons() returns the set in context, so a part you render yourself uses the same icons as the rest of the flow.

Portals

portalContainer decides where dialogs and popovers render. Pass the element your app portals into; DomainKit checks it belongs to the same document before using it, and falls back to the body when it does not.

Was this page helpful?