Custom Branded Keycloak Login Page

Build a custom Keycloak login page with React, Tailwind CSS, and Magic UI.

Introduction

30 Second Summary

Every app has a login page, and most of them look exactly the same. If you use Keycloak for authentication, you know the default login screen feels generic and disconnected from your brand.

In this project, you will build a fully custom-branded Keycloak login page using React, Tailwind CSS, and animated Magic UI components. You will package it into a deployable theme and test it in a real Keycloak instance running in Docker.

What You'll Build

A custom Keycloak login page featuring a branded color scheme, an animated shimmer effect on the sign-in button, and a subtle dot pattern background.

By the end of this project, you'll have:

  • A Keycloakify React project with Storybook previews of every login scenario.
  • An ejected login page component fully redesigned with Tailwind CSS and Magic UI animated components.
  • A built .jar theme file deployed and tested in a local Keycloak Docker container.
  • Secret Mission: Eject and customize the registration page to match your branded login page.

Are there any prerequisites?

You should have basic familiarity with React (components, props, and JSX). You also need Git, Node.js, and Docker installed and running on your machine.

Before We Start

Before We Start...

Set Up the Keycloakify Starter Project

Keycloak handles authentication for thousands of applications, but its default login page looks completely generic. Keycloakify lets you replace that default page with a custom-branded login experience built with React and modern frontend tools.

In this step, you'll clone the official Keycloakify starter project, install its dependencies, and explore the key files that make it all work.

In this step, get ready to:
  • Clone the Keycloakify starter repository and install dependencies.
  • Initialize a fresh Git repository with the starter code.
  • Explore the project structure and key configuration files.
Clone the starter repository
  • Open your terminal. Press Ctrl+Alt+T (Ubuntu/WSL2) to launch it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Clone the Keycloakify starter repository by running:
git clone https://github.com/keycloakify/keycloakify-starter

What does this command do?

git clone downloads the entire Keycloakify starter project from GitHub onto your machine. This starter is the official recommended way to begin a Keycloakify project. It comes pre-configured with Vite as the build tool and Storybook for previewing your theme.

  • Confirm the repository was cloned by running:
ls

You should see a keycloakify-starter folder in the output.

Clone command failed?

Make sure you have an active internet connection. The git clone command needs to download files from GitHub.

If you see fatal: destination path 'keycloakify-starter' already exists, a previous clone attempt left a folder behind. Remove it by running rm -rf keycloakify-starter and then try the clone command again.

Help me troubleshoot my git clone error

  • Move into the project folder and install its dependencies by running these commands:
cd keycloakify-starter
npm install

What does npm install do?

npm install reads the package.json file and downloads every library the project needs into a node_modules folder. This includes React, Keycloakify, Vite, Storybook, and TypeScript.

The install may take a minute or two. You should see a summary showing the number of packages added with no critical errors.

Seeing errors during install?

Check your Node.js version by running node --version. The starter requires Node ^18.0.0 or >=20.0.0.

If you see permission errors, do not use sudo npm install. Instead, check your Node.js installation was set up with the correct permissions.

Help me fix my npm install errors

Initialize a fresh Git repository

The cloned repository comes with the Keycloakify team's Git history. You'll start a fresh repository so your commits track only your own changes.

  • Remove the existing Git history and initialize a new repository by running these commands:
rm -rf .git
git init
git add .
git commit -m "Initial commit: keycloakify starter"

What do these commands do?

  • rm -rf .git deletes the old Git history that came from the clone.
  • git init creates a brand-new, empty Git repository.
  • git add . stages every file in the project for the first commit.
  • git commit -m saves a snapshot of the starter code with a descriptive message.
  • Verify your commit was created by running:
git log --oneline

You should see a single line with the message Initial commit: keycloakify starter.

Commit failed?

If you see a message about configuring your identity, Git needs your name and email. Run the following two commands, replacing the placeholder values with your own name and email:

git config --global user.name "Your Name"
git config --global user.email "your@email.com"

Then run git commit -m "Initial commit: keycloakify starter" again.

Help me fix my git commit error

Explore the project structure in VSCodium

Before writing any code, take a few minutes to understand how the starter project is organized. Three files are especially important to know about.

  • Open the project in VSCodium by running:
codium .

codium command not found?

If your terminal does not recognize codium, open VSCodium manually instead. Press Cmd+Space (macOS) or the Windows key (Windows), type VSCodium, and press Enter. Then click File, select Open Folder, and navigate to the keycloakify-starter folder on your Desktop.

  • In the Explorer sidebar on the left, click on vite.config.ts to open it.
  • Open package.json from the same sidebar.
  • Expand the src folder, then the login folder, and open KcPage.tsx.

What are these key files?

  • vite.config.ts is the Vite build configuration. Vite is the tool that bundles your React code into a format Keycloak can use. You'll see the keycloakify() plugin registered here alongside the React plugin.
  • package.json lists the project's scripts and dependencies. The key scripts are build-keycloak-theme (packages your theme into a JAR file for Keycloak) and storybook (launches a preview tool for developing your theme visually).
  • src/login/KcPage.tsx is the main entry point for the login page theme. All your login page customizations will flow through this file. Right now it renders the DefaultPage component from Keycloakify, which gives you the standard Keycloak login look.

What is Vite's role in this project?

Vite serves two roles in a Keycloakify project. During development, it provides a fast development server with Hot Module Replacement (HMR) so your Storybook previews update instantly when you change code.

During production, it bundles your React code so the Keycloakify compiler can process it into a Keycloak-compatible theme. The better you understand Vite's dual role, the more sense the build process will make later.

Your project is cloned, dependencies are installed, and you know your way around the key files. Next up, you'll launch Storybook and see the default Keycloak login page in action.

Launch Storybook and Preview the Default Login Theme

Your Keycloakify starter project is set up with all dependencies installed. Now it's time to see what the default Keycloak login page actually looks like.

