← Back to Blog
Orythemes

How to Build a Custom Login Page for Ory Kratos

A complete guide to building a custom Ory Kratos login page with Ory Elements and React. Covers CSS variable theming, all self-service flows, and common pitfalls.

ory-kratos ory-elements react theming

How to Build a Custom Login Page for Ory Kratos

Ory Kratos is headless — it handles identity flows but ships no login UI. Every team self-hosting Ory must build their own login, registration, and recovery pages. This is by design: Kratos focuses on identity logic, and your UI framework handles the presentation layer.

The two approaches are building from scratch (raw HTML + CSS against the Kratos API) or using Ory Elements, Ory’s official React component library. This guide covers the second path — Ory Elements with CSS variable theming so your auth UI actually looks like your product.

Prerequisites

Before starting, you’ll need:

  • Ory Kratos running (self-hosted or on Ory Network)
  • Next.js 14+ app (or Remix / Vite-based React app)
  • @ory/elements v0.9 installed
  • Node.js 18+

Setting up Ory Elements

Install the package:

npm install @ory/elements

Wrap your app with ThemeProvider in your root layout. In Next.js 14 with the App Router:

// app/layout.tsx
import { ThemeProvider } from '@ory/elements'
import '@ory/elements/style.css'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

The ThemeProvider sets up the .ory-elements root class and makes CSS variables available throughout the component tree.

Understanding CSS Variable Theming

Ory Elements uses a two-layer token system scoped to the .ory-elements class.

Base palette tokens define raw color values:

.ory-elements {
  --ui-50: #f9fafb;
  --ui-100: #f3f4f6;
  /* ... through --ui-900 */
  --accent-500: #6366f1;
}

Semantic tokens reference base tokens and define component-level intent:

.ory-elements {
  --button-primary-bg: var(--accent-500);
  --button-primary-text: var(--ui-50);
  --input-border: var(--ui-300);
  --input-border-focus: var(--accent-500);
}

This hierarchy means you can retheme the entire component suite by overriding a handful of base tokens. Change --accent-500 and every button, link, and focus ring updates.

Building a Minimal Custom Login Page

Here’s a working login page using UserAuthCard from Ory Elements:

// app/login/page.tsx
import { UserAuthCard } from '@ory/elements'
import { getLoginFlow } from './get-login-flow'

export default async function LoginPage({
  searchParams,
}: {
  searchParams: { flow?: string }
}) {
  const flow = await getLoginFlow(searchParams.flow)

  return (
    <main className="flex min-h-screen items-center justify-center">
      <UserAuthCard
        flowType="login"
        flow={flow}
        additionalProps={{
          forgotPasswordURL: '/recovery',
          signupURL: '/registration',
        }}
      />
    </main>
  )
}

One critical gotcha: the identifier node from the default group. When Kratos returns a login flow, it includes a hidden identifier field in the default node group. Ory Elements handles this automatically, but if you’re building custom form components, you must include this field — even if it’s type="hidden" — or Kratos will reject the form submission with a cryptic error.

Theming All Six Flows

The same CSS overrides apply to all Kratos self-service flows. You don’t need per-page CSS:

/* theme.css — one file covers all flows */
.ory-elements {
  /* Base palette override */
  --ui-900: #0a0a0a;
  --ui-50: #fafafa;
  --accent-500: #f0c040;
  --accent-600: #c89000;

  /* Typography */
  --font-family-base: 'IBM Plex Mono', monospace;

  /* Radius */
  --border-radius-base: 4px;
  --border-radius-button: 4px;
}

Import this file once in your root layout (after the Ory Elements base styles) and every flow — login, registration, recovery, verification, settings, error — inherits the overrides.

Common Pitfalls

1. Forgetting to scope overrides to .ory-elements

CSS variable overrides must be inside .ory-elements { ... } or they won’t apply. Declaring at :root doesn’t work because Ory Elements uses the class scope, not :root.

2. Social provider buttons not styled

OAuth/OIDC buttons are rendered inside a [data-node-group="oidc"] container. If your button styles aren’t applying to social buttons, target:

.ory-elements [data-node-group="oidc"] button {
  /* your social button overrides */
}

3. CORS misconfiguration causing flow fetch errors

Kratos requires explicit CORS configuration. Your kratos.yml must list your UI domain:

serve:
  public:
    cors:
      enabled: true
      allowed_origins:
        - https://yourdomain.com

4. Missing CSRF token

Ory sends a csrf_token hidden field in every flow. This is handled automatically by Ory Elements’ form components. If you’re rendering custom forms, you must render the csrf_token node — it’s present in every flow’s ui.nodes array.

Save the CSS Work

If you’d rather skip building and maintaining the CSS, Orythemes offers production-ready themes for Ory Elements. One CSS import covers all six Kratos flows, with light and dark mode, accessible contrast ratios, and compatibility with Ory Elements 0.9.

Join the waitlist to be notified when the first themes launch.

Save the CSS work

Orythemes offers production-ready themes for Ory Elements. Join the waitlist to be notified at launch.

No spam. Unsubscribe anytime. We email to announce new themes and major updates — nothing else.