In this post, we’ll explain how to create branded Keycloak themes step-by-step using FreeMarker and Maven for quick development and easy deployment.
· Prerequisites
· Overview
∘ What is Apache FreeMarker®?
∘ Why Use Maven for Keycloak Theme Development?
∘ Themes in Keycloak
∘ Theme types
· Creating a custom theme
∘ Create a Maven Project
∘ Directory structure
∘ Understanding the Folder Structure and Key Files
· Deploying the Keycloak Theme
∘ Install the theme
∘ Enable the Custom Theme
· Conclusion
· References
Prerequisites
This is the list of all the prerequisites:
- An installed Keycloak instance
- Basic knowledge of Keycloak
- Knowledge of FreeMarker (for template customization)
- Maven 3.6.3 or higher
- Java 21 or higher
- IntelliJ IDEA, Visual Studio Code, or another IDE
This story is based on Keycloak version
26 or higher.
Overview
Customizing Keycloak’s themes allows you to create a seamless and branded authentication experience for your users. By tailoring the login pages, account management screens, and even email templates, you ensure consistent branding and improved user trust.
What is Apache FreeMarker®?
Apache FreeMarker® is a template engine: a Java library to generate text output (HTML web pages, e-mails, configuration files, source code, etc.) based on templates and changing data. Templates are written in the FreeMarker Template Language (FTL), which is a simple, specialized language (not a full-blown programming language like PHP). Usually, a general-purpose programming language (like Java) is used to prepare the data (issue database queries, do business calculations). Then, Apache FreeMarker displays that prepared data using templates. In the template you are focusing on how to present the data, and outside the template you are focusing on what data to present.
Why Use Maven for Keycloak Theme Development?
Maven is not required to build a Keycloak theme. We can go with a manual approach (copy files into the Keycloak server directory) or take a clean, maintainable, and automatable approach using Maven.
Maven lets us:
- Version control our theme
- Build and package themes into a
.jaror.zip - Automate theme deployment
Themes in Keycloak
Keycloak provides theme support for web pages and emails. This allows customizing the look and feel of end-user-facing pages so they can be integrated with your applications. The default Keycloak themes are packaged inside the org.keycloak.keycloak-themes-<version>.jar file, within the $KEYCLOAK_HOME/lib/lib/main folder. Here is the list of built-in themes:
- base: Provides basic layout, message handling, and FreeMarker macros
- keycloak: It is the default Keycloak theme, used in production. It adds full branding, CSS, and complete UI used in Keycloak v1 UI.
- keycloak.v2: This is the modernized UI theme introduced in Keycloak 22+, based on PatternFly 5. It is still under development. It is not enabled by default on older versions of Keycloak.
Theme types
Keycloak recognises five theme types, each corresponding to a distinct user-facing surface:

Themes are located under the /themes directory of the Keycloak distribution.
In this story, we’ll build a login theme using a Freemarker template with custom CSS and images.
Creating a custom theme
Create a Maven Project
I’ve created a Maven project using the IntelliJ IDE based on the quickstart archetype.