Before customizing anything, you need a way to preview your theme without spinning up a full Keycloak server. Storybook gives you exactly that.

In this step, get ready to:
  • Generate a Storybook story for the Keycloak login page.
  • Launch Storybook and explore different login scenarios.
  • Commit your changes to Git.
Generate a login page story

Storybook stories are configuration files that tell Storybook which components to render and with what data. Keycloakify provides a CLI command that generates these story files for each Keycloak page type.

  • Generate a login page story by running this command in your terminal:
npx keycloakify add-story
  • The CLI displays a list of available Keycloak pages. Select login.ftl from the list.
  • Press Enter to confirm your selection.

What does this command do?

npx keycloakify add-story creates a Storybook story file for the Keycloak page you selected. The generated file contains multiple "stories" that represent different login scenarios.

These scenarios include the default login view, login with an error message, and login with social provider buttons. Each one lets you preview the page in a different state without touching a real Keycloak server.

  • Confirm the story file was created by checking the Explorer sidebar in VSCodium.
  • Expand src, then login, then pages. You should see a new file called Login.stories.tsx.

Don't see the story file?

Make sure you selected login.ftl when the CLI prompted you. Run npx keycloakify add-story again if you need to retry.

Check that your terminal is in the keycloakify-starter directory. Run pwd to confirm.

I ran npx keycloakify add-story but don't see Login.stories.tsx in my project

Launch Storybook and explore login scenarios

Now that you have a story file, you can launch Storybook to preview your login page in the browser. Storybook renders your React components in isolation so you can develop and test your theme visually.

  • Start Storybook by running this command:
npm run storybook
  • Storybook compiles your project and opens a browser tab automatically. This can take a minute on the first run.
  • Once it finishes, your browser opens to http://localhost:6006.

What is Storybook?

Storybook is a tool that renders your React components outside of the full application. You don't need a running Keycloak server to preview your theme.

Each "story" simulates a different state of the login page. This lets you test how your theme handles errors, social login buttons, and other scenarios without setting up real user accounts.

You should see the Storybook interface with a sidebar on the left listing your login stories.

  • In the sidebar, expand the Pages section, then expand Login.
  • Click the Default story to see the standard login form.
  • Click through the other stories to see the login page with an error message and with social provider buttons.

Each scenario shows the same login page in a different state. This is your preview environment for the rest of the project.

Storybook not opening?

If your browser didn't open automatically, navigate to http://localhost:6006 manually.

If you see a compilation error, check that you ran npm install in Step 1. Make sure your terminal is inside the keycloakify-starter folder.

Storybook isn't loading or is showing compilation errors

Commit your changes to Git

