@owizzo/kratos-ui
v1.3.0
Published
White-label authentication UI for Ory Kratos: Ory Elements flows rendered with Ozora components.
Readme
@owizzo/kratos-ui
White-label authentication UI for Ory Kratos
as a reusable React library. It renders all Kratos self-service flows of
Ory Elements (@ory/elements-react) with
components of the Ozora
design system.
The library ships no branding: product name and logo come from the configuration, colors from the app's Ozora theme. The same package serves owizzo products as well as white-label tenants.
| Flow | Component | Default route |
| ------------ | -------------------- | -------------------- |
| Login | KratosLogin | /auth/sign-in |
| Registration | KratosRegistration | /auth/sign-up |
| Recovery | KratosRecovery | /auth/recovery |
| Verification | KratosVerification | /auth/verification |
| Settings | KratosSettings | /settings |
| Errors | KratosError | /auth/error |
| Signed in | AccountCard | / |
The UI adapts to the identity schema and the methods enabled in Kratos (password, OIDC, passkeys, TOTP, lookup secrets, …). On top of Ory Elements it adds a password confirmation on registration and a current-password check when changing the password in the settings flow.
Installation
npm install @owizzo/kratos-ui @owizzo/ozora @ory/[email protected] @ory/[email protected] [email protected]Peer dependencies: React 19, Tailwind CSS 4, @owizzo/ozora (^2.0.0),
@ory/elements-react, @ory/client-fetch and react-intl.
react-intland@ory/client-fetchare pinned to the exact versions@ory/elements-reactpins, and@ory/elements-reactitself to the version those pins belong to. A second copy would give the library components a different React context than Ory'sIntlProvider(runtime error "Could not find required intl object").src/config/peerVersions.test.tsfails when an Ory upgrade changes the pins — then alignpeerDependenciesanddevDependencies.
react-intl7.1.13 declares TypeScript^5.6as optional peer. Apps on TypeScript 6 get a peer warning from npm;legacy-peer-deps=truein the app's.npmrcsilences it until Ory moves to a newerreact-intl.
Styles
Like Ozora, the library ships Tailwind classes, not compiled CSS. Add it to
the app's Tailwind entry stylesheet. The stylesheet registers its own sources
(@source) and puts the Ory Elements styles into Tailwind's components
layer — no @layer declaration or @source for kratos-ui needed:
@import "tailwindcss";
@import "@owizzo/ozora/index.css";
@import "@owizzo/kratos-ui/styles.css";
/* Your brand — see "Branding / white label" */
:root {
--ozora-primary: #7c3aed;
--ozora-secondary: #f59e0b;
--ozora-link: #7c3aed;
--ozora-link-hover: #5b21b6;
}styles.css contains the Ory Elements base styles (scoped to
.ory-elements), a theme for them derived from the Ozora tokens, the card
styling, a type scale aligned with Ozora (14 px body, labels and buttons;
inputs 16 px on phones so iOS does not zoom) and the .kratos-ui-scene page
background used by <AuthScene>.
Upgrading from 1.0: the
@layer theme, base, ory-elements, …line and the@sourcefor kratos-ui can be removed; keeping them does no harm.
Usage
useKratosFlow loads the flow for a UI route: it resumes the flow from
?flow=<id> (Kratos redirects there), replaces an expired one, otherwise
creates a new browser flow, and follows Kratos redirects (e.g. "already
signed in"). Then render the matching component:
import { AuthScene, createOryConfig, KratosLogin, useKratosFlow } from "@owizzo/kratos-ui"
import { ProductLogo } from "./ProductLogo"
const sdkUrl = "https://auth.example.com"
const config = createOryConfig({ name: "Northwind", sdkUrl })
export function SignInPage() {
const { flow, error, reload } = useKratosFlow("login", { sdkUrl })
if (error) return <ErrorView message={error.message} onRetry={reload} />
return (
<AuthScene>
{flow && <KratosLogin flow={flow} config={config} logo={<ProductLogo />} />}
</AuthScene>
)
}The flow type follows the kind (useKratosFlow("settings") returns a
SettingsFlow). Pass search when the router owns the query string;
server-rendered apps can keep fetching the flow on the server (see
getFlowError in @owizzo/kratos-ui/server).
Inside an app page, render the settings embedded — without Ory's page header (logo, account menu) and with forms flush to the page content:
<KratosSettings flow={flow} config={config} embedded />Building your own flow layout? Spread ozoraComponents into the
components prop of any Ory Elements flow, or pass extra overrides via the
components prop of the Kratos* components (merged on top of the Ozora
components).
The current-password check in the settings flow calls
/self-service/login/browseron the same origin, i.e. it expects Kratos to be reachable through the app's own domain (reverse proxy), which is also what keeps the session cookie first-party.
Branding / white label
Everything brand-specific is supplied by the app:
| What | How |
| ------------ | ------------------------------------------------------------------------- |
| Product name | createOryConfig({ name }) (required): card heading if there is no logo |
| Logo | logo prop of the Kratos* components and AccountCard (any React element, e.g. an SVG component), or createOryConfig({ logoUrl }) |
| Colors | Ozora tokens on :root: --ozora-primary, --ozora-secondary, --ozora-background, --ozora-text, --ozora-link, --ozora-link-hover (or Ozora's ThemeProvider) |
| Card / page | optional --kratos-ui-* variables (below) |
| Texts | locale + customTranslations (Ory message IDs and kratosUi.*) |
| Single parts | components prop: replace any Ory Elements slot |
| Flow content | customization prop: methods, SSO, fields, steps (see below) |
The Ory Elements color scale (--brand-*), the card accent strip and the
page background are derived from the Ozora tokens, so a tenant theme is
just a set of Ozora tokens. Surfaces, text, borders and validation colors use
Ozora's semantic tokens (--ozora-surface, --ozora-border,
--ozora-text-muted, --ozora-danger, …), so Ozora's dark mode
(<html data-ozora-theme="dark">) applies to the whole auth UI, including
the parts Ory Elements renders itself. Optional fine-tuning:
| Variable | Default |
| ------------------------------ | -------------------------------------------------------- |
| --kratos-ui-card-radius | 1.25rem |
| --kratos-ui-card-shadow | soft shadow tinted with the primary color |
| --kratos-ui-card-accent | primary → secondary gradient (none removes the strip) |
| --kratos-ui-border | --ozora-border, else slate-200 |
| --kratos-ui-scene-background | --ozora-background |
| --kratos-ui-scene-orb-1/-2 | primary / secondary, translucent (transparent hides them) |
Ozora components
Inputs, password fields, checkboxes, buttons, SSO buttons, labels, field
messages, flow messages (Alert), links, text nodes, the TOTP secret
(CopyField), the card itself, the logo/name heading, custom card headers,
the registration stepper and dividers are Ozora components
(ozoraComponents). A few parts are still rendered by Ory Elements because
their logic is internal to it: the default card header texts, the card
footer links (sign up / sign in / choose another method), the method list of
the "choose a method" screen, the OTP code input and the settings section
frames. They are themed through the same Ozora tokens.
Fields, methods, SSO and registration steps
What a user has to provide is decided in two layers:
| Layer | Decides | Where |
| ----- | ------- | ----- |
| Kratos (server) | which fields exist and are required (identity schema traits), which methods are enabled (password, code, passkey, TOTP, …), which SSO providers exist, one- or two-step registration | Kratos config + identity schema(s) |
| This library (per app, at runtime) | which of these are offered, in which order, with which labels/icons, and how the registration form is split into steps | customization prop |
The UI renders whatever the Kratos flow contains, so enabling a method or
adding a trait in Kratos needs no UI change. customization can only narrow
down and present — a field Kratos requires cannot be removed client-side.
Different requirements per app: identity schemas
For apps that need different registration data (e.g. a product app vs. a tenant
portal), define one identity schema per app in Kratos, mark it
selfservice_selectable: true and start the flow with it:
identity:
default_schema_id: default
schemas:
- id: default
url: file:///etc/config/kratos/identity.default.schema.json
- id: partner-portal
url: file:///etc/config/kratos/identity.partner-portal.schema.json
selfservice_selectable: trueimport { selfServiceUrl } from "@owizzo/kratos-ui"
// e.g. the app's "sign up" button
location.href = selfServiceUrl(sdkUrl, "registration", {
identitySchema: "partner-portal",
returnTo: "https://portal.example.com/",
})Required fields, formats and the field order come from that schema.
customization
Every Kratos* flow component takes a customization object. It is plain
data (apart from optional icon elements), so it can live in code or be loaded
per tenant from a backend:
const login: FlowCustomization = {
// Only offer these methods (Kratos may have more enabled).
methods: ["password", "oidc"],
sso: {
providers: ["microsoft", "google"], // allow-list + order (Kratos provider IDs)
position: "top", // or "bottom" (default)
display: { microsoft: { label: "Mit Microsoft 365 anmelden" } },
},
fields: {
identifier: { label: "Geschäftliche E-Mail", placeholder: "[email protected]" },
},
// Replaces the card title/subtitle Ory Elements derives from the flow.
title: "Anmelden",
description: "Willkommen zurück.",
}
<KratosLogin flow={flow} config={config} customization={login} />| Key | Effect |
| ------------- | ------ |
| methods | Allow-list of method groups: password, code, link, passkey, webauthn, totp, lookup_secret, oidc, saml. Without any identifier-based method (e.g. SSO only), the login identifier field is hidden too. |
| sso.providers | Which providers to show, in this order. |
| sso.display | Label and icon (element or image URL) per provider. Built-in logos: apple, auth0, discord, github, gitlab, google, linkedin, microsoft, slack, spotify, x, yandex. |
| sso.position / sso.hidden | SSO above/below the credentials, or not at all. |
| fields | Per node name (identifier, password, traits.email, traits.name.first, …): label, placeholder, description (help text), hidden (optional fields only). |
| title / description | Card heading and subtitle. |
| steps | Registration only, see below. |
Registration steps
steps splits the fields of a Kratos registration screen into client-side
steps with an Ozora Stepper and next/back buttons:
const registration: FlowCustomization = {
steps: [
{ id: "account", title: "Konto", fields: ["traits.email"] },
{
id: "profile",
title: "Profil",
description: "Wie dürfen wir Sie ansprechen?",
fields: ["traits.name.first", "traits.name.last", "traits.company"],
},
{ id: "security", title: "Sicherheit", fields: ["password"] },
],
}- Steps adapt to what Kratos shows: steps without fields on the current screen are skipped, and fields no step lists go to the last one. With Kratos' two-step registration (profile screen → credentials screen) the steps apply to whichever screen has at least two of them; with fewer, the form renders without steps.
- "Next" validates the step's fields (required per identity schema, formats such as e-mail); Enter advances as well. All steps are one Kratos submit.
- Kratos validation errors after submit jump back to the first step with an affected field. SSO is only offered on the first step.
Configuration
createOryConfig(options) returns an OryClientConfiguration (no Ory
branding, standard routes):
| Option | Default | Description |
| -------------------- | ------------------- | ------------------------------------------------------------ |
| name | – (required) | Product name |
| logoUrl | – | Logo image URL (alternative to the logo prop) |
| locale | "de" | UI language (any Ory Elements locale) |
| routes | defaultAuthRoutes | Route overrides; must match the Kratos ui_urls |
| sdkUrl | – | Kratos public URL (or the app origin when proxied) |
| customTranslations | – | Extra/overriding messages per locale (kratosUi.*, Ory IDs) |
| project | – | Any other Ory project setting (enabled flows, …) |
Texts rendered by the library itself use kratosUi.* message IDs (see
src/config/messages.ts, German and English included). AccountCard and
LogoutButton live outside the Ory context and take their texts via the
labels / label props.
Kratos configuration
Point the Kratos self-service UI URLs at the app, e.g.:
selfservice:
default_browser_return_url: https://account.example.com/
flows:
login:
ui_url: https://account.example.com/auth/sign-in
registration:
ui_url: https://account.example.com/auth/sign-up
recovery:
ui_url: https://account.example.com/auth/recovery
verification:
ui_url: https://account.example.com/auth/verification
settings:
ui_url: https://account.example.com/settings
error:
ui_url: https://account.example.com/auth/errorSSO via Microsoft (Entra ID)
SSO buttons appear automatically once Kratos has an OIDC provider configured
(selfservice.methods.oidc). The Microsoft provider is configured in the
Kratos config (see ocip.core.service/docker/kratos/config/kratos.yml for
local dev, mapper: oidc.microsoft.jsonnet):
- Create an Azure App Registration (Entra ID → App registrations) with
redirect URI (type "Web")
<kratos public base_url>/self-service/methods/oidc/callback/microsoftand a client secret. - Set
client_id/client_secretin the Kratos config (or via env:SELFSERVICE_METHODS_OIDC_CONFIG_PROVIDERS_0_CLIENT_ID/..._CLIENT_SECRET). microsoft_tenant:common,organizationsor a specific tenant ID.
Package entry points
| Import | Contents |
| ------------------------------- | ---------------------------------------------------------- |
| @owizzo/kratos-ui | all components, flows, config and helpers |
| @owizzo/kratos-ui/config | createOryConfig, defaultAuthRoutes, selfServiceUrl, messages |
| @owizzo/kratos-ui/server | getFlowError (no React, usable server-side) |
| @owizzo/kratos-ui/styles.css | stylesheet (see above) |
Development
src/
├─ components/ AccountCard, AuthScene, BackToSignIn, LogoutButton (+ stories)
├─ nodes/ Ozora overrides for Ory Elements nodes (ozoraComponents)
├─ card/ Ozora card root, header, logo, content and divider slots
├─ customization/ FlowCustomization types and context
├─ steps/ registration steps (provider, stepper, navigation)
├─ sso/ SSO root/button overrides, provider logos
├─ flows/ Kratos* flow components (+ stories, tests)
├─ config/ createOryConfig, routes, messages
├─ server/ getFlowError
├─ styles/ styles.css (shipped as-is)
└─ test/ flow fixtures and a demo brand for stories and testsnpm run storybook # component development with fixture flows (no Kratos needed)*
npm test # vitest
npm run typecheck
npm run build # Vite library build → dist/ (ESM + CJS + .d.ts, one file per module)* The Storybook configuration (.storybook/) is local and not versioned,
like in Ozora: @storybook/react-vite with a separate Vite config
(@vitejs/plugin-react, @tailwindcss/vite), stories src/**/*.stories.tsx
and a preview stylesheet importing Tailwind, the Ozora theme and
src/styles/index.css.
The build keeps one output file per source module (preserveModules), so
the "use client" directives of the components are preserved for React
Server Components setups.
