Dynamic UI Composition with Quarkus Qute: The Future of Java Frontends
Explore Qute’s dynamic include feature in Quarkus 3.26 and learn how to build modular, data-driven dashboards for modern enterprise apps.
Dynamic user interfaces are essential in enterprise systems where every user might see something different. Admins get control panels, managers get reports, and employees get tasks. Traditionally, Qute templates handled includes statically. But starting with Quarkus 3.26, you can now include templates dynamically at runtime using the new _id parameter.
In this hands-on tutorial, you’ll build a role-based dashboard system that renders different widgets for each user role.
Why You Should Care
In traditional Qute templates, an include is always static:
{#include userProfile /}That’s fine for fixed layouts, but enterprise apps often need flexibility. Dashboards, CMS-driven UIs, feature flags, or plugin systems. Dynamic includes let you do this:
{#include _id=templateVariable /}Here, _id tells Qute to evaluate the variable instead of treating it as a literal name. This small change unlocks a big design pattern: data-driven templates.
Prerequisites
Java 21+
Maven 3.9+
Quarkus 3.26+
Basic understanding of REST and Qute templating
Project Setup
Create a new Quarkus app with Qute and Quarkus REST:
mvn io.quarkus:quarkus-maven-plugin:create \
-DprojectGroupId=com.enterprise \
-DprojectArtifactId=qute-dynamic-dashboard \
-Dextensions="rest-qute,rest-jackson"
cd qute-dynamic-dashboardIf you want to, go grab the running example from my Github repository.
Model the Domain
We’ll use two simple model classes: User and DashboardConfig.
src/main/java/com/enterprise/model/User.java
package com.enterprise.model;
public class User {
private String username;
private String role;
private String email;
public User(String username, String role, String email) {
this.username = username;
this.role = role;
this.email = email;
}
public String getUsername() {
return username;
}
public String getRole() {
return role;
}
public String getEmail() {
return email;
}
}src/main/java/com/enterprise/model/DashboardConfig.java
package com.enterprise.model;
import java.util.List;
public class DashboardConfig {
private User user;
private List<String> widgets;
public DashboardConfig(User user, List<String> widgets) {
this.user = user;
this.widgets = widgets;
}
public User getUser() {
return user;
}
public List<String> getWidgets() {
return widgets;
}
}Add Business Logic
We’ll map each role to a list of widget templates.
src/main/java/com/enterprise/service/DashboardService.java
package com.enterprise.service;
import java.util.List;
import com.enterprise.model.DashboardConfig;
import com.enterprise.model.User;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class DashboardService {
public DashboardConfig getDashboardConfig(String username) {
User user = getUserByUsername(username);
List<String> widgets = getWidgetsForRole(user.getRole());
return new DashboardConfig(user, widgets);
}
private User getUserByUsername(String username) {
return switch (username) {
case “admin” -> new User(”admin”, “ADMIN”, “admin@company.com”);
case “manager” -> new User(”manager”, “MANAGER”, “manager@company.com”);
default -> new User(username, “EMPLOYEE”, username + “@company.com”);
};
}
private List<String> getWidgetsForRole(String role) {
return switch (role) {
case “ADMIN” -> List.of(
“widgets/admin-stats”,
“widgets/user-management”,
“widgets/system-health”,
“widgets/audit-log”);
case “MANAGER” -> List.of(
“widgets/team-performance”,
“widgets/approval-queue”,
“widgets/reports”);
default -> List.of(
“widgets/my-tasks”,
“widgets/timesheet”);
};
}
}Create the Widget Templates
Inside src/main/resources/templates/, create a widgets directory.
Add simple HTML fragments, for example:
widgets/admin-stats.html
<div class=”widget admin-stats”>
<h3>System Statistics</h3>
<div class=”stats-grid”>
<div class=”stat”><span class=”label”>Total Users</span><span class=”value”>1,247</span></div>
<div class=”stat”><span class=”label”>Active Sessions</span><span class=”value”>342</span></div>
<div class=”stat”><span class=”label”>System Load</span><span class=”value”>67%</span></div>
</div>
</div>You can add similar templates for managers (team-performance.html, approval-queue.html) and employees (my-tasks.html, timesheet.html).
The content doesn’t matter much. The point is that each file is an independent, reusable component. Grab some examples from my Github repository.
Create the Dashboard Template
Now we’ll create the main template that dynamically includes these widgets.
src/main/resources/templates/dashboard.html
<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8”>
<title>Dashboard - {config.user.role}</title>
<style>
<!-- see github -->
</style>
</head>
<body>
<div class=”header”>
<h1>Welcome, {config.user.username} ({config.user.role})</h1>
</div>
<div class=”container”>
<div class=”dashboard-grid”>
{#for widgetId in config.widgets}
{#include _id=widgetId /}
{/for}
</div>
</div>
</body>
</html>
That single line {#include _id=widgetId /} is what makes everything dynamic. Qute will render each widget based on the list provided by the backend service.
Expose the Dashboard via REST
src/main/java/com/enterprise/resource/DashboardResource.java
package com.enterprise.resource;
import com.enterprise.model.DashboardConfig;
import com.enterprise.service.DashboardService;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
@Path(”/dashboard”)
public class DashboardResource {
@Inject
Template dashboard;
@Inject
DashboardService dashboardService;
@GET
@Produces(MediaType.TEXT_HTML)
public TemplateInstance getDashboard(@QueryParam(”user”) String username) {
if (username == null || username.isEmpty()) {
username = “employee”;
}
DashboardConfig config = dashboardService.getDashboardConfig(username);
return dashboard.data(”config”, config);
}
}Run and Verify
Start the app in dev mode:
./mvnw quarkus:devThen open these URLs:
You’ll see different widget combinations rendered dynamically. No branching logic in the template, no duplication.
Understanding _id
Before Quarkus 3.26:
{#include widgetId /}would literally look for a template named widgetId.
Now, with _id, Qute evaluates the expression:
{#include _id=widgetId /}If widgetId equals “widgets/admin-stats”, Qute includes that file dynamically at runtime.
Advanced Uses
Conditional Widgets
Add conditional loading easily in your service layer:
if (role.equals(”ADMIN”)) widgets.add(”widgets/audit-log”);
if (featureEnabled(”new-analytics”)) widgets.add(”widgets/advanced-analytics”);Theming
{#include _id=”themes/” + tenantId + “/header” /}A/B Testing
{#include _id=experimentService.getVariant(’checkout’, user.id) /}Fallback Handling
{#let templateId = calculateTemplateId() /}
{#include _id=templateId /}
{#catch}
{#include widgets/default /}
{/catch}Best Practices
Organize templates hierarchically (
templates/widgets/admin,templates/widgets/employee, etc.)Validate input if template names come from external sources.
Cache template resolution if it involves complex logic.
Declare parameters at the top of widgets with
{@com.enterprise.model.User user}if you pass context.Use fallback templates to avoid rendering errors.
Performance Notes
Dynamic includes are safe for production:
Templates are compiled once and cached.
Expression evaluation adds negligible overhead.
Changes hot-reload in dev mode.
All templates are validated at build time in production builds.
Real-World Applications
Dynamic includes are perfect for:
CMS-driven sites with configurable layouts.
Multi-tenant systems needing branded templates.
Feature-flag rollouts and experiments.
Plugin-based architectures that load UI extensions dynamically.
White-label applications serving different clients with one codebase.
The Component Composition Engine
The _id parameter turns Qute into a true component composition engine.
Instead of hardcoding templates, you can drive UI composition from data. Whether it’s user roles, feature flags, or CMS configurations.
It’s a small feature that dramatically simplifies complex UIs.
Try it yourself: start with static includes, then switch to _id where flexibility matters most.
Further Reading





Looks nice!