Your login page story is generated and previewing in Storybook. Time to save this progress with a Git commit.

  • Your Storybook server is still running in the current terminal. Open a new terminal in VSCodium by pressing Ctrl+Shift+`.
  • Stage and commit your changes by running these commands:
git add .
git commit -m "feat: add login page storybook story"
  • You should see a commit confirmation message listing the new story file that was added.

Commit failed?

If you see nothing to commit, the story file may not have been generated. Run npx keycloakify add-story again and select login.ftl.

If Git says Author identity unknown, you need to configure your Git identity first. Set your name and email with the commands git config user.name "Your Name" and git config user.email "you@example.com".

My git commit isn't working in the keycloakify-starter project

You've got your preview environment running. Next, you'll use it to apply your first CSS customizations to the login page and see the changes in real time.

Apply Your First CSS Customization

Your Storybook is showing you the default Keycloak login page. It works, but it looks like every other Keycloak installation out there.

In this step, you will create a custom CSS file and wire it into your theme. This is the fastest way to change your login page's look because you can target Keycloakify's built-in CSS classes without touching any React component code.

In this step, get ready to:
  • Create a custom CSS stylesheet targeting Keycloakify's class system.
  • Import the stylesheet into your login theme entry point.
  • Preview and iterate on CSS changes with live hot-reloading in Storybook.
Create your custom CSS file and import it

Keycloakify uses two sets of CSS classes on every element in the login page. Classes prefixed with kc (like kcBodyClass and kcButtonClass) have no default styles applied to them. They exist purely as hooks for your custom CSS.

The other set, prefixed with pf-, comes from PatternFly, a CSS framework by Red Hat that gives Keycloak its default look.

  • In the VSCodium Explorer sidebar, right-click on the src/login folder.
  • Select New File and name it main.css.
  • Add your first custom CSS rules by pasting this code into main.css:
/* Custom Keycloakify theme styles */

/* Change the page header area */
.kcHeaderWrapperClass {
    color: #4f46e5;
    font-size: 1.75rem;
    font-weight: 700;
}

/* Set a custom page background color */
body.kcBodyClass {
    background-color: #f0f4ff;
}

/* Style the form card area */
.kcFormCardClass {
    border: 1px solid #c7d2fe;
    border-radius: 12px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}

What does this CSS do?

Each rule targets a different kc-prefixed class that Keycloakify places on login page elements.

  • .kcHeaderWrapperClass targets the page header where the realm title appears. You are giving it an indigo color and bolder font.
  • body.kcBodyClass targets the page body. You are swapping the default background for a light blue.
  • .kcFormCardClass targets the card that wraps the login form. You are adding a subtle border, rounded corners, and a soft shadow.

The CSS file exists now, but Keycloakify does not know about it yet. You need to import it into the login theme's entry point.

  • Open src/login/KcPage.tsx in VSCodium.
  • Add this import line at the very top of the file, above all existing imports:
import "./main.css";

Why import CSS in a TypeScript file?

Vite (the build tool running under the hood) understands CSS imports in JavaScript and TypeScript files. When it sees import "./main.css", it bundles the stylesheet into the final theme output so Keycloak can load it.

  • Save both src/login/main.css and src/login/KcPage.tsx.
  • Switch to your browser where Storybook is running at http://localhost:6006. The page hot-reloads automatically. You should see the header text turn indigo, the background shift to a light blue, and the form card gain a rounded border with a shadow.

Don't see any changes in Storybook?

  • Make sure you saved both files. Unsaved changes will not trigger a reload.
  • Confirm the import line in KcPage.tsx reads exactly import "./main.css"; with the ./ prefix. A missing dot means Vite cannot find the file.
  • Check that main.css is inside src/login/, not in the project root or another folder.

Still stuck? Why aren't my CSS changes showing in Storybook?

Experiment with button styling using browser dev tools

You now know the pattern for CSS customization. Target a kc-prefixed class, write your rule, and Storybook shows the result instantly. To discover more class names, you can inspect elements directly in the browser.

  • In your Storybook browser tab, right-click the Sign In button on the login preview.
  • Select Inspect to open browser DevTools.
  • Look at the classes on the button element. You will see kcButtonClass, kcButtonPrimaryClass, kcButtonBlockClass, and kcButtonLargeClass alongside the PatternFly pf- classes.

What is Hot Module Replacement?

Hot Module Replacement (HMR) is a Vite feature that pushes code changes to your browser without a full page refresh. Every time you save a file, Storybook updates in place so you can iterate on your design in real time.

Now add button styles to your stylesheet to see HMR in action.

  • Switch back to src/login/main.css in VSCodium.
  • Add these button rules below the existing styles:
/* Style the primary action button */
.kcButtonPrimaryClass {
    background-color: #4f46e5;
    border-color: #4f46e5;
    color: #ffffff;
    transition: background-color 0.2s ease;
}

.kcButtonPrimaryClass:hover {
    background-color: #4338ca;
    border-color: #4338ca;
}

What does this CSS do?

.kcButtonPrimaryClass targets the Sign In button specifically. You are overriding PatternFly's default blue with an indigo that matches the header. The :hover rule darkens the button when the learner moves their cursor over it.

  • Save main.css.
  • Check Storybook in your browser. The Sign In button should now be indigo, and hovering over it darkens it slightly.

Button still looks the same?

  • PatternFly's default styles have high specificity. If your rule is not winning, try adding !important after the values temporarily to confirm the selector is correct, then check specificity.
  • Make sure you are targeting .kcButtonPrimaryClass (capital P, capital C). CSS class names are case-sensitive.

Need help? My button CSS rules are not overriding the default styles in Storybook.

Find more class names in DevTools

Right-click and inspect any element in the Storybook preview to discover its kc-prefixed class name. Common ones include kcLabelClass for input labels, kcInputClass for text fields, and kcFormGroupClass for form sections.

✔️ Awesome, I've got everything!

Great. Double check you have saved src/login/main.css and src/login/KcPage.tsx.

ⓧ I'd like to double check the full code

Here is the complete main.css file at this point:

/* Custom Keycloakify theme styles */

/* Change the page header area */
.kcHeaderWrapperClass {
    color: #4f46e5;
    font-size: 1.75rem;
    font-weight: 700;
}

/* Set a custom page background color */
body.kcBodyClass {
    background-color: #f0f4ff;
}

/* Style the form card area */
.kcFormCardClass {
    border: 1px solid #c7d2fe;
    border-radius: 12px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}

/* Style the primary action button */
.kcButtonPrimaryClass {
    background-color: #4f46e5;
    border-color: #4f46e5;
    color: #ffffff;
    transition: background-color 0.2s ease;
}

.kcButtonPrimaryClass:hover {
    background-color: #4338ca;
    border-color: #4338ca;
}

And your KcPage.tsx should have this line added at the very top, above all other imports:

import "./main.css";
Commit your CSS customization
  • Open your terminal in VSCodium by pressing Ctrl+`.
  • Stage and commit your changes by running these commands:
git add .
git commit -m "feat: add custom CSS styling to login page"
  • You should see output confirencing the commit with the message feat: add custom CSS styling to login page.

Commit failed?

  • If you see nothing to commit, make sure you saved all files before running git add.
  • If Git asks for your name and email, set them by running git config user.name "Your Name" and git config user.email "you@example.com".

Need help? My git commit is not working after adding the CSS file.

You have a custom-styled login page and a clean Git commit to mark the milestone. Next up, you will add Tailwind CSS and Magic UI components to take the design even further.

Add Tailwind CSS and Magic UI

Your login page already has custom CSS overrides targeting Keycloakify's kc- classes. That approach works for quick reskinning, but it limits you to the existing HTML structure.

To fully redesign the login page in the next step, you need a modern styling toolkit. In this step, you will install Tailwind CSS for utility-first styling, then add two animated Magic UI components that will power your branded login page.

In this step, get ready to:
  • Install Tailwind CSS and configure the path alias that Magic UI depends on.
  • Initialize shadcn/ui and install the ShimmerButton and DotPattern components from Magic UI.
  • Commit your changes with Git.
Install Tailwind CSS and configure path aliases

Tailwind CSS is a utility-first CSS framework. Instead of writing custom class names, you apply small, single-purpose classes directly in your markup.

Both shadcn/ui and Magic UI depend on Tailwind, so you need to install it first.

  • Make sure your terminal is in the keycloakify-starter folder. If Storybook is still running from the previous step, press Ctrl+C to stop it.
  • Install Tailwind CSS and its companion tools as dev dependencies by running:
npm install -D tailwindcss postcss autoprefixer

What are postcss and autoprefixer?

PostCSS is a tool that transforms your CSS. Tailwind uses it as its processing engine.

Autoprefixer automatically adds browser-specific prefixes (like -webkit-) so your styles work across different browsers.

  • Generate the Tailwind and PostCSS configuration files by running:
npx tailwindcss init -p

What does this command do?

The -p flag tells Tailwind to create both tailwind.config.js and postcss.config.js in your project root.

  • Confirm the files were created by running:
ls tailwind.config.js postcss.config.js

You should see both file names printed in the output.

Don't see the files?

Make sure you ran npx tailwindcss init -p from inside the keycloakify-starter folder, not a parent or child directory.

