Keycloak’s theming system lets you ditch the default look and feel, but the real magic is how it uses Freemarker templates to render everything, not just the login page.
Let’s build a custom login page.
First, we need to understand the structure. Keycloak themes are organized by type (login, account, admin, etc.) and then by theme name. Inside a theme, you’ll find theme.properties and a resources directory for static assets like CSS and images, and a templates directory for your Freemarker files.
Project Setup:
-
Create a theme directory:
mkdir -p /opt/keycloak/themes/mytheme/login/templates mkdir -p /opt/keycloak/themes/mytheme/login/resources/css mkdir -p /opt/keycloak/themes/mytheme/login/resources/img -
Create
theme.properties: This file tells Keycloak about your theme.# /opt/keycloak/themes/mytheme/login/theme.properties parent=keycloak import=login/keycloakparent=keycloakmeans we’re inheriting from the default Keycloak theme.import=login/keycloakspecifically imports the default login theme’s resources and templates, so we only have to override what we want to change. -
Create a custom CSS file:
/* /opt/keycloak/themes/mytheme/login/resources/css/custom.css */ body { background-color: #f0f0f0; font-family: 'Arial', sans-serif; } .kc-login-box { border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .btn-primary { background-color: #007bff; border-color: #007bff; } -
Create a custom login template: We’ll override the default
login.ftl.<#import "template.ftl" as layout> <@layout.registrationLayout displayMessage=!message.error?has_content> <#if message.type??> <div class="${kcSanitize(message.type)}"> <#if message.type == "success"><span class="${properties.kcSuccessIcon!}"></span></#if> <#if message.type == "warning"><span class="${properties.kcWarningIcon!}"></span></#if> <#if message.type == "error"><span class="${properties.kcErrorIcon!}"></span></#if> <#if message.type == "info"><span class="${properties.kcInfoIcon!}"></span></#if> ${message.summary?no_esc} </div> </#if> <#if section = "header"> <#nested "header"> </#if> <div id="kc-form"> <div id="kc-row-wrapper"> <div class="kc-login-box"> <h2 class="kc-title">${msg("loginTitle")}</h2> <form id="kc-form-login" action="${url.loginAction}" method="post"> <div class="kc-form-group"> <label for="username" class="kc-label">${msg("usernameHtml")}</label> <input type="text" id="username" name="username" value="${(login.username!'')?no_esc}" required autofocus autocomplete="off" /> </div> <div class="kc-form-group"> <label for="password" class="kc-label">${msg("passwordHtml")}</label> <input type="password" id="password" name="password" autocomplete="off" /> </div> <#if client.protocol == "openid-connect" && display.rememberMe??> <div class="kc-form-options"> <input id="rememberMe" name="rememberMe" type="checkbox" ${rememberMe?string('checked', '')} /> <label for="rememberMe">${msg("rememberMe")}</label> </div> </#if> <div class="kc-form-buttons"> <input class="btn btn-primary" type="submit" value="${msg("doLogIn")}" /> </div> </form> </div> </div> </div> </@layout.registrationLayout>Notice how we’re importing
template.ftl(which is part of thekeycloakparent theme) and using its@layout.registrationLayoutdirective. We then define our custom content within this layout, including the form elements and some basic styling classes. -
Place theme files: Copy your
mythemedirectory into Keycloak’s themes directory. The exact location depends on your installation, but it’s often something like/opt/keycloak/themes/. -
Configure Keycloak: You need to tell Keycloak to use your new theme. This is done via the Admin Console.
- Navigate to your Realm settings.
- Go to the "Themes" tab.
- Under "Login Theme," select "mytheme."
- Click "Save."
-
Restart Keycloak: For the theme changes to take effect, you’ll need to restart your Keycloak instance.
Now, when you navigate to your realm’s login page (e.g., http://localhost:8080/realms/myrealm/protocol/openid-connect/auth?client_id=myclient&redirect_uri=http://localhost:8080/myclient/callback&response_type=code&scope=openid), you should see your custom-styled login page.
The power of Freemarker here is that it’s not just about HTML. You have access to a rich set of variables provided by Keycloak, including url, message, client, and msg for internationalization. You can conditionally render elements, iterate over data, and even call Java methods if you extend Keycloak’s template extensions.
What most people miss is how deeply integrated the Freemarker rendering is. It’s not just a static HTML replacement; Keycloak dynamically injects context-specific data into these templates for every request, allowing for truly dynamic and interactive login experiences that can adapt to different authentication flows or user states without needing to rebuild the entire application.
The next step is usually exploring how to customize the account management console, which uses a similar theming approach but with different Freemarker templates and available variables.