Reference

Theme reference

Pages, partials, template variables, helpers, manifest.json options and security settings for custom themes.

View .md llms.txt

A theme is a set of Handlebars templates plus manifest.json. Templates run in a sandboxed engine: logic-less, whitelisted helpers only, no code execution. For an introduction see Themes.

#Files

File
layoutThe page shell. Defines named blocks with {{#component name}}…{{/component}} and places them with {{#render name}}{{/render}}
login, registration, otp-options, otp-verification, password-recovery, password-reset, authorization, account, logout, error, lost, redirect, device, mfa, mfa-enrollOne partial per page. Pages a theme doesn't include come from the default theme
Other partialsAny {{> name}} you use, e.g. field-input
manifest.jsonOptions and security settings

Limits: 60 templates, 256 KB each, 2 MB in total; names use a-z, 0-9, -, _. Uploaded files (CSS, scripts, fonts, images): 2 MB each, 100 per tenant, referenced as {{ assets_url }}<file name>.

#Template data

Available on every page:

Variable
$Translations of the page's language: {{ $.login.title }}. Placeholders: {{fmt $.login.continue_with provider=name}}
themeThe theme's option values: {{ theme.button_color }} (images are ready-to-use URLs, or "")
lang{ code, name, direction, rtl }
languages[{ code, name, url, active }] when the tenant has several
application{ name, logo, alternative_logo, icon, env, sandbox, login_url, logout_url, logout_everywhere_url, password_recovery_url, register_url, policy_url, tos_url, support_url }
userWhen signed in: { id, established, display_name, initial, name, email, picture, profile }, else null
clientOn the consent page: { id, name, icon, tos, privacy_policy }
page{ name, <name>: true }, e.g. {{#if page.login}}
errors, noticesArrays of messages to show
csrf{ key, token }: put both in every form (the default theme's csrf-fields component does)
txThe sign-in transaction id: keep it in forms
csp_noncePut nonce="{{ csp_nonce }}" on every <script>
assets_urlThe base URL of uploaded files
json:<name>Any object above as script-safe JSON, e.g. {{{ json:application }}}

Page data:

PageVariables
loginsubmit_url, identifier { label, input_type }, login_methods { password, otp }, old { identifier }, social.providers [{ code, name, color, icon, url }]
registrationsubmit_url, old, registration { fields [...], agreement }
otp-optionssubmit_url, otp.options [{ id, type, hint, description }]
otp-verificationsubmit_url, otp { challenge, channel, destination, length, change_option_url }
password-recoverysubmit_url, identifier, old
password-resetsubmit_url
authorizationsubmit_url, authorization.scopes [{ name, description }], client
accountaccount { profile_url, password_url, requires_current_password, fields [...], sessions [{ device, ip, last_seen, current, terminate_url }] }
devicesubmit_url, device { state, user_code }
mfasubmit_url
mfa-enrollsubmit_url, mfa { state, enroll_token, secret_display, qr, cancel_url }
errorerror { status, title, message, code, retry_url }

Field objects (registration.fields, account.fields) have name, label, input_type, autocomplete, value, required, readonly, maxlength and, for dropdowns, options. Keep the form field names and hidden fields of the default theme: the server reads them.

#Helpers

Helper
if, unless, each, withAs in Handlebars; {{else}} and {{else if}} work
eq, ne, lt, gt, and, or, notComparisons, as subexpressions: {{#if (eq theme.color_scheme "dark")}}
default{{default user.name "there"}}
concatJoins values into a string
lookup{{lookup obj key}}
fmtFills {name} placeholders: {{fmt $.otp.sent_to destination=hint}}
jsonScript-safe JSON: {{json $.layout.show_password}}
logDoes nothing (kept for compatibility)
option-target{{option-target "logo" name="Logo"}} inside a tag: marks the element for the console's preview editor; writes nothing on real pages
component, renderDefine and place named blocks
Partials{{> name}}, {{> name context}}, partial blocks and {{#*inline "name"}}

{{x}} is HTML-escaped; {{{x}}} is raw (only for values you trust, such as json). Data lookups see only plain data, never prototypes or functions. Nesting is limited to 64 levels and output to 4 MB.

#manifest.json

json
{
  "version": 1,
  "name": "Acme",
  "options": {
    "Look": {
      "collapsed": false,
      "properties": {
        "button_color": { "title": "Accent color", "type": "color", "default": "#1a1a1a" },
        "corner_radius": { "title": "Corners", "type": "enum", "enum": { "0": "Square", "10": "Rounded" }, "default": "10" },
        "show_side": { "title": "Show a side panel", "type": "boolean", "default": false },
        "side_tagline": { "title": "Tagline", "type": "string", "default": "", "when": "show_side" },
        "logo_dark": { "title": "Logo on dark pages", "type": "image", "default": "" }
      }
    }
  },
  "security": { "styleSrc": ["https://fonts.googleapis.com"], "fontSrc": ["https://fonts.gstatic.com"] }
}

Options, grouped as they appear in the console:

Field
title, descriptionShown in the console
typeenum, color (#rrggbb), boolean, string, number, image
enumFor enum: { "value": "Label" }
defaultUsed until the tenant changes it
overridesFor enum: other option values set when a value is chosen, e.g. a dark scheme setting dark colors
whenShow only while another option is on ("show_side") or has a value ("color_scheme=custom")
targetCSS selector of the elements it styles (for the preview editor)
css_var, unitThe CSS custom property it sets, for instant preview updates
preview{ "page": "account", "device": "mobile" }: where the option shows

Option keys use a-z, 0-9 and _. "true"/"false" enum values become booleans in templates.

Security (security): the outside origins the theme may load, added to the page's Content Security Policy. Lists of https:// origins: scriptSrc, styleSrc, imgSrc (also data:), fontSrc (also data:), connectSrc, frameSrc. allowInlineScripts: true allows inline scripts without the nonce, for themes ported as they are from SSOfy; prefer nonces.

JSON Schema: /schemas/theme-manifest.json.

#Validation and fallbacks

Uploads are validated: syntax, unknown helpers, missing partials, the manifest, and option-target names. If a custom theme fails while rendering a page anyway, 9in renders that page with the default theme, so a broken theme can never lock users out, and the console shows an alert.