help me troubleshoot missing tailwind config files

Tailwind needs to know which files contain class names so it can generate only the CSS you actually use. You configure this in the content array.

  • Open tailwind.config.js in VSCodium.
  • Find the content array, which starts as an empty array [].
  • Replace the entire file contents with this configuration:
/** @type {import('tailwindcss').Config} */
export default {
  // Tell Tailwind to scan all component files for class names
  content: ["./src/**/*.{ts,tsx,js,jsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
}

What does the content array do?

The glob pattern ./src/**/*.{ts,tsx,js,jsx} tells Tailwind to scan every TypeScript and JavaScript file in your src/ directory. Tailwind reads the class names from these files and generates only the CSS rules you actually use, keeping the output small.

  • Save tailwind.config.js.

Next, you need to add Tailwind's core directives to your CSS file so Tailwind's utility classes become available.

  • Open src/login/main.css in VSCodium.
  • Add these three lines at the very top of the file, above your existing CSS rules:
/* Import Tailwind's base reset, component classes, and utility classes */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Your existing kc- CSS overrides stay below this line */

What do these directives do?

  • @tailwind base injects a CSS reset that normalizes styles across browsers.
  • @tailwind components injects any reusable component classes (used by plugins and custom components).
  • @tailwind utilities injects all of Tailwind's utility classes like p-4, text-white, and rounded-lg.
  • Save src/login/main.css.

Both shadcn/ui and Magic UI import components using the @/ path alias, which is shorthand for your src/ directory. The Keycloakify starter does not include this alias by default, so you need to configure it in three places.

  • First, install the @types/node package so you can use Node's path module in your Vite config by running:
npm install -D @types/node
  • Open tsconfig.json in VSCodium.
  • Find the compilerOptions object. Add baseUrl and paths inside it so the file looks like this:
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

    /* Path alias for @/ imports */
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

What does the path alias do?

The baseUrl tells TypeScript where to start resolving paths. The paths mapping means any import starting with @/ resolves to your src/ directory. For example, @/components/ui/shimmer-button resolves to src/components/ui/shimmer-button.

  • Save tsconfig.json.

You also need to add the same alias to tsconfig.app.json so the alias is available when Vite compiles your application code.

  • Create a new file called tsconfig.app.json in the root of your keycloakify-starter folder.
  • Paste the following contents:
{
  "compilerOptions": {
    /* Path alias for @/ imports */
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
  • Save tsconfig.app.json.

TypeScript now understands the @/ alias, but Vite also needs its own alias configuration to resolve the same paths at build time.

  • Open vite.config.ts in VSCodium.
  • Replace the entire file with the following code, which adds the path alias alongside the existing Keycloakify plugin:
import path from "path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { keycloakify } from "keycloakify/vite-plugin";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    keycloakify({
      accountThemeImplementation: "none"
    })
  ],
  // Resolve the @/ alias so Vite can find shadcn/Magic UI components
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});

What changed here?

  • import path from "path" brings in Node's path module so you can construct absolute file paths.
  • The resolve.alias block maps @ to your src/ directory. This mirrors the TypeScript path alias so both the editor and the build tool resolve imports the same way.
  • The existing keycloakify() plugin stays untouched. The alias lives in a separate resolve block.
  • Save vite.config.ts.

Seeing red squiggles on the path import?

If VSCodium underlines import path from "path", make sure you installed @types/node in the previous terminal command. Run npm install -D @types/node again if needed.

help me fix the path import error in vite.config.ts

Initialize shadcn/ui and install Magic UI components

shadcn/ui is a component library that installs components directly into your source code as files, not as npm packages. Magic UI builds on top of shadcn/ui, so you need to initialize shadcn first.

  • Initialize shadcn/ui in your project by running:
npx shadcn@latest init

What will the CLI ask?

The shadcn CLI detects your Vite project automatically. It may ask you to select a base color and confirm configuration options. Accept the defaults or choose your preferred color. The CLI creates a components.json file and sets up import aliases for your project.

Seeing a framework detection error?

Make sure you are running the command from inside the keycloakify-starter folder where package.json and vite.config.ts live. The CLI uses these files to detect your framework.

help me fix the shadcn init framework detection error

Now you can install the two Magic UI components you will use for the branded login page.

  • Install the ShimmerButton component by running:
npx shadcn@latest add @magicui/shimmer-button

What is ShimmerButton?

ShimmerButton is an animated button with a shimmering light effect that sweeps across its surface. You will use it to replace the default Keycloak sign-in button with something more polished.

  • Install the DotPattern component by running:
npx shadcn@latest add @magicui/dot-pattern

What is DotPattern?

DotPattern renders a subtle SVG dot grid as a background. It auto-resizes to fill its container and adds a professional, modern feel without being distracting on an authentication page.

  • Verify both components were installed by running:
ls src/components/ui/

You should see shimmer-button.tsx and dot-pattern.tsx listed in the output.

Don't see the component files?

Make sure you ran the npx shadcn@latest add commands after initializing shadcn with npx shadcn@latest init. The init step creates the components.json file that tells the CLI where to place components.

help me troubleshoot missing Magic UI components

Why are components copied as files?

Unlike traditional npm packages, shadcn/ui and Magic UI copy component source code directly into your project. This means you own the code and can customize every line. It also works perfectly with Keycloakify's build system because the components are just regular React files in your source tree.

Commit your Tailwind and Magic UI setup
  • Stage and commit all changes by running these commands:
git add -A
git commit -m "feat: add Tailwind CSS and Magic UI components"
  • Confirm your commit was recorded by running:
git log --oneline -1

You should see the message feat: add Tailwind CSS and Magic UI components in the output.

✔️ Awesome, I've got everything!

Double check that you have saved all your files before moving on.

ⓧ I'd like to double check the full code

Here is the full content of each file you modified or created in this step.

