Skip to content

Universal Login Page Templates

The Universal Login page template controls the chrome around the login widget — the logo, the dark-mode toggle, the language picker, the "powered by" badge, the terms/privacy links, and any custom content you place above or below the card.

It is rendered with Liquid (the same engine as Email Templates), so a template can use variables, conditionals, and a small set of AuthHero slot tags that expand to ready-made, fully-styled components.

The mental model in one sentence

You don't author a whole HTML page — you compose a body out of slots and your own markup, and AuthHero wraps it in a fixed, accessible page shell.

What you control, and what you don't

AuthHero splits the page into two layers:

text
┌─ Page shell — owned by AuthHero (fixed) ───────────────────┐
│  <!doctype html> · <head> · favicon · fonts · page CSS     │
│  background tint · dark-mode runtime · responsive layout   │
│                                                            │
│   ┌─ Body — owned by your template (Liquid) ───────────┐   │
│   │  {%- authhero:logo -%}        {%- authhero:settings -%}│
│   │                                                    │   │
│   │              ┌────────────────────┐                │   │
│   │              │  {%- auth0:widget -%}│  ← required   │   │
│   │              └────────────────────┘                │   │
│   │                                                    │   │
│   │  {%- authhero:powered-by -%}   {%- authhero:legal -%} │
│   └────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────┘
  • The shell is fixed. You never write <head>, the dark-mode runtime, the background CSS, or the responsive rules. That keeps every tenant's login accessible, secure, and consistent across upgrades — and keeps you out of CSS-and-JS authoring you'd otherwise have to maintain.
  • The body is yours. You decide which slots render, where they go, and what custom markup sits around them.

Why this design

The goal is a gentle slope from "tweak the defaults" to "fully custom", with no cliff in between:

  • Adjust the default design. Start from the default template and rearrange or restyle the built-in components.
  • Add ready-made components. Drop in a dark-mode toggle or a language picker as a single tag — they come pre-built, themed, and wired up (the toggle persists a cookie; the picker re-renders the page in the chosen locale). You don't reimplement them.
  • Remove what you don't want. Delete a slot tag and that component is gone. No flag, no config — absence is the configuration.
  • Go full custom. Because the body is plain Liquid + HTML, you can ignore every slot except the required widget and build whatever you like around it.

How this differs from Auth0

Auth0's page template is a single Liquid layout that you own end-to-end: you write the <html>/<head>/<body>, your own CSS, and drop in two magic tags — {%- auth0:head -%} and {%- auth0:widget -%}. Powerful, but you're now responsible for the whole document, and a dark-mode toggle or language switcher is something you build and maintain yourself.

AuthHero keeps the document shell and gives you composable, pre-built slot components instead:

Auth0AuthHero
What you authorThe whole page (<html>, <head>, CSS)Just the body
Magic tagsauth0:head, auth0:widgetauth0:widget (required) + authhero:* component slots; auth0:head in full-document mode
Dark mode / language pickerBuild it yourself{%- authhero:dark-mode-toggle -%} / {%- authhero:language-picker -%}
Page CSS / runtimeYours to maintainManaged by the shell
Removing a componentEdit your own markupDelete the slot tag
Bad templateCan break the pageValidated on save; falls back to a working page at render

The {%- auth0:widget -%} tag is intentionally identical to Auth0's, so the muscle memory carries over. The authhero:* namespace is the additive part: batteries-included components you opt into.

Auth0-style full-document templates

For migration, AuthHero also accepts a full HTML document — the Auth0 page-template shape — as an escape hatch. If your stored template contains an <html> element, AuthHero renders it as the entire page instead of wrapping it in the shell, and the {%- auth0:head -%} tag expands to the functional head essentials (page CSS, fonts, favicon, the widget script, and the dark-mode runtime):

html
<!DOCTYPE html>
<html>
  <head>
    {%- auth0:head -%}
    <link rel="stylesheet" href="https://example.com/my-brand.css" />
  </head>
  <body>
    {%- auth0:widget -%}
  </body>
</html>

This means an existing Auth0 template can be pasted in and work. auth0:head ships the essentials the widget needs to render and theme — the page CSS (including a centered body layout on the page background, so a vanilla Auth0 template looks styled out of the box rather than rendering top-left on a blank page), fonts, favicon, the widget script, and the dark-mode runtime. The trade-off still mirrors Auth0: in full-document mode you own the document, so the curated corner-chip chrome isn't forced on you — but the authhero:* chip slots remain available if you want to drop them in.

Which mode am I in?

A template containing <html> is rendered as a full document. Anything else is a body fragment wrapped in the fixed shell (the recommended path). Most tenants should stay with fragments — reach for a full document only when migrating an Auth0 template or when you truly need to own the whole page.

The render pipeline

Slot tags are real Liquid tags, so they can live inside {% if %} blocks and loops, and they sit alongside ordinary variable output (the branding.logo_url style references shown below).

Slots

Slot tagPositionRenders when
{%- auth0:widget -%}centerrequired — the login widget mount
{%- auth0:head -%}<head>full-document templates only — injects the head essentials (see below)
{%- authhero:logo -%}top-leftlogo placement is set to chip (otherwise the logo sits inside the widget)
{%- authhero:settings -%}top-rightalways — wraps the dark-mode toggle and language picker
{%- authhero:dark-mode-toggle -%}always — just the toggle
{%- authhero:language-picker -%}two or more languages are available
{%- authhero:powered-by -%}bottom-lefta "powered by" logo is configured
{%- authhero:legal -%}bottom-righta terms & conditions URL is configured

