Branding the Login UI

The login SPA is runtime-configurable via a branding.json file served from the web root. No rebuild is required, just mount your config and assets.

How It Works

On startup, the SPA fetches /branding.json. If the file doesn’t exist or is unreachable, defaults are used. (A host server can also inline the config as a <script type="application/json" id="authagonal-boot"> boot payload; when present, the SPA reads it instead of fetching.) The config controls:

Organisation Name

On a multi-tenant host that resolves an organisation for the request (e.g. a custom domain pinned to one customer), the authagonal-boot payload can carry a third member alongside branding and providers:

{
  "branding": { "appName": "Acme Corp", "...": "..." },
  "providers": [],
  "organization": { "id": "org_123", "slug": "widgets-inc", "name": "Widgets Inc" }
}

organization is null (or the member is absent) when the request resolved to no organisation: a single-tenant deployment, or one with no custom-domain pin for this host. This library does not resolve an organisation for an anonymous, pre-authentication request itself (OrganizationSelector needs a signed-in AuthUser); a host that has its own pre-auth resolution (Authagonal Cloud pins one per custom domain) sets organization when it assembles the boot payload.

When organization.name is present and differs from branding.appName (case-insensitive, trimmed, so an organisation named the same as the tenant does not produce “Acme / Signing in to Acme”), the login card renders a subtitle under the heading: “Signing in to {name}” (data-testid="login-org-name", i18n key login.signingInTo). It is rendered once, by the shared AuthLayout header, so every route that mounts through it (sign in, register, forgot/reset password, the MFA challenge and setup pages, the device page, consent and agent-consent, grants, and account) shows it identically. Nothing renders, and the header keeps its normal spacing, when organization is absent, null, or its name matches branding.appName.

Configuration

Place a branding.json file in the wwwroot/ directory (or mount it into the Docker container):

{
  "appName": "Acme Corp",
  "logoUrl": "/branding/logo.svg",
  "primaryColor": "#1a56db",
  "darkPrimaryColor": "#3b82f6",
  "darkMode": "auto",
  "supportEmail": "help@acme.com",
  "showForgotPassword": true,
  "customCssUrl": "/branding/custom.css"
}

Options

Property Type Default Description
appName string "Authagonal" Displayed in the header and browser tab title
logoUrl string \| null null URL to a logo image. When set, replaces the text header.
primaryColor string "#2563eb" Hex color for buttons, links, and focus indicators
supportEmail string \| null null Support contact email (reserved for future use)
showForgotPassword boolean true Show/hide the “Forgot password?” link on the login page
showRegistration boolean false Show/hide the self-service registration link
customCssUrl string \| null null URL to a custom CSS file loaded after the default styles
welcomeTitle LocalizedString null Optional greeting rendered under the header on the auth pages (plain string or { "en": "...", "de": "..." }). Nothing is rendered when unset.
welcomeSubtitle LocalizedString null Optional line under welcomeTitle, same form. Nothing is rendered when unset.
languages array \| null null Language selector options ([{ "code": "en", "label": "English" }, ...]). null shows all shipped languages except novelty locales (see Localization).
poweredBy boolean true Show/hide the “Powered by Authagonal” footer on the auth pages
darkMode "off" \| "auto" \| "force" "auto" Default theme when the visitor hasn’t picked one: "off" (light only), "auto" (follow the OS preference), "force" (always dark). The visitor’s theme toggle still wins.
lightBg string \| null null Page background color in light mode
lightCardBg string \| null null Card/form background color in light mode
darkBg string \| null null Page background color in dark mode
darkCardBg string \| null null Card/form background color in dark mode
darkPrimaryColor string \| null null Overrides primaryColor in dark mode
lightLogoBg string \| null null Logo chip background in light mode (see below)
darkLogoBg string \| null null Logo chip background in dark mode (see below)

