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.