You can use the mvn command like this:
mvn archetype:generate -DgroupId=com.bootlabs \
-DartifactId=keycloak-custom-theme \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false
Directory structure
Create a directory structure for our custom theme as shown below:
keycloak-custom-theme/ # Root Maven project for the custom Keycloak theme
├── pom.xml # Maven config file (build, package, deploy the theme)
└── src/
└── main/
├── java/ # (Optional) Add custom Java classes (e.g., custom SPI extensions)
└── resources/
├── META-INF/
│ └── keycloak-themes.json # Describes available themes (especially for JAR packaging)
└── theme/
└── iam-redesign/ # custom theme name (used in Keycloak config)
└── login/ # Theme type: login (can also have 'account', 'admin', 'email')
├── login.ftl # FreeMarker template for login page
├── messages/
│ ├── messages_en.properties # i18n - English messages
│ ├── messages_es.properties # i18n - Spanish messages
│ ├── messages_fr.properties # i18n - French messages
├── resources/
│ ├── css/
│ │ └── login.css # Custom CSS for styling login page
│ ├── img/
│ │ ├── background.jpg # Background image
│ │ ├── favicon.ico # Favicon for branding
│ │ └── logo.png # Logo used in login header/footer
│ └── js/ # Custom JavaScript (optional)
├── template.ftl # Base layout used by other FTL files
└── theme.properties # Required file that declares theme metadata
Understanding the Folder Structure and Key Files
In this section, we’ll review the key files that make up a custom theme, explaining their function in simple terms.
src/main/resources/META-INF/keycloak-themes.json
It defines the metadata and structure of Keycloak’s custom themes. It is a JSON file containing an array of objects telling Keycloak that your theme exists and should be included at runtime. It acts as a registration file, making your theme accessible when Keycloak starts up.
In our example, we will define the theme name, which is “iam-redesign”:
{
"themes": [
{
"name" : "iam-redesign",
"types": [ "login" ]
}
]
}
theme.properties
The theme.properties file is a configuration file used to customize the appearance and behavior of a Keycloak theme. It is a Java properties file that contains key-value pairs representing various settings for the theme.
Here is the content of this file:
# ----------------------------------------
# Inheritance and Imports
# ----------------------------------------
# Inherit from the base Keycloak theme (provides default structure and styles)
parent=base
# Import shared resources from the common/keycloak theme folder
import=common/keycloak
# ----------------------------------------
# Stylesheets
# ----------------------------------------
# Load custom CSS files (located in resources/css/)
styles=css/login.css
# ----------------------------------------
# Meta Tags
# ----------------------------------------
# Set viewport meta tag to ensure mobile responsiveness
meta=viewport==width=device-width,initial-scale=1.0
# ----------------------------------------
# Layout and Structure
# ----------------------------------------
# Class applied to the <html> element
kcHtmlClass=login-pf
# Class applied to the main login container
kcLoginClass=login-pf-page
# ----------------------------------------
# Branding
# ----------------------------------------
# URL to navigate to when clicking the logo
kcLogoLink=https://www.sitecore.com
# ----------------------------------------
# Page Layout Containers
# ----------------------------------------
# Class for the outermost container div
kcContainerClass=container-fluid
# Class for the inner content section of the login form
kcContentClass=col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-6 col-lg-offset-3
# ----------------------------------------
# Form Styling
# ----------------------------------------
# Class for wrapping form groups (label + input)
kcFormGroupClass=form-group
kcLabelClass=control-label
# Input field styling
kcInputClass=form-control
# Error message styling for invalid inputs
kcInputErrorMessageClass=input-error
# Container class for options like "Remember me"
kcFormOptionsClass=form-options
# Container for form buttons (e.g., Login, Submit)
kcFormButtonsClass=form-buttons
# Container for additional form settings (e.g., forgot password)
kcFormSettingClass=login-pf-settings
kcFormOptionsWrapperClass=login-pf-options
# ----------------------------------------
# Buttons
# ----------------------------------------
# Base button class
kcButtonClass=btn
# Primary button (e.g., Login)
kcButtonPrimaryClass=btn-primary
# Default/secondary button (e.g., Cancel)
kcButtonDefaultClass=btn-default
# Large button size
kcButtonLargeClass=btn-lg
# Button spans full container width
kcButtonBlockClass=btn-block
# ----------------------------------------
# Feedback Icons (Validation / Alerts)
# ----------------------------------------
# Error icon (e.g., form error)
kcFeedbackErrorIcon=fa fa-exclamation-circle
# Warning icon
kcFeedbackWarningIcon=fa fa-exclamation-triangle
# Success icon (e.g., account created)
kcFeedbackSuccessIcon=fa fa-check-circle
# Informational icon
kcFeedbackInfoIcon=fa fa-info-circle
# ----------------------------------------
# Social Login Styling
# ----------------------------------------
# Container for the social login section
kcFormSocialAccountSectionClass=kc-social-section
# List container for social providers (e.g., Google, Facebook)
kcFormSocialAccountListClass=kc-social-list
# Individual link/button styling for a provider
kcFormSocialAccountListLinkClass=kc-social-link
template.ftl
It’s the base layout file used by Keycloak’s FreeMarker templates. It defines the overall HTML structure (like <html>, <head>, and <body>), handles shared elements (e.g., CSS includes, meta tags), and provides placeholders (<#nested>) where individual pages like login.ftl inject their content. This promotes consistency across all pages in a theme.
Here is the structure:
┌─────────────────────────────────────┐
│ template.ftl │
│ ┌─────────────────────────────┐ │
│ │ Header/Logo │ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ │ │
│ │ login.ftl │ │
│ │ ┌─────────────────┐ │ │
│ │ │ Username │ │ │
│ │ ├─────────────────┤ │ │
│ │ │ Password │ │ │
│ │ ├─────────────────┤ │ │
│ │ │ Login Button │ │ │
│ │ └─────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ Footer │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
login.ftl
It’s the FreeMarker template that defines the structure and layout of the Keycloak login page. It extends the base template.ftl and includes form elements like username, password inputs, a login button, and any login-related messages.
<#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','password') displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??; section>
<#if section = "header">
${msg("loginAccountTitle")}
<#elseif section = "form">
<div id="kc-form">
<div id="kc-form-wrapper">
<#if realm.password>
<form id="kc-form-login" onsubmit="login.disabled = true; return true;" action="${url.loginAction}" method="post">
<div class="form-group">
<label for="username" class="${properties.kcLabelClass!}">${msg("username")}</label>
<#if usernameEditDisabled??>
<input tabindex="1" id="username" class="${properties.kcInputClass!}" name="username" value="${(login.username!'')}" type="text" disabled />
<#else>
<input tabindex="1" id="username" class="${properties.kcInputClass!}" name="username" value="${(login.username!'')}" type="text" autofocus autocomplete="off"
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
/>
<#if messagesPerField.existsError('username','password')>
<span id="input-error" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}
</span>
</#if>
</#if>
</div>
<div class="form-group">
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label>
<input tabindex="2" id="password" class="${properties.kcInputClass!}" name="password" type="password" autocomplete="off"
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
/>
</div>
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
<div id="kc-form-options">
<#if realm.rememberMe && !usernameEditDisabled??>
<div class="checkbox">
<label>
<#if login.rememberMe??>
<input tabindex="3" id="rememberMe" name="rememberMe" type="checkbox" checked> ${msg("rememberMe")}
<#else>
<input tabindex="3" id="rememberMe" name="rememberMe" type="checkbox"> ${msg("rememberMe")}
</#if>
</label>
</div>
</#if>
</div>
<div class="forgot-password">
<#if realm.resetPasswordAllowed>
<a tabindex="5" href="${url.loginResetCredentialsUrl}">${msg("doForgotPassword")}</a>
</#if>
</div>
</div>
<div id="kc-form-buttons" class="${properties.kcFormGroupClass!}">
<input type="hidden" id="id-hidden-input" name="credentialId" <#if auth.selectedCredential?has_content>value="${auth.selectedCredential}"</#if>/>
<input tabindex="4" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/>
</div>
</form>
</#if>
</div>
</div>
<#elseif section = "info">
<#if realm.password && realm.registrationAllowed && !registrationDisabled??>
<div id="kc-registration-container">
<div id="kc-registration">
<span>${msg("noAccount")} <a tabindex="6" href="${url.registrationUrl}">${msg("doRegister")}</a></span>
</div>
</div>
</#if>
<#elseif section = "socialProviders">
<#if realm.password && social.providers??>
<div id="kc-social-providers" class="${properties.kcFormSocialAccountSectionClass!}">
<hr/>
<h4>${msg("identity-provider-login-label")}</h4>
<ul class="${properties.kcFormSocialAccountListClass!} <#if social.providers?size gt 4>${properties.kcFormSocialAccountDoubleListClass!}</#if>">
<#list social.providers as p>
<li class="${properties.kcFormSocialAccountListLinkClass!}">
<a href="${p.loginUrl}" id="zocial-${p.alias}" class="zocial ${p.providerId}">
<span>${p.displayName}</span>
</a>
</li>
</#list>
</ul>
</div>
</#if>
</#if>
</@layout.registrationLayout>
Deploying the Keycloak Theme
Themes can be deployed to Keycloak by copying the theme directory to themes or it can be deployed as an archive. During development, you can copy the theme to the themes directory, but in production, you may want to consider using an archive. An archive makes it simpler to have a versioned copy of the theme, especially when you have multiple instances of Keycloak, for example, with clustering.
Install the theme
Let’s build our “keycloak-custom-theme” Keycloak theme:
mvn install
This will generate a JAR file containing your custom theme inside the target/ directory.
Then, we’ll copy the keycloak-custom-theme.jar into the providers folder of our Keycloak installation:
cp target/keycloak-boot-theme-1.0.jar $KEYCLOAK_HOME/providers/
Finally, rebuild your configuration with the new Keycloak theme:
$KEYCLOAK_HOME/bin/kc.sh build
Enable the Custom Theme
We have now created a theme with support for the login type.
Update your Keycloak realm or realm JSON configuration:
- Log in to the Admin Console to check out the new theme
- Go to Realm Settings → Themes
- Set Login Theme to the theme name (e.g.,
iam-redesign) - Save changes

Our custom login theme should now appear when we open the Keycloak login page.



Conclusion
Well done !!. In this post, we walked through how to customize Keycloak themes using FreeMarker and Maven. We explored the theme structure, key files like theme.properties, and how to deploy your custom theme. With these tools and steps, you’re now ready to build and manage branded login experiences in Keycloak.
The complete source code is available on GitHub.
Support me through GitHub Sponsors.
Thank you for reading!! See you in the next post.