Skip to content

Theming

The bundled components (<SignIn />, <SignUp />, <UserProfile />, <UserButton />, …) are customisable along three axes: CSS design tokens, per-element className overrides, and structural layout toggles. The configuration is per-environment; once an operator sets it, every consumer of the SDK in that environment picks it up automatically without a code change.

Theming is available from v0.5 of authn.sh.

Three blocks, every block optional, every key inside it optional. Missing keys mean “use the default”:

interface Appearance {
variables?: {
colorPrimary?: string;
colorBackground?: string;
colorText?: string;
colorTextOnPrimary?: string;
colorInputBackground?: string;
colorInputText?: string;
colorDanger?: string;
colorSuccess?: string;
colorWarning?: string;
colorNeutral?: string;
fontFamily?: string;
fontFamilyButtons?: string;
fontSize?: string;
borderRadius?: string;
spacingUnit?: string;
};
elements?: Record<ElementName, string>;
layout?: {
logoImageUrl?: string | null;
logoLinkUrl?: string | null;
socialButtonsPlacement?: 'top' | 'bottom';
socialButtonsVariant?: 'blockButton' | 'iconButton';
showOptionalFields?: boolean;
privacyPageUrl?: string | null;
termsPageUrl?: string | null;
helpPageUrl?: string | null;
animations?: boolean;
};
}

A loose-string map applied as CSS custom properties on the component root. The server doesn’t validate hex vs rgb() vs named colours — whatever you write round-trips. Lengths can be px, rem, em, %.

{
"variables": {
"colorPrimary": "#0a84ff",
"colorTextOnPrimary": "#ffffff",
"borderRadius": "0.5rem",
"fontFamily": "Inter, system-ui, sans-serif"
}
}

The full token catalogue:

TokenApplied to
colorPrimaryButtons, focus rings, links.
colorBackgroundCard / surface background.
colorTextDefault body text colour.
colorTextOnPrimaryText rendered on top of colorPrimary (e.g. primary button labels).
colorInputBackgroundForm input background.
colorInputTextForm input text colour.
colorDangerDestructive / error accent (delete buttons, validation errors).
colorSuccessSuccess accent (verified states, success banners).
colorWarningWarning accent (pending verification, caution banners).
colorNeutralNeutral accent (secondary buttons, muted text).
fontFamilyDefault font for body copy.
fontFamilyButtonsButton typography. Falls back to fontFamily.
fontSizeBase font size (e.g. "14px", "1rem").
borderRadiusComponent corner radius.
spacingUnitSpacing scale unit; multiplied for padding / gap.

elements — per-element className overrides

Section titled “elements — per-element className overrides”

A map keyed by stable element names. Values are space-separated className strings the SDK appends to the default class list. This is the affordance for layering Tailwind / utility classes on top of authn.sh’s defaults without owning the markup.

{
"elements": {
"card": "shadow-2xl border border-slate-700",
"formButtonPrimary": "tracking-wide uppercase",
"formFieldInput": "ring-1 ring-slate-700"
}
}

The catalogue of stable element keys:

Element keyTargets
formButtonPrimaryPrimary submit buttons.
formButtonResetSecondary / cancel buttons.
formFieldInputAll <input> elements.
formFieldLabel<label> text above fields.
formFieldErrorTextField-level validation error text.
cardThe outer card / dialog surface.
headerTitleCard title (e.g. “Sign in”).
headerSubtitleCard subtitle.
dividerLineThe horizontal rule between social buttons and form.
dividerTextThe “or” label that floats over the divider.
socialButtonsBlockButtonOAuth provider buttons in blockButton variant.
socialButtonsProviderIconProvider icon inside a social button.
footerActionLink”Don’t have an account? Sign up” footer link.
userPreviewMainIdentifierThe user’s email / name in <UserButton /> and <UserProfile />.
userPreviewSecondaryIdentifierThe user’s secondary identifier (typically a handle / org).
userButtonAvatarBox<UserButton />’s avatar circle.
userButtonPopoverActionButtonAction rows inside the <UserButton /> popover.

Unknown keys are ignored — the SDK only honours the stable catalogue.

Structural opts applied to the component shell:

