Customize the login design
Choose a starter login design, add your branding, and publish it as a revision.
The login component ships with a built-in template that follows your project without any files in your repo. Its look comes from your app: theme it with --zl-* custom properties and put any page layout — a split screen, a brand pane, marketing copy — around it in your own code. When you need to change the widget's structure, you take ownership of its template: the CLI copies a starter design into .zitadel/branding/, and from then on the login renders your files.
Starter designs
| Design | Layout | Uses logo |
|---|---|---|
centered | The default card, ejected as-is | yes |
minimal | The same form without card chrome | no |
Both render every step of your login flow — sign-in, registration, passkey ceremonies, and their error states — because they template the flow engine's step responses rather than individual screens.
Eject a design
zitadel setup never applies a design: it embeds the maintained login component and publishes no branding revision. Take ownership of the template at any time:
npx @zitadel/cli@alpha branding ejectbranding eject offers the two starter designs above, then scaffolds .zitadel/branding/ locally — nothing is uploaded until you apply:
branding.json— the descriptor: layout, brand asset URLs, and the template reference.login.liquid— the LiquidJS template the login renders for every step.README.md— the template rules and workflow, kept next to the files.
Add your branding
Brand assets live in branding.json and must be hosted https:// URLs — the CLI validates them at plan time, and the platform rejects anything else:
{
"$schema": "../meta/branding.json",
"layout": "centered",
"logo_url": "https://cdn.example.com/logo.svg",
"liquid_template": { "$file": "./login.liquid" }
}Only some designs render each asset (see the table above); a URL configured for a design that does not use it is simply ignored. Custom fonts are not configurable yet — load fonts from the page embedding the login instead.
Publish and iterate
Branding follows the same loop as schemas and flows:
npx @zitadel/cli@alpha plan
npx @zitadel/cli@alpha applyplan validates the descriptor and the template (LiquidJS parse, banned patterns, required tags) and shows the pending change; apply publishes it. Every publish creates an immutable revision — there is no in-place update, and rolling back means re-applying an earlier template. The live login picks up the newest revision on its next step response.
The scaffolded README.md in .zitadel/branding/ documents the template rules (compose zl-* components, no scripts or inline styles, copy through translation keys).
Ownership trade-off
An ejected template is yours: it stops receiving improvements to the built-in designs. Eject when you want to change the login's structure and look; to change only wording, the locales prop covers copy without touching templates (see Customize copy). Staying on the built-in template costs nothing — branding eject is always one command away.