# Login theming

White-label every realm's sign-in experience so it looks like it belongs to the tenant, not the platform.

## What it is

Each realm carries its own **login branding**: a logo, a colour scheme and related presentation fields. When a user lands on a realm's sign-in pages they see the tenant's identity — the right logo, the right colours — with no generic platform chrome. One HelixIAM deployment can therefore serve many tenants, each with a fully branded login.

Branding is scoped to the realm and applies across its authentication surfaces: the login form, multi-factor challenges, password and account flows, and — where [self-registration](realm-settings.md#registration) is enabled — the `/register` sign-up page.

## In the console

**Manage → Login theme** edits the realm's structured theme, or one [organization](../manage/organizations.md)'s layer:

- **Theme for** picks the layer: the realm (every sign-in) or one organization (only while it is in context). The layer stack next to it shows the built-in default, the file theme (if any), the realm and the organization.
- Tabs: **Colours** (14 roles, light and dark; an empty dark value is derived), **Type & shape**, **Logo & images** (an uploaded image or an `https` URL), **Layout & texts** (texts per language; "All languages" is the `default` key), **Links**, **Library** (upload, list, use, copy the URL of and delete fonts and images; a file still in use cannot be deleted, and the console shows the server's "still used by" message) and, for the realm only, **Advanced** (file theme selection and custom CSS).
- An empty field means "inherit"; its placeholder shows the value it currently inherits. **Save theme** sends the whole layer; a field you emptied is cleared.
- The console checks what it can before sending — colour format, sizes, fonts, image references, texts, locales, links, custom CSS constructs, and for uploads the type (confirmed by the file's first bytes), size, name, weight, style and the per-realm counts — and shows the server's `400` field errors on the same fields.
- **Contrast (WCAG AA).** The contrast pairs the edited layer touches are computed while you type and shown under the colours concerned; dark values the server derives are checked by the server, through the live preview. A contrast failure blocks saving.
- **Live preview** renders the sign-in page with the unsaved layer (`POST /admin/realms/{realm}/theme/preview`) at desktop or mobile (390 px) width, scaled to fit, in the console's language; **Open full size** shows it in a new tab. An organization is previewed as its layer over the stored realm layer. The theme's fonts load in the preview because the font files answer CORS for exactly the console origins.

**Realm settings → Branding** no longer edits the five deprecated fields (`logoUrl`, `primaryColor`, `backgroundColor`, `welcomeText`, `customCss`). They are part of the realm theme, so the tab shows them read-only with a link to Login theme. A realm settings save sends them back as they are stored at that moment, so saving settings never overwrites a theme change.

**Organizations → an organization → Sign-in branding** edits the organization's display name, logo URL and primary colour (`GET`/`PUT /admin/realms/{realm}/organizations/{orgId}/branding`); only the fields you change are sent. **Customize the full theme** opens that organization's layer on the Login theme page.

**Import / export** moves a branded realm: the JSON document, or the archive with the theme's fonts and images (see [Import / export](../integration/import-export.md#archives-with-theme-files)).

!!! tip "Match the locale too"
    Pair branding with the realm's [internationalization](realm-settings.md) settings so a tenant's users get both the right look and the right language on the login page.

## How to white-label a tenant

- Use a logo that reads well on the realm's background colour.
- Keep colour contrast accessible so the login form stays legible. The structured theme refuses colour pairs below the WCAG AA minimums; custom CSS is not checked (see [Accessibility](../compliance/accessibility.md)).
- Confirm the branding on both desktop and mobile widths — the sign-in pages are responsive.
- Apply consistent branding to the [notification](notifications.md) templates (email, magic-link) so the full sign-in journey feels like one brand.

## Over the API

The simple branding fields below are part of the realm-settings object at `/admin/realms/{realm}/settings`; they are deprecated in favour of the structured theme (the console no longer edits them there). The structured, validated theme (colours for light and dark, texts, layout) has its own endpoint, `/admin/realms/{realm}/theme` — see [Structured theme and contrast checks](#structured-theme-and-contrast-checks). The examples below assume you have set `$HELIX_URL` / `$REALM` and logged in — see [Authenticating to the API](../getting-started/api-authentication.md).

The relevant fields are `logoUrl`, `primaryColor`, `backgroundColor`, `welcomeText` and `customCss`. On a freshly seeded realm they are unset:

```bash
curl -s -b cookies.txt "$HELIX_URL/admin/realms/$REALM/settings"
```
```json
{
  "realmId": "acme",
  "displayName": "Acme",
  "logoUrl": null,
  "primaryColor": null,
  "backgroundColor": null,
  "welcomeText": null,
  "customCss": null
}
```

Set them with a settings `PUT` (refresh CSRF after the read):

```bash
# Refresh CSRF after any GET, then PUT (see the auth guide for the helper)
curl -s -b cookies.txt -c cookies.txt "$HELIX_URL/admin/realms/$REALM/settings" >/dev/null
CSRF=$(awk '($6=="__Host-XSRF-TOKEN"||$6=="XSRF-TOKEN")&&$7~/^r1\./{t=$7} END{print t}' cookies.txt)

curl -s -b cookies.txt -X PUT \
  -H "Content-Type: application/json" \
  -H "X-XSRF-TOKEN: $CSRF" \
  -d '{
        "displayName": "Acme",
        "logoUrl": "https://cdn.example.com/acme-logo.svg",
        "primaryColor": "#0B5FFF",
        "backgroundColor": "#0B1020",
        "welcomeText": "Sign in to Acme",
        "customCss": ".login-card{border-radius:12px}"
      }' \
  "$HELIX_URL/admin/realms/$REALM/settings"
```

The response echoes the updated `RealmSettingsDto` with the new branding fields.

## Structured theme and contrast checks

The theme is built from layers merged field by field: the built-in default, an optional file theme, the realm theme
and, while a sign-in runs for an [organization](../manage/organizations.md), that organization's theme. A field a
layer leaves out is inherited from the layer below.

`PUT /admin/realms/{realm}/theme` replaces the realm's layer. Input is parsed strictly: an unknown field is a `400`,
never silently dropped. The server refuses a theme whose colours fall below the WCAG contrast minimums for the roles
it sets: text, muted text and text on the primary colour against their backgrounds, links on the surfaces, error and
success text on their tints (4.5:1), and the focus ring on the surface (3:1). A refusal is a
`400 {message, fieldErrors}` naming the pair, for example `contrast.inkOnSurface.dark`. The console's *Branding* tab
shows the same warning while you type a colour (Manage → Login theme). Custom CSS, logos and texts are not checked.

`GET …/theme?effective=true` returns the merged result. The full reference of the theme model, the CSS variables, file themes and the e-mail
templates is available to licensed customers on request.

### Full endpoint set

| Method & path (under `/admin/realms/{realm}`) | Purpose |
| --- | --- |
| `GET /settings` · `PUT /settings` | Read / set branding (`logoUrl`, `primaryColor`, `backgroundColor`, `welcomeText`, `customCss`) as part of realm settings |
| `GET /theme` · `PUT /theme` | Read (`?effective=true`: merged) / replace the realm's structured theme layer, validated |
| `GET /organizations/{orgId}/theme` · `PUT /organizations/{orgId}/theme` | Read / replace an organization's theme layer (no custom CSS) |
| `GET /organizations/{orgId}/branding` · `PUT /organizations/{orgId}/branding` | An organization's display name, logo URL and primary colour |
| `POST /theme/preview` | Render the sign-in page with an unsaved layer (answers in the request's `Accept-Language`) |
| `GET /theme/assets` · `POST /theme/assets` · `DELETE /theme/assets/{assetId}` | The realm's uploaded fonts and images (multipart upload; `409` while still used) |
| `GET /theme/base` · `PUT /theme/base` | The realm's file theme selection |

See the [API reference](../integration/api-reference.md).

## See also

- [Realm settings](realm-settings.md)
- [Notifications](notifications.md)
- [Multi-factor authentication](../authentication/mfa.md)