FieldTypeDefaultDescription
logoImageUrlstring | nullnullImage rendered above the card title. null hides the logo.
logoLinkUrlstring | nullenvironment home_urlWhere the logo links to when clicked.
socialButtonsPlacement'top' | 'bottom''top'Render the OAuth provider buttons above or below the identifier form.
socialButtonsVariant'blockButton' | 'iconButton''blockButton'blockButton — labelled “Continue with Google” buttons stacked vertically. iconButton — icon-only buttons laid out horizontally.
showOptionalFieldsbooleantrueWhen false, fields the environment marks as optional (e.g. first_name, last_name) are hidden on the sign-up form.
privacyPageUrlstring | nullnullLinked from the footer / sign-up legal copy.
termsPageUrlstring | nullnullLinked from the footer / sign-up legal copy.
helpPageUrlstring | nullnullLinked from the help affordance in <UserProfile />.
animationsbooleantrueWhen false, the SDK uses prefers-reduced-motion-style static styling.

Server-side appearance — the dashboard editor

Section titled “Server-side appearance — the dashboard editor”

Open Dashboard → Configure → Appearance. The editor renders the same <SignIn />, <SignUp />, <UserProfile />, <UserButton /> your end users see, in a live preview pane. Edits propagate through the existing FAPI surface:

  • Each save call goes to PATCH /v1/instance/appearance — a sparse merge against the current blob.
  • A full overwrite is available via PUT /v1/instance/appearance if you want to replace the entire shape (useful for export / import flows).
  • The current blob is readable via GET /v1/instance/appearance (BAPI, operator auth) or as Environment.appearance on the FAPI GET /v1/environment response (public, used by the SDK at boot).

The FAPI response wraps the blob with an etagsha256(canonical-JSON(appearance)) — so the SDK can short-circuit re-renders when the config hasn’t changed.

The FAPI surface for GET /v1/environment serves with ETag + a short Cache-Control window. Edits saved in the dashboard propagate to end-user browsers within a few seconds (the SDK revalidates on focus + per-page-load), so designers can iterate against a real production-style preview.

Self-hosters running their own CDN in front of the FAPI host should respect the Cache-Control headers the app emits — see Operating an instance in the helm README for details.

Client-side appearance — the appearance prop

Section titled “Client-side appearance — the appearance prop”

When you want to override appearance in just one consumer (e.g. embed authn in a customer-portal page with a custom palette) without touching the per-environment config, pass an appearance prop directly to the component. The prop and the server-side blob are merged at render time:

import { SignIn } from '@authn.sh/sdk-react';
<SignIn
appearance={{
variables: {
colorPrimary: '#7c3aed',
},
elements: {
formButtonPrimary: 'font-mono uppercase tracking-widest',
},
}}
/>

The merge is per-key, deep, with the prop winning:

  • variables — token-by-token override. Keys absent from the prop fall through to the server blob; keys absent from both fall to the SDK default.
  • elements — className strings from prop and server are concatenated (not overwritten). This is intentional — operators can establish a brand baseline via the dashboard, and a specific embed can layer additional utility classes on top without losing the baseline.
  • layout — key-by-key override. Same fall-through as variables.

To explicitly null-out a server-side value from the prop, pass null:

<SignIn appearance={{ layout: { logoImageUrl: null } }} />

This hides the operator-configured logo in just this embed, without touching the dashboard setting.

Just the primary accent — most consumers won’t need anything else:

{
"variables": {
"colorPrimary": "#0a84ff",
"colorTextOnPrimary": "#ffffff"
}
}

Full token sweep for an inverted theme:

{
"variables": {
"colorPrimary": "rgb(10 132 255)",
"colorBackground": "#0b1220",
"colorText": "#e6edf3",
"colorTextOnPrimary": "#ffffff",
"colorInputBackground": "#111827",
"colorInputText": "#e6edf3",
"colorDanger": "#f87171",
"colorSuccess": "#34d399",
"colorWarning": "#fbbf24",
"colorNeutral": "#9ca3af"
}
}

The default primary button gets Tailwind utility classes layered on top:

{
"elements": {
"formButtonPrimary": "tracking-wide uppercase shadow-lg hover:shadow-xl"
}
}

The SDK’s own primary-button class still applies underneath — the override appends rather than replaces.

Sign-ups with the absolute minimum identifier-and-password surface:

{
"layout": {
"showOptionalFields": false
}
}

first_name / last_name (when they’re marked optional on the environment) disappear from the sign-up form. Required fields are unaffected.

Icon-only social buttons at the bottom of the form

Section titled “Icon-only social buttons at the bottom of the form”
{
"layout": {
"socialButtonsPlacement": "bottom",
"socialButtonsVariant": "iconButton"
}
}

When operator-driven edits happen, the FAPI emits an instance.config.appearance_updated webhook event with the previous + current blob and a diff. Useful for audit pipelines or for caches that want to invalidate proactively rather than wait for the next ETag check.