import path from "path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { keycloakify } from "keycloakify/vite-plugin";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    keycloakify({
      accountThemeImplementation: "none"
    })
  ],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
/** @type {import('tailwindcss').Config} */
export default {
  content: ["./src/**/*.{ts,tsx,js,jsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
}

Your toolkit is ready. Tailwind CSS gives you utility classes for rapid styling, and Magic UI gives you the ShimmerButton and DotPattern components for a polished, animated look. Next up, you will eject the login page and redesign it with all of these tools.

Eject and Redesign the Login Page

So far, your CSS customizations have been re-skinning the existing login layout. The structure of the page, its HTML elements and React logic, stayed the same.

To get full control over the login page, you need to eject it. Ejecting copies the full React component into your project so you can rewrite the HTML structure, swap in your own components, and control every pixel.

In this step, get ready to:
  • Eject the login page component into your project.
  • Redesign the login page with ShimmerButton, DotPattern, and Tailwind CSS.
  • Disable default PatternFly styles and preview in Storybook.
Eject the login page component

CSS customization lets you re-skin the existing layout. Component-level customization gives you full control over the HTML structure and React logic.

CSS vs component-level customization

With CSS customization (what you did in the previous step), you target kc- prefixed classes to change colors, spacing, and borders. The underlying HTML stays the same.

With component-level customization, you eject the entire React component into your project. You can rewrite the JSX, add new components, and restructure the page however you want.

  • Eject the login page by running this command in your terminal:
npx keycloakify eject-page
  • When prompted, select login.ftl from the list.

What does this command do?

The eject-page command copies the full React component for the selected page from Keycloakify's source code into your project. For the login page, this creates src/login/pages/Login.tsx.

The command also prints instructions to update KcPage.tsx with a lazy import and a new case in the switch statement. Follow those instructions in the console output.

The console output tells you exactly which lines to add to KcPage.tsx. Apply those changes now so the ejected page gets picked up by the application.

  • Open src/login/pages/Login.tsx in VSCodium to explore the ejected component.

You should see a full React component with username and password fields, social provider buttons, a "remember me" checkbox, and a "forgot password" link. This is all the code that powers the default Keycloak login page.

What is inside the ejected Login.tsx?

The component receives kcContext (Keycloak's runtime data like URLs, realm settings, and error messages) and i18n (internationalization helpers for translated text). It wraps everything in a Template component that handles the shared page layout.

The kcClsx function generates CSS class names from Keycloakify's class system. When you set doUseDefaultCss to false later, these classes become empty strings. You can then replace them with Tailwind classes.

Don't see Login.tsx in src/login/pages/?

Make sure you ran the command from the keycloakify-starter folder and selected login.ftl when prompted. If you selected a different page, run the command again and choose login.ftl.

Help me troubleshoot why Login.tsx was not created after running npx keycloakify eject-page

Redesign the login page with Magic UI and Tailwind

Now you will replace the default login page with your own branded design. You will swap the plain submit button for a ShimmerButton, add a DotPattern animated background, and apply Tailwind CSS utility classes throughout.

Before you start editing the login component, you need to disable Keycloak's default PatternFly styles so they do not clash with your Tailwind classes.

  • Open src/login/KcPage.tsx.
  • Find the case "login.ftl" block that was added when you ejected the page.
  • Find the doUseDefaultCss prop inside that case. It is currently set to true:
doUseDefaultCss={true}
  • Change it to false so the line now looks like this:
doUseDefaultCss={false}

Why disable default CSS?

Setting doUseDefaultCss to false strips out all of Keycloak's built-in PatternFly styles. The global PatternFly stylesheet is not even loaded.

This gives you a blank canvas to style entirely with Tailwind. Without this, PatternFly's layout rules would fight your custom styles.

  • Save KcPage.tsx.

Now it is time to redesign the login component itself. You will replace the entire contents of Login.tsx with your branded version.

  • Open src/login/pages/Login.tsx.
  • Select all the code in the file and replace it with the following. Start by pasting the imports and component signature:
import { useState, useEffect, useReducer } from "react";
import { assert } from "keycloakify/tools/assert";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
// Import Magic UI components
import { ShimmerButton } from "@/components/ui/shimmer-button";
import { DotPattern } from "@/components/ui/dot-pattern";

export default function Login(props: PageProps<Extract<KcContext, { pageId: "login.ftl" }>, I18n>) {
    const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;

    const { kcClsx } = getKcClsx({
        doUseDefaultCss,
        classes
    });

    const { social, realm, url, usernameHidden, login, auth, registrationDisabled, messagesPerField } = kcContext;

    const { msg, msgStr } = i18n;

    const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);

What is new in the imports?

Two new imports pull in the Magic UI components you installed earlier. ShimmerButton replaces the plain HTML submit button. DotPattern adds the animated dot background behind the login form.

The @/ import alias resolves to ./src/ thanks to the path alias you configured in the previous step.

  • Below the component setup you just pasted, add the return statement with the Template wrapper and DotPattern background:
    return (
        <Template
            kcContext={kcContext}
            i18n={i18n}
            doUseDefaultCss={doUseDefaultCss}
            classes={classes}
            displayMessage={!messagesPerField.existsError("username", "password")}
            headerNode={msg("loginAccountTitle")}
            displayInfo={realm.password && realm.registrationAllowed && !registrationDisabled}
            infoNode={
                <div id="kc-registration-container">
                    <div id="kc-registration">
                        <span>
                            {msg("noAccount")}{" "}
                            <a tabIndex={8} href={url.registrationUrl} className="text-blue-400 hover:underline">
                                {msg("doRegister")}
                            </a>
                        </span>
                    </div>
                </div>
            }
            socialProvidersNode={
                <>
                    {realm.password && social.providers?.length && (
                        <div id="kc-social-providers" className="mt-6">
                            <hr className="border-gray-600" />
                            <h2 className="text-sm text-gray-400 my-4 text-center">{msg("identity-provider-login-label")}</h2>
                            <ul className="flex flex-col gap-2">
                                {social.providers.map((...[p, , providers]) => (
                                    <li key={p.alias}>
                                        <a
                                            id={`social-${p.alias}`}
                                            className="flex items-center justify-center gap-2 w-full py-2 px-4 rounded-lg border border-gray-600 text-gray-200 hover:bg-gray-700 transition"
                                            type="button"
                                            href={p.loginUrl}
                                        >
                                            {p.iconClasses && <i className={clsx(p.iconClasses)} aria-hidden="true"></i>}
                                            <span>{p.displayName}</span>
                                        </a>
                                    </li>
                                ))}
                            </ul>
                        </div>
                    )}
                </>
            }
        >

What does this section do?

The Template component provides the shared page layout (header, footer, messages). You pass it headerNode, infoNode (the registration link), and socialProvidersNode (social login buttons).

Each node now uses Tailwind classes instead of kcClsx for styling. For example, the social provider links use rounded-lg, border-gray-600, and hover:bg-gray-700.

  • Next, paste the login form body with the DotPattern background, input fields, and ShimmerButton. This goes directly after the closing > of the Template's opening tag:
            {/* Full-page wrapper with animated dot background */}
            <div className="relative flex items-center justify-center min-h-[400px] p-8">
                <DotPattern
                    className="absolute inset-0 opacity-30 [mask-image:radial-gradient(ellipse_at_center,white,transparent_70%)]"
                />

                {/* Login card */}
                <div className="relative z-10 w-full max-w-md bg-gray-900 rounded-2xl shadow-xl p-8 border border-gray-700">
                    <div id="kc-form">
                        <div id="kc-form-wrapper">
                            {realm.password && (
                                <form
                                    id="kc-form-login"
                                    onSubmit={() => {
                                        setIsLoginButtonDisabled(true);
                                        return true;
                                    }}
                                    action={url.loginAction}
                                    method="post"
                                    className="space-y-6"
                                >
                                    {/* Username / email field */}
                                    {!usernameHidden && (
                                        <div>
                                            <label htmlFor="username" className="block text-sm font-medium text-gray-300 mb-1">
                                                {!realm.loginWithEmailAllowed
                                                    ? msg("username")
                                                    : !realm.registrationEmailAsUsername
                                                        ? msg("usernameOrEmail")
                                                        : msg("email")}
                                            </label>
                                            <input
                                                tabIndex={2}
                                                id="username"
                                                className="w-full px-4 py-2 rounded-lg bg-gray-800 border border-gray-600 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
                                                name="username"
                                                defaultValue={login.username ?? ""}
                                                type="text"
                                                autoFocus
                                                autoComplete="username"
                                                aria-invalid={messagesPerField.existsError("username", "password")}
                                            />
                                            {messagesPerField.existsError("username", "password") && (
                                                <span id="input-error" className="text-red-400 text-sm mt-1 block" aria-live="polite">
                                                    {messagesPerField.getFirstError("username", "password")}
                                                </span>
                                            )}
                                        </div>
                                    )}

                                    {/* Password field */}
                                    <div>
                                        <label htmlFor="password" className="block text-sm font-medium text-gray-300 mb-1">
                                            {msg("password")}
                                        </label>
                                        <PasswordWrapper kcClsx={kcClsx} i18n={i18n} passwordInputId="password">
                                            <input
                                                tabIndex={3}
                                                id="password"
                                                className="w-full px-4 py-2 rounded-lg bg-gray-800 border border-gray-600 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
                                                name="password"
                                                type="password"
                                                autoComplete="current-password"
                                                aria-invalid={messagesPerField.existsError("username", "password")}
                                            />
                                        </PasswordWrapper>
                                        {usernameHidden && messagesPerField.existsError("username", "password") && (
                                            <span id="input-error" className="text-red-400 text-sm mt-1 block" aria-live="polite">
                                                {messagesPerField.getFirstError("username", "password")}
                                            </span>
                                        )}
                                    </div>

                                    {/* Remember me and forgot password */}
                                    <div className="flex items-center justify-between text-sm">
                                        <div id="kc-form-options">
                                            {realm.rememberMe && !usernameHidden && (
                                                <div className="flex items-center gap-2">
                                                    <input
                                                        tabIndex={5}
                                                        id="rememberMe"
                                                        name="rememberMe"
                                                        type="checkbox"
                                                        className="rounded bg-gray-800 border-gray-600"
                                                        defaultChecked={!!login.rememberMe}
                                                    />
                                                    <label htmlFor="rememberMe" className="text-gray-400">{msg("rememberMe")}</label>
                                                </div>
                                            )}
                                        </div>
                                        <div>
                                            {realm.resetPasswordAllowed && (
                                                <a tabIndex={6} href={url.loginResetCredentialsUrl} className="text-blue-400 hover:underline">
                                                    {msg("doForgotPassword")}
                                                </a>
                                            )}
                                        </div>
                                    </div>

                                    {/* ShimmerButton replaces the default submit button */}
                                    <div id="kc-form-buttons">
                                        <input type="hidden" id="id-hidden-input" name="credentialId" value={auth.selectedCredential} />
                                        <ShimmerButton
                                            type="submit"
                                            tabIndex={7}
                                            disabled={isLoginButtonDisabled}
                                            className="w-full py-3 text-base font-semibold"
                                            shimmerColor="#60a5fa"
                                            shimmerSize="0.08em"
                                            background="rgba(59, 130, 246, 0.8)"
                                        >
                                            {msgStr("doLogIn")}
                                        </ShimmerButton>
                                    </div>
                                </form>
                            )}
                        </div>
                    </div>
                </div>
            </div>
        </Template>
    );
}

What does this code do?

The DotPattern component renders an SVG-based animated dot background. The opacity-30 and radial gradient mask keep it subtle so it does not distract from the form.

The login card sits on top with relative z-10, using a dark background (bg-gray-900), rounded corners (rounded-2xl), and a shadow. All inputs use consistent Tailwind classes for spacing, borders, and focus states.

The ShimmerButton replaces the default <input type="submit">. It still uses type="submit" so the form submission works. The shimmerColor and background props give the button a branded blue glow effect.

  • Finally, paste the PasswordWrapper helper function at the bottom of the file, after the closing brace of the Login function:
function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: string; children: JSX.Element }) {
    const { kcClsx, i18n, passwordInputId, children } = props;

    const { msgStr } = i18n;

    const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);

    useEffect(() => {
        const passwordInputElement = document.getElementById(passwordInputId);

        assert(passwordInputElement instanceof HTMLInputElement);

        passwordInputElement.type = isPasswordRevealed ? "text" : "password";
    }, [isPasswordRevealed]);

    return (
        <div className="relative">
            {children}
            <button
                type="button"
                className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-200"
                aria-label={msgStr(isPasswordRevealed ? "hidePassword" : "showPassword")}
                aria-controls={passwordInputId}
                onClick={toggleIsPasswordRevealed}
            >
                <i className={kcClsx(isPasswordRevealed ? "kcFormPasswordVisibilityIconHide" : "kcFormPasswordVisibilityIconShow")} aria-hidden />
            </button>
        </div>
    );
}

What does PasswordWrapper do?

This helper wraps the password input with a show/hide toggle button. The useReducer hook toggles between revealed and hidden states. The useEffect switches the input's type attribute between text and password.

The Tailwind classes position the toggle button absolutely inside the input field, in the top-right corner.

  • Save Login.tsx.

Seeing TypeScript errors in Login.tsx?

Make sure the @/ path alias is configured in both tsconfig.json and tsconfig.app.json. Without the alias, the ShimmerButton and DotPattern imports cannot resolve.

Also confirm that src/components/ui/shimmer-button.tsx and src/components/ui/dot-pattern.tsx exist. If they are missing, reinstall them:

  • Run npx shadcn@latest add @magicui/shimmer-button.
  • Run npx shadcn@latest add @magicui/dot-pattern.

Help me fix TypeScript errors in my redesigned Login.tsx file

Preview the redesigned login page in Storybook

Your Storybook story for the login page is already set up from an earlier step. Because Vite's HMR (Hot Module Replacement) is active, Storybook picks up your changes automatically.

  • If Storybook is not already running, start it by running:
npm run storybook
  • Open Storybook in your browser (it usually launches at http://localhost:6006).
  • In the sidebar, expand the login page stories.
  • Click through the different scenarios: the default login view, the login-with-error view, and the login-with-social-providers view.

You should see your dark-themed login card with the animated dot pattern behind it. The sign-in button has a shimmering blue glow. Error messages appear in red text below the input fields.

Login page still looks like the old default?

Double-check that you set doUseDefaultCss={false} in KcPage.tsx for the login.ftl case. If it is still true, PatternFly styles will override your Tailwind classes.

Also verify that the ejected Login component is actually being imported in KcPage.tsx. The eject command prints instructions for adding a lazy import and a switch case. If you missed those, the default page renders instead of your custom one.

Help me debug why my branded login page is not showing up in Storybook

  • Commit your changes by running these commands:
git add -A
git commit -m "feat: redesign login page with Magic UI components"

What does this commit do?

The git add -A stages all new and modified files, including the ejected Login.tsx and the updated KcPage.tsx. The commit message follows the conventional commits format you have been using throughout this project.

Your login page is now fully branded with Tailwind CSS styling, a ShimmerButton for the sign-in action, and a DotPattern animated background. Next up, you will build the theme into a JAR file and deploy it to a real Keycloak Docker container.

Build and Deploy Your Theme to Keycloak

Your login page is fully redesigned with a shimmer button and an animated dot background. Now it's time to package everything into a format that Keycloak can actually load.

Keycloakify compiles your React code into a .jar theme file that Keycloak can load as a plugin. In this step, you'll build that file and deploy it to a live Keycloak instance running in Docker.

In this step, get ready to:
  • Build a .jar theme file from your React project.
  • Launch a Keycloak Docker container with your custom theme loaded.
  • Activate your branded theme in the Keycloak admin console.
Build the theme JAR file

The build process uses Maven behind the scenes to package your React code into a Java Archive (JAR) file. Before building, confirm that Maven is available on your system.

  • Check that Maven is installed by running this command:
mvn --version

You should see a version number like Apache Maven 3.x.x along with your Java version printed in the terminal.

Seeing 'command not found'?

If Maven isn't recognized, install it in your WSL2 terminal by running sudo apt-get install maven. This also installs a compatible Java JDK as a dependency.

help me install Maven in WSL2 so I can build my Keycloakify theme

  • Build your theme into a JAR file by running:
npm run build-keycloak-theme

What does this command do?

This command runs two stages. First, Vite bundles your React components, Tailwind styles, and Magic UI animations into optimized static files. Then, Keycloakify's compiler packages those files into a .jar file that Keycloak recognizes as a theme provider.

The build output lands in the dist_keycloak/ directory at the root of your project.

The build takes a minute or two. Wait until it finishes and you see your terminal prompt return.

  • Confirm the JAR was created by running:
ls dist_keycloak/*.jar

You should see at least one .jar file listed, such as keycloakify-starter-keycloak-theme-6.1.5.jar. Note the exact file name. You'll need it in the next substep.

Build failed or no JAR file?

If the build fails with a Maven-related error, check that mvn --version shows Maven 3.1.1 or higher and Java 7 or higher.

If the build succeeded but ls dist_keycloak/*.jar shows nothing, make sure you're running the command from the keycloakify-starter folder.

help me troubleshoot my Keycloakify theme build

Run Keycloak in a Docker container

With the JAR file built, you can spin up a real Keycloak server and load your theme into it. Docker lets you run Keycloak locally without installing the full server on your machine.

  • Make sure Docker is running on your machine. If you're using Docker Desktop, confirm the application is open.
  • Launch a Keycloak container with your theme JAR mounted by running this command:
docker run -d \
  --name keycloak \
  -p 8080:8080 \
  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
  -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
  -v "$(pwd)/dist_keycloak":/opt/keycloak/providers \
  quay.io/keycloak/keycloak:26.0.7 start-dev

What does this command do?

  • -d runs the container in the background so you keep your terminal free.
  • -p 8080:8080 maps port 8080 on your machine to port 8080 inside the container.
  • The two -e flags set the admin username and password for the Keycloak console to admin / admin.
  • -v mounts your dist_keycloak/ directory into /opt/keycloak/providers/ inside the container. This is where Keycloak looks for theme plugins.
  • start-dev launches Keycloak in development mode, which is perfect for local testing.
  • Check that Keycloak is starting up by following the container logs:
docker logs -f keycloak

Wait until you see a message like Listening on: http://0.0.0.0:8080 in the output. This means Keycloak is ready to accept connections. Keycloak can take 30 to 60 seconds to start.

  • Press Ctrl+C to stop following the logs once you see the ready message.
  • Open your browser and go to http://localhost:8080. You should see the Keycloak welcome page.

Container failed to start?

If you see an error about port 8080 already being in use, stop any other service on that port first. If Storybook is still running, press Ctrl+C in the Storybook terminal to stop it. Storybook uses port 6006 by default, but double check that nothing else is using 8080.

If you see an error about a container named keycloak already existing, remove it first by running docker rm -f keycloak and then try the docker run command again.

If Docker itself isn't running, make sure Docker Desktop is open or that the Docker daemon is active in WSL2.

help me troubleshoot my Keycloak Docker container

Activate your custom theme and see it live

Keycloak is running and your theme JAR is loaded. The last step is to tell Keycloak to use your custom theme for the login page.

  • On the Keycloak welcome page at http://localhost:8080, click Administration Console.
  • Enter admin for the username.
  • Enter admin for the password.
  • Click Sign In.

You should now see the Keycloak admin dashboard for the master realm.

  • In the left sidebar, click Realm settings.
  • Click the Themes tab.
  • In the Login theme dropdown, select keycloakify-starter.
  • Click Save.

Why 'keycloakify-starter'?

The theme name in the dropdown comes from the project's package.json name field. The starter project ships with the name keycloakify-starter. If you wanted a different theme name, you'd change the name in package.json and rebuild.

Time to see your branded login page in action.

  • Open a new browser tab.
  • Navigate to http://localhost:8080/realms/master/account/.

This URL opens the Keycloak account console, which requires authentication. Keycloak redirects you to the login page, and you should see your custom branded design with the animated dot pattern background and the shimmer button.

Still seeing the default Keycloak login page?

Make sure you selected keycloakify-starter in the Login theme dropdown and clicked Save. The dropdown defaults to the built-in Keycloak theme, so it's easy to miss.

If the theme isn't appearing in the dropdown at all, the JAR might not have loaded correctly. Restart the container by running docker restart keycloak and wait for it to start up again. Then check the Themes tab once more.

Try a hard refresh in your browser (Cmd+Shift+R on macOS or Ctrl+Shift+R on Windows) to clear any cached styles.

help me get my custom Keycloak theme to show up

One last thing. Commit your work to lock in this milestone.

  • Commit your changes by running these commands:
git add .
git commit -m "feat: build and deploy custom theme to Keycloak"

Secret mission

Customize the Registration Page

Your login page has a polished, branded look. New users clicking Register still see the plain default Keycloak form. Take your branding further by customizing the registration page to match.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing cloud costs. The main resources to manage are your Docker container, Docker image, and project files on your machine.

Resources you used:

  • Keycloakify starter project folder (keycloakify-starter on your Desktop).
  • Built theme JAR file in dist_keycloak/.
  • Keycloak Docker container (running on port 8080).
  • Keycloak Docker image (quay.io/keycloak/keycloak:26.0.7).
  • Maven and Java JDK installed via apt in WSL2.
  • Local Git repository with feature commits.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Keep everything running

No action needed. Choose this if you're still actively building on your theme or plan to continue soon.

  • Your Keycloak Docker container stays running on port 8080.
  • Your project files and Git history remain on your Desktop.
  • The Docker container uses some memory while running. Stop it later when you no longer need it.

Pause - I'll come back to this later

Shut down the running Docker container to free up memory. Your project files and built JAR stay on disk so you can pick up where you left off.

  • Stop the Keycloak Docker container by running:
docker ps
  • Find the container ID for the Keycloak container in the output, then stop it by running:
docker stop [[CONTAINER_ID="your Keycloak container ID"]]
  • Your project files, Git history, and built JAR all remain in the keycloakify-starter folder.
  • When you're ready to resume, start the container again with docker start followed by the same container ID.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes your project files, Docker container, and Docker image.

  • Stop and remove the Keycloak Docker container by running:
docker ps
docker stop [[CONTAINER_ID="your Keycloak container ID"]]
docker rm [[CONTAINER_ID="your Keycloak container ID"]]
  • Remove the Keycloak Docker image by running:
docker rmi quay.io/keycloak/keycloak:26.0.7
  • Delete the project folder from your Desktop by running:
cd ~/Desktop
rm -rf keycloakify-starter
  • Maven and Java remain installed system-wide. To remove them, run:
sudo apt-get remove maven

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully custom Keycloak login experience from scratch and deployed it to a real Keycloak server running in Docker.

You've learned how to:

  • Ejected and fully redesigned the Keycloak login page using React, Tailwind CSS, and animated Magic UI components like ShimmerButton and DotPattern.
  • Used Storybook to preview and iterate on your theme across multiple login scenarios without needing a running Keycloak server.
  • Built a .jar theme file with Keycloakify and deployed it to a local Keycloak instance running in a Docker container.
  • Customized the registration page to match your branded login design.

Ready to quiz yourself?