A slot whose condition isn't met renders nothing — so you can list every slot in your template and they'll appear only when relevant. An unknown slot (a typo like authhero:lego) also renders empty rather than erroring.

The default template

This is what's served when a tenant hasn't uploaded a custom template — copy it as your starting point:

liquid
<div class="ah-widget-stack">
  <div class="ah-above-widget" data-ah-slot="above-widget"></div>
  {%- auth0:widget -%}
  <div class="ah-below-widget" data-ah-slot="below-widget"></div>
</div>
{%- authhero:logo -%}
{%- authhero:settings -%}
{%- authhero:powered-by -%}
{%- authhero:legal -%}

Examples

Remove a component

Don't want the language picker, but want to keep the dark-mode toggle? Replace the bundled settings slot (which contains both) with just the toggle, and drop the legal slot:

liquid
{%- auth0:widget -%}
{%- authhero:dark-mode-toggle -%}
{%- authhero:powered-by -%}

Want the absolute minimum — just the widget, nothing else? That's a valid template:

liquid
{%- auth0:widget -%}

Content above and below the widget

The widget sits in an optional .ah-widget-stack that centers a column and shares the widget's width. Drop your own markup into the .ah-above-widget / .ah-below-widget regions — empty regions collapse, so they only take space when used:

liquid
<div class="ah-widget-stack">
  <div class="ah-above-widget">
    <h1>Welcome to {{ client.name }}</h1>
  </div>

  {%- auth0:widget -%}

  <div class="ah-below-widget">
    Need help? <a href="https://support.example.com">Contact support</a>
  </div>
</div>
{%- authhero:settings -%}

By default the corner chips render as translucent pills when there's a background image, and as plain text on a solid background. Override per slot with a style argument:

liquid
{%- auth0:widget -%}
{%- authhero:legal style="plain" -%}      {# always plain text #}
{%- authhero:powered-by style="pill" -%}   {# always a pill #}

style accepts auto (default), plain, or pill, and works on logo, settings, powered-by, and legal.

Branch on page context

Slot tags are real Liquid, so you can branch — for example, only show a heading when there's no background image competing with it:

liquid
<div class="ah-widget-stack">
  {% unless page.has_background_image %}
    <div class="ah-above-widget"><h1>Sign in</h1></div>
  {% endunless %}
  {%- auth0:widget -%}
</div>
{%- authhero:settings -%}
{%- authhero:legal style="plain" -%}

Full custom

Because the body is just Liquid + HTML, you can ignore the helper classes entirely and lay things out yourself. Only {%- auth0:widget -%} is required:

liquid
<header class="my-banner">
  <img src="{{ branding.logo_url }}" alt="{{ client.name }}" />
</header>

<main class="my-layout">
  {%- auth0:widget -%}
</main>

{%- authhero:dark-mode-toggle -%}

Variables

These are available in the Liquid scope:

liquid
{{ branding.logo_url }}          {{ branding.colors.primary }}
{{ theme.page_background.background_image_url }}
{{ client.name }}                <!-- the application name -->
{{ prompt.screen.name }}         <!-- current screen, e.g. "login-id" -->
{{ locale }}                     <!-- active language code -->

{{ page.has_background_image }}  <!-- true / false -->
{{ page.dark_mode }}             <!-- "auto" | "light" | "dark" -->
{{ page.logo_position }}         <!-- "widget" | "chip" | "none" -->
{{ page.layout }}                <!-- "center" | "left" | "right" -->

branding and theme are the tenant's full Branding and Theme objects, so anything stored there is reachable.

Validation & safety

Saving a template (PUT) is validated:

  • It must mount the widget — any spelling of the tag works ({%- auth0:widget -%}, {% auth0:widget %}).
  • It must be syntactically valid Liquid. An unclosed {% if %} is rejected with 400.

At render time, if a stored template somehow fails (e.g. a variable edge case), AuthHero falls back to the default template, and finally to the bare widget — the login page can never be taken down by a template.

Management API

http
GET    /api/v2/branding/templates/universal-login
PUT    /api/v2/branding/templates/universal-login
DELETE /api/v2/branding/templates/universal-login
POST   /api/v2/branding/templates/universal-login/preview
  • GET returns the stored template, or the AuthHero default when none is set.
  • PUT stores a template ({ "body": "…" }), subject to the validation above.
  • DELETE reverts to the default.
  • POST …/preview renders a full-page preview HTML for a sample screen. It accepts optional body, branding, and theme overrides so an editor can preview unsaved edits:
json
{
  "screen": "login",
  "body": "{%- auth0:widget -%}{%- authhero:legal style=\"plain\" -%}"
}

Admin UI

In the admin app, Branding → Universal Login has the template editor, the slot/variable reference, and an Open full preview button that opens the rendered page (with your unsaved edits) in a new tab. The branding preview pane also has a full-page preview that reflects your live colour/logo edits.

See Also

Dual-licensed: AGPL-3.0-only or commercial license.