Theme reference
Pages, partials, template variables, helpers, manifest.json options and security settings for custom themes.
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 | |
|---|---|
layout | The 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-enroll | One partial per page. Pages a theme doesn't include come from the default theme |
| Other partials | Any {{> name}} you use, e.g. field-input |
manifest.json | Options 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}} |
theme | The 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 } |
user | When signed in: { id, established, display_name, initial, name, email, picture, profile }, else null |
client | On the consent page: { id, name, icon, tos, privacy_policy } |
page | { name, <name>: true }, e.g. {{#if page.login}} |
errors, notices | Arrays of messages to show |
csrf | { key, token }: put both in every form (the default theme's csrf-fields component does) |
tx | The sign-in transaction id: keep it in forms |
csp_nonce | Put nonce="{{ csp_nonce }}" on every <script> |
assets_url | The base URL of uploaded files |
json:<name> | Any object above as script-safe JSON, e.g. {{{ json:application }}} |
Page data:
| Page | Variables |
|---|---|
login | submit_url, identifier { label, input_type }, login_methods { password, otp }, old { identifier }, social.providers [{ code, name, color, icon, url }] |
registration | submit_url, old, registration { fields [...], agreement } |
otp-options | submit_url, otp.options [{ id, type, hint, description }] |
otp-verification | submit_url, otp { challenge, channel, destination, length, change_option_url } |
password-recovery | submit_url, identifier, old |
password-reset | submit_url |
authorization | submit_url, authorization.scopes [{ name, description }], client |
account | account { profile_url, password_url, requires_current_password, fields [...], sessions [{ device, ip, last_seen, current, terminate_url }] } |
device | submit_url, device { state, user_code } |
mfa | submit_url |
mfa-enroll | submit_url, mfa { state, enroll_token, secret_display, qr, cancel_url } |
error | error { 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, with | As in Handlebars; {{else}} and {{else if}} work |
eq, ne, lt, gt, and, or, not | Comparisons, as subexpressions: {{#if (eq theme.color_scheme "dark")}} |
default | {{default user.name "there"}} |
concat | Joins values into a string |
lookup | {{lookup obj key}} |
fmt | Fills {name} placeholders: {{fmt $.otp.sent_to destination=hint}} |
json | Script-safe JSON: {{json $.layout.show_password}} |
log | Does 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, render | Define 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
{
"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, description | Shown in the console |
type | enum, color (#rrggbb), boolean, string, number, image |
enum | For enum: { "value": "Label" } |
default | Used until the tenant changes it |
overrides | For enum: other option values set when a value is chosen, e.g. a dark scheme setting dark colors |
when | Show only while another option is on ("show_side") or has a value ("color_scheme=custom") |
target | CSS selector of the elements it styles (for the preview editor) |
css_var, unit | The 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.