Color values must be a hex color (#rgb, #rrggbb, #rrggbbaa) or an rgb()/rgba()/hsl()/hsla() expression; anything else is ignored. The per-mode colors are injected as a <style id="branding-theme-vars"> rule after the bundled styles: the light* values at :root:where(:not(.dark)), so they never apply in dark mode; the dark values at .dark; and primaryColor at :root, since it is the base color for both modes. :where() adds no specificity, so customCssUrl still overrides them all.

Logo Background Chip

If your logo has white or transparent artwork it can disappear against the light card. Set lightLogoBg and/or darkLogoBg to render the logo inside a padded, rounded “chip” with that background color:

{
  "logoUrl": "/branding/logo.svg",
  "lightLogoBg": "#1c1e22",
  "darkLogoBg": "#1c1e22"
}

The chip (a data-auth="logo-chip" wrapper driven by the --auth-logo-bg CSS variable) only gets its padding and background when a logo background is configured, so tenants that don’t set one see the logo flush on the card exactly as before. The two fields are independent: set only lightLogoBg to chip the logo in light mode and leave it bare in dark mode.

Docker Example

Mount your branding files into the container:

docker run -p 8080:8080 \
  -v ./my-branding/branding.json:/app/wwwroot/branding.json \
  -v ./my-branding/logo.svg:/app/wwwroot/branding/logo.svg \
  -v ./my-branding/custom.css:/app/wwwroot/branding/custom.css \
  -e Storage__ConnectionString="..." \
  -e Issuer="https://auth.example.com" \
  authagonal

Or with docker-compose:

services:
  authagonal:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - ./my-branding/branding.json:/app/wwwroot/branding.json
      - ./my-branding/assets:/app/wwwroot/branding
    environment:
      - Storage__ConnectionString=...
      - Issuer=https://auth.example.com

Custom CSS

The customCssUrl option loads an additional stylesheet after the default styles, so your rules take precedence. Useful for changing fonts, adjusting spacing, or restyling specific elements. The URL must be same-origin (relative URLs like /branding/custom.css are fine); cross-origin stylesheets are silently skipped.

CSS Custom Properties

The login UI exposes several CSS custom properties for fine-grained control:

Property Default Description
--brand-primary #2563eb Primary color for buttons, links, focus rings
--auth-bg #f3f4f6 Page background color
--auth-card-bg #ffffff Card/form background color
--auth-logo-bg transparent Logo chip background (chip padding only appears when a logo bg is configured)
--auth-radius 0.5rem Border radius for the auth card
--auth-font (inherit; system font stack) Font family for the auth card
--auth-heading #111827 Heading text color

The color variables here map directly to config fields (primaryColor, lightBg/darkBg, lightCardBg/darkCardBg, lightLogoBg/darkLogoBg), so prefer the config for simple color changes and reserve custom CSS for everything else.

Override them in your custom CSS:

:root {
  --brand-primary: #059669;
  --auth-bg: #0f172a;
  --auth-card-bg: #1e293b;
  --auth-heading: #f8fafc;
}

The login UI uses Tailwind CSS. Custom CSS can target standard HTML elements and Tailwind utility classes. The exported UI components (Button, Input, Card, Alert, etc.) use Tailwind internally.

Dark Mode

The login SPA ships with light, dark, and system themes. The theme toggle is always visible in the layout. User selection is persisted to localStorage under the auth-theme key.

How It Works

CSS Variables

Light values are declared at :root; dark-mode overrides are scoped to .dark, so tenant branding in customCssUrl always takes precedence when supplied.

Variable Light Dark
--auth-bg #f3f4f6 (or lightBg) #030712 (or darkBg)
--auth-card-bg #ffffff (or lightCardBg) #111827 (or darkCardBg)
--auth-heading #111827 #f9fafb
--auth-logo-bg transparent (or lightLogoBg) transparent (or darkLogoBg)
--brand-primary #2563eb (or primaryColor) the light value (or darkPrimaryColor)

Disabling or Overriding

Tenant branding always wins. To force a single theme, set your own values in customCssUrl:

/* Force dark palette regardless of user choice */
:root {
  --auth-bg: #0f172a;
  --auth-card-bg: #1e293b;
  --auth-heading: #f8fafc;
}
.dark {
  --auth-bg: #0f172a;
  --auth-card-bg: #1e293b;
  --auth-heading: #f8fafc;
}

To remove the theme toggle entirely, use the npm package path, import AuthLayout and render without the toggle, or fork the SPA.

Data Attributes

All login form elements have data-auth attributes for CSS targeting and test automation:

Attribute Element
data-auth="page" Main page wrapper
data-auth="header" Header section
data-auth="logo-chip" Wrapper around the logo image (padded only when a logo background is set)
data-auth="logo" Logo image
data-auth="app-name" App name heading
data-auth="welcome-title" / data-auth="welcome-subtitle" The optional welcomeTitle / welcomeSubtitle lines (only present when set)
data-auth="content" Main content area
data-auth="languages" Language selector
data-auth="language-trigger" Language selector trigger button
data-auth="theme-toggle" Light/system/dark theme toggle
data-auth="powered-by" “Powered by Authagonal” footer
data-auth="login-form", "email-field", "password-field", "submit-button" The sign-in form and its parts (login page only)

Target these in your custom CSS:

[data-auth="header"] {
  background: linear-gradient(135deg, #667eea, #764ba2);
}

Example: Custom Background and Font

/* custom.css */
body {
  font-family: 'Inter', sans-serif;
  background-color: #0f172a;
}

Customization Tiers

Level What You Do Update Path
Config only Mount branding.json + logo Seamless, update the Docker image, keep your mounts
Config + CSS Add customCssUrl with style overrides Same, CSS classes are stable
npm package npm install @authagonal/login, customize branding.json, build into wwwroot/ Updatable, npm update pulls new versions
Fork the SPA Clone login-app/, modify source, build your own You own the UI, server updates are independent
Write your own Build a completely custom frontend against the auth API Full control, see Auth API for the contract

See demos/custom-server/ for a working example with custom branding (green theme, “Acme Corp”).