Last updated: June 2026
When a custom Keycloak theme does not show up, the cause is almost always theme caching (enabled by default in production) or an incorrect theme directory structure, not your CSS. Keycloak caches both rendered templates and static resources unless you explicitly disable caching, and it only discovers themes placed under themes/<name>/<type>/ with a valid theme.properties file present. After making changes, you must either restart Keycloak or run it with caching disabled before your updates will appear.
This guide walks through every layer of Keycloak’s theme system — how it discovers themes, how you select them per realm, what caching settings control visibility, and how the Quarkus-based distribution differs from older references you may find online. By the end you will have a repeatable checklist for diagnosing why a custom theme is not loading and exactly what to change.
How Keycloak discovers themes
Keycloak looks for themes in two locations at startup:
- The
themes/directory in the Keycloak installation root. - Provider JARs dropped into the
providers/directory.
At startup, Keycloak scans both locations and builds an in-memory registry of available themes. If your theme folder or JAR is not present, correctly named, or is missing required files at scan time, Keycloak will not add it to the registry, and it will never appear in the admin console dropdown regardless of cache settings.
The themes/ directory structure
Every theme lives under its own named folder inside themes/. Within that folder, Keycloak expects one or more theme-type subdirectories:
themes/
└── my-company/
├── login/
│ ├── theme.properties
│ └── resources/
│ ├── css/
│ └── img/
├── account/
│ └── theme.properties
└── email/
└── theme.properties
The four supported theme types are:
- login — the login, registration, and error pages users see during authentication flows
- account — the self-service account management console
- email — email templates (password reset, verification, etc.)
- admin — the Keycloak admin console itself (rarely customized)
You only need to include the types you actually want to customize. A theme named my-company with only a login/ subdirectory is valid; Keycloak falls back to the base theme for account and email.
The theme.properties file
Each theme-type directory must contain a theme.properties file. Without it, Keycloak ignores the directory entirely. A minimal file looks like this:
parent=keycloak
import=common/keycloak
The parent directive tells Keycloak which theme to inherit from when a template or resource is not found in your theme. For login themes, keycloak (or keycloak.v2 for the newer React-based account console) is the standard parent. Setting an incorrect or misspelled parent causes Keycloak to fall back to the base theme silently rather than throwing a visible error.
For a deeper walkthrough of building out theme templates and overriding specific pages, see the Keycloak theme customization guide.
Selecting the theme in Realm Settings
Even when Keycloak has discovered a theme correctly, it does not apply automatically. You must assign it to a realm:
- Open the Keycloak admin console and select your realm.
- Navigate to Realm Settings > Themes.
- Use the Login theme, Account theme, Email theme, or Admin console theme dropdowns to select your theme by name.
- Click Save.
If your theme name does not appear in the dropdown, the theme has not been discovered. Go back and verify directory structure and theme.properties before continuing.
The number-one cause: theme and template caching
This is where most developers lose time. Keycloak’s production configuration caches both the compiled theme files and the rendered Freemarker templates. The cache is populated at startup and is not invalidated when you modify files on disk. This means:
- You deploy a corrected CSS file.
- You reload the login page.
- The old version appears.
- You assume your change did not work.
The caching behavior is controlled by two SPI settings:
| Setting | Default (production) | Effect |
|---|---|---|
spi-theme-cache-themes |
true |
Caches the full theme registry built at startup |
spi-theme-cache-templates |
true |
Caches compiled Freemarker templates |
spi-theme-static-max-age |
2592000 (30 days) |
Browser cache max-age for static resources (CSS, JS, images) |
In production (using start), all three are active. In development mode (using start-dev), Keycloak automatically sets spi-theme-cache-themes=false, spi-theme-cache-templates=false, and spi-theme-static-max-age=-1, which disables all three layers of caching.
Disabling caching for development
To iterate on themes without restarting Keycloak, pass these flags when starting in dev mode:
bin/kc.sh start-dev
--spi-theme-static-max-age=-1
--spi-theme-cache-themes=false
--spi-theme-cache-templates=false
You can also set them as environment variables:
KC_SPI_THEME_STATIC_MAX_AGE=-1
KC_SPI_THEME_CACHE_THEMES=false
KC_SPI_THEME_CACHE_TEMPLATES=false
With caching disabled, changes to .ftl templates and CSS files reflect on the next browser reload. Changes to theme.properties (including the parent setting) still require a restart because they affect theme discovery, not template rendering.
Do not disable theme caching in production. The performance cost is significant — Freemarker template compilation runs on every page request. Use start-dev only on local machines or in a dedicated development environment.
For Quarkus-based Docker deployments, see Keycloak Docker Compose production setup for the correct environment variable structure.
Packaging themes as JAR providers
The alternative to the themes/ directory is to package your theme as a Keycloak provider JAR and drop it into the providers/ directory. This approach is common for organizations that distribute their theme as a reusable artifact across multiple Keycloak instances.
A provider JAR follows this internal layout:
my-theme-provider.jar
└── META-INF/
│ └── keycloak-themes.json
└── theme/
└── my-company/
└── login/
├── theme.properties
└── resources/
The keycloak-themes.json manifest declares the theme names inside the JAR:
{
"themes": [
{
"name": "my-company",
"types": ["login", "account"]
}
]
}
After placing the JAR in providers/, run bin/kc.sh build to reindex providers before starting Keycloak. Skipping the build step is a common reason JAR-packaged themes do not appear — Keycloak 26.x with Quarkus requires an explicit build phase when the providers/ directory contents change.
For SPI development patterns beyond theme packaging, the Keycloak custom SPI development guide covers the broader provider system.
Symptom, cause, and fix reference
| Symptom | Most likely cause | Fix |
|---|---|---|
| Theme not in dropdown at all | Missing theme.properties, wrong directory name, or JAR not built |
Verify directory structure; run kc.sh build after adding JARs |
| Theme in dropdown but old styles appear after edit | Template or static resource cache active | Restart Keycloak, or use --spi-theme-cache-themes=false in dev |
| CSS changes not visible even after restart | Browser cache serving stale static assets | Hard refresh (Ctrl+Shift+R) or set spi-theme-static-max-age=-1 |
| Login page falls back to default Keycloak theme | Realm setting not saved, or wrong theme assigned to wrong type | Re-check Realm Settings > Themes and save |
NullPointerException in Keycloak logs on theme load |
Misspelled parent= value or missing parent theme |
Check theme.properties parent value matches an existing theme name |
| Email templates still show default content | Email theme type not customized or not assigned | Add email/ subdirectory with theme.properties and assign in Realm Settings |
| Account console shows default theme | Mixing old Account v1 and new Account v2 themes | Use keycloak.v2 as parent for the account type |
Quarkus distribution specifics
Keycloak 26.x runs exclusively on Quarkus. If you are reading older blog posts or Stack Overflow answers that reference standalone.xml, WildFly configuration, or the standalone/themes/ path, that information does not apply to current releases.
Key differences in Quarkus Keycloak:
- Two startup commands:
start-dev(development, caching off) vsstart(production, caching on). These are not interchangeable —startwill fail ifhostnameis not configured;start-devaccepts a loose configuration suitable for local work. - Build step: Configuration changes to
providers/, and some SPI configurations, requirebin/kc.sh buildbefore starting. This pre-bakes configuration into the Quarkus application. - Environment variables: Keycloak SPI settings map to environment variables using the pattern
KC_SPI_<SPI_NAME>_<SETTING_NAME>in uppercase with hyphens replaced by underscores. themes/path: Still valid and still the simplest option. Mount it as a volume in Docker, or include it in your custom Docker image built on top ofquay.io/keycloak/keycloak.
A Docker Compose setup targeting the dev mode with volume-mounted themes looks like this:
services:
keycloak:
image: quay.io/keycloak/keycloak:26.0
command: start-dev
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
KC_SPI_THEME_STATIC_MAX_AGE: "-1"
KC_SPI_THEME_CACHE_THEMES: "false"
KC_SPI_THEME_CACHE_TEMPLATES: "false"
volumes:
- ./themes:/opt/keycloak/themes
ports:
- "8080:8080"
The volume mount means you can edit files in ./themes on your host and see changes immediately without rebuilding the container image.
Custom theme not appearing in the admin dropdown
If you have placed your theme correctly but it still does not appear in the Realm Settings dropdown, work through this checklist in order:
- Confirm the directory is at
themes/<theme-name>/<type>/theme.properties— not nested further or at the wrong level. - Confirm the theme name (the parent folder) uses only lowercase letters, digits, and hyphens. Spaces and uppercase letters can cause discovery failures.
- If using a JAR provider, confirm you ran
bin/kc.sh buildafter adding it toproviders/. - Check the Keycloak startup logs for lines containing
Loading themeor anyWARN/ERRORentries related to themes. Discovery errors are logged at startup, not at request time. - Restart Keycloak fully. Even with caching disabled, theme discovery only happens at startup.
CSS not applying after theme selection
Once a theme is selected and visible, CSS changes sometimes still do not appear. The layers to check:
Parent theme import in theme.properties: If you want to extend rather than replace the base styles, your stylesheet must import the parent theme’s CSS. Add this to resources/css/login.css:
@import url("../../../keycloak/login/resources/css/login.css");
The relative path traverses up three levels to reach the base theme. A missing or broken import causes the entire parent stylesheet to be skipped silently, leaving your page with raw HTML styling.
Static resource path in templates: If you have overridden a .ftl template and changed a resource path, confirm the URL resolves. Keycloak provides the ${url.resourcesPath} Freemarker variable for building correct resource URLs:
<link rel="stylesheet" href="${url.resourcesPath}/css/custom.css" />
Browser cache: Keycloak sets long cache headers on static resources in production. A hard refresh (Ctrl+Shift+R on most browsers) bypasses the disk cache. If you need to force re-validation across all users, change the resource filename or add a query string, then restart Keycloak.
For configuring email-related templates and customizing message content, the Keycloak email configuration guide covers the email theme type in detail.
Getting started with a clean theme baseline
If you are setting up themes for the first time rather than debugging an existing one, the Keycloak getting started 2026 guide covers the initial Keycloak setup, including realm creation and admin console navigation, before you reach the theme configuration steps.
Frequently asked questions
Why is my Keycloak custom theme not showing?
The most common reasons are a missing or malformed theme.properties file, placing theme files in the wrong directory depth, or theme discovery cache still holding the previous state. Keycloak only discovers themes at startup, so always restart after adding a new theme folder. If the theme appears in the dropdown but pages still show the old design, template caching is the cause — use --spi-theme-cache-templates=false during development or restart in production.
How do I disable theme caching in Keycloak?
Pass --spi-theme-cache-themes=false --spi-theme-cache-templates=false --spi-theme-static-max-age=-1 as arguments to bin/kc.sh start-dev. Alternatively, set the equivalent environment variables: KC_SPI_THEME_CACHE_THEMES=false, KC_SPI_THEME_CACHE_TEMPLATES=false, and KC_SPI_THEME_STATIC_MAX_AGE=-1. These settings are intended for development only. In production, disable the Keycloak process and apply changes with a restart rather than running with caching off.
Where do Keycloak themes go?
Themes belong in the themes/ directory at the root of the Keycloak installation — for example, /opt/keycloak/themes/ in the standard Docker image. Each theme is a subdirectory named after the theme, containing type subdirectories (login/, account/, email/) with a theme.properties file in each type folder. Alternatively, package your theme as a JAR and place it in the providers/ directory, then run kc.sh build.
Do I need to restart Keycloak after changing a theme file?
In production (using kc.sh start), yes — Keycloak caches templates and the theme registry at startup. In development mode (kc.sh start-dev) with cache flags disabled, .ftl template and CSS changes are picked up on the next request without a restart. Changes to theme.properties itself always require a restart because they affect theme discovery.
Can I use the same theme across multiple realms?
Yes. A theme registered in the themes/ directory is available to all realms in that Keycloak instance. Each realm independently selects which theme to use via Realm Settings > Themes. You can have five realms all pointing to the same theme, or each using a different one.
Managing theme deployments, cache configuration, and version consistency across environments is overhead that compounds as your Keycloak footprint grows. Skycloak handles Keycloak operations — including upgrades, restarts, and environment parity — so your team can focus on building the authentication experience rather than maintaining infrastructure. See Skycloak’s managed plans if you want a production-ready Keycloak instance without the operational burden.