Layout Templates
A theme sets colors, the logo, and other visual options. A layout template changes page structure. A theme can use one layout template (chosen on the theme's Advanced tab), and several themes can share the same template. With no template assigned, the panel uses its built-in layout.
Creating a template
- Go to Settings → Layout Templates and create a template.
- Add one or more surfaces (for example
LoginPageorShell) and edit each surface's markup and CSS. Use Validate / Preview to check a surface as you work. - Save the template, then open a theme's Advanced tab and choose it. Select that theme to see it in action.
Add ?safeMode=1 to the panel's address to temporarily ignore the template's markup and CSS and fall back to the built-in layout — handy if a custom login or shell ever renders incorrectly.
How a surface is written
A template is organized into surfaces — named regions you can take over (the login page, the whole shell, individual parts of the shell, or the pages of a game or Docker server). In a surface you write ordinary HTML with three special tags:
{{ Variable }}— inserts a value, such as the application name or the signed-in user.{% component "name" %}— drops in a live, interactive piece of the panel (the login form, the navigation menu, the dark-mode toggle, and so on).{% surface "Key" %}— places one of the customizable regions inside your shell.
Each surface also has its own CSS box, and the template has a base stylesheet that applies whenever the template is active. The editor's Reference tab lists every available variable, component, and surface key.
Templates imported from others run in a safe mode that strips scripts, so a shared template can't run code. Some advanced CSS (for example blur or animations) is also filtered unless an administrator marks the theme as trusted.
Variables
Insert a variable with {{ AppName }}, or the dotted form for nested values such as {{ Branding.Title }}. Output is automatically HTML-encoded.
| Variable | Type | Description |
|---|---|---|
AppName | string | The application name. |
AppVersion | string | The running version. |
Culture | string | The current language code. |
IsAuthenticated | boolean | Whether the visitor is signed in. |
UserName | string | The signed-in user's name. |
Branding.Title | string | The branding title. |
Branding.Subtitle | string | The branding subtitle. |
Branding.LogoUrl | string | The branding logo image URL. |
Components
Add a component with {% component "name" %}. The most useful ones:
| Component | What it adds |
|---|---|
login-form | The sign-in form (required somewhere on the login page). |
brand | The logo and branding block. |
shell | The entire built-in shell — a quick base when you only want to restyle with CSS. |
body | The current page's content (required somewhere in a custom shell). |
menu | The navigation drawer and menu. |
menu-toggle | The button that opens and collapses the drawer. |
topbar | The complete built-in top bar. |
breadcrumbs | The breadcrumb trail. |
dark-mode-toggle | The light / dark switch. |
language-selector | The language picker. |
connection-status | A warning shown only when the live connection drops. |
connection-indicator | An always-on connection dot (green / amber / red). |
health-indicator | A system-health indicator (shown only to permitted users). |
announcements | The customer announcements control. |
footer-resources | The current page's live stats — a game or Docker server's disk, CPU, memory, network and players. |
The surfaces
Login page
The LoginPage surface replaces the entire login page. It must include the login-form component, or no one could sign in.
- HTML
- CSS
<div class="login-glass-card">
<h2>🎨 My Custom Login Template</h2>
<p>{{ Branding.Title }} — v{{ AppVersion }}</p>
{% component "brand" %}
{% component "login-form" %}
{% component "language-selector" %}
</div>
.login-glass-card {
text-align: center;
}
.login-glass-card h2 {
color: var(--mud-palette-primary);
}
App shell
The Shell surface replaces the shell that wraps every page. It must include either the body component (the page content) or the shell component (the entire built-in shell). The example below rebuilds the shell from individual pieces — a custom top bar, the menu, the page body, and the footer region — and places the top-bar and footer surfaces inside it.
When you hand-build the shell like this, one CSS rule is required so the navigation drawer sits correctly below your top bar — it's the first rule in the CSS tab below.
- HTML
- CSS
<div class="tca-shell-header">
{% component "menu-toggle" %}{% surface "Shell.TopBarStart" %}
<span class="tca-shell-title">{{ AppName }}</span>
{% component "breadcrumbs" %}
<span class="tca-shell-spacer"></span>
{% component "connection-status" %}{% component "dark-mode-toggle" %}{% surface "Shell.TopBarEnd" %}
</div>
{% component "menu" %}
<div class="tca-shell-body">
{% component "body" %}
</div>
{% surface "Shell.Footer" %}
/* Required: keeps the navigation drawer below the 64px top bar */
.mud-drawer.mud-drawer-fixed.mud-drawer-pos-left {
top: 64px !important;
bottom: 0 !important;
height: auto !important;
}
.tca-shell-header {
position: fixed;
top: 0; left: 0; right: 0;
height: 64px;
z-index: 1200;
display: flex;
align-items: center;
gap: 12px;
padding: 0 16px;
background: linear-gradient(90deg, #1a1f3a, #2d1b4e);
color: #fff;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
}
.tca-shell-title { font-weight: 800; letter-spacing: 0.5px; }
.tca-shell-spacer { flex: 1; }
.tca-shell-body { margin-top: 64px; margin-bottom: 40px; padding: 24px; }
If you only want to recolor or restyle the panel without rebuilding it, make your Shell simply {% component "shell" %} and put everything in the stylesheet — you won't need the drawer rule above.
And if you don't need to change the shell's markup or CSS at all, just disable the Shell surface (or leave it out) — the built-in shell is used, and the other surfaces (top bar, drawer, footer and content slots) still appear on their own.
Top bar slots
The Shell.TopBarStart and Shell.TopBarEnd surfaces add extra content at the start (left) or end (right) of the top app bar, alongside the built-in toggles. Both appear automatically in the built-in top bar; in a hand-built Shell you position them with {% surface "Shell.TopBarStart" %} / {% surface "Shell.TopBarEnd" %}, as shown in the shell example above.
- HTML
- CSS
<!-- Shell.TopBarStart -->
<div class="tca-topbar-start">
<span class="tca-topbar-start-icon">⚡</span>
<span>Status: Online</span>
</div>
<!-- Shell.TopBarEnd -->
<div class="tca-topbar-end">
<span class="tca-topbar-end-icon">★</span>
<span>Pro Plan</span>
</div>
.tca-topbar-start {
display: inline-flex;
align-items: center;
gap: 6px;
margin-left: 10px;
padding: 3px 12px;
border-radius: 999px;
background: linear-gradient(135deg, #7c4dff 0%, #00b0ff 100%);
color: #fff;
font-weight: 700;
font-size: 0.78rem;
}
.tca-topbar-end {
display: inline-flex;
align-items: center;
gap: 6px;
margin-right: 8px;
padding: 3px 12px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.55);
color: #fff;
font-weight: 600;
font-size: 0.78rem;
}
Drawer header and footer
Shell.DrawerHeader and Shell.DrawerFooter add content above or below the navigation menu inside the drawer — for example a brand zone at the top, or an account and sign-out block at the bottom.
- HTML
- CSS
<!-- Shell.DrawerHeader -->
<div class="tca-drawer-header">
<div class="tca-drawer-header-logo">⚡</div>
<div>
<div class="tca-drawer-header-name">{{ AppName }}</div>
<div class="tca-drawer-header-sub">{{ Branding.Subtitle }}</div>
</div>
</div>
<!-- Shell.DrawerFooter -->
<div class="tca-drawer-footer">
<span class="tca-drawer-footer-dot"></span>
<span class="tca-drawer-footer-text">Signed in as <strong>{{ UserName }}</strong></span>
</div>
.tca-drawer-header {
display: flex;
align-items: center;
gap: 10px;
padding: 16px;
background: linear-gradient(135deg, #1a1f3a, #2d1b4e);
color: #fff;
}
.tca-drawer-header-name { font-weight: 800; letter-spacing: 0.5px; }
.tca-drawer-header-sub { font-size: 0.72rem; opacity: 0.7; text-transform: uppercase; }
.tca-drawer-footer {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border-top: 1px solid rgba(124, 77, 255, 0.35);
font-size: 0.8rem;
}
.tca-drawer-footer-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: #4caf50;
box-shadow: 0 0 6px #4caf50;
}
Before and after content
Shell.BeforeContent and Shell.AfterContent are banner strips shown immediately above or below the page content, on every page.
- HTML
- CSS
<!-- Shell.BeforeContent -->
<div class="tca-before-content">
<span class="tca-before-content-icon">ℹ</span>
<span>Scheduled maintenance this Sunday at 02:00 UTC.</span>
</div>
<!-- Shell.AfterContent -->
<div class="tca-after-content">
© {{ AppName }} — all rights reserved.
</div>
.tca-before-content {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
padding: 10px 16px;
border-radius: 10px;
background: linear-gradient(90deg, #536dfe, #7c4dff);
color: #fff;
font-size: 0.9rem;
}
.tca-after-content {
margin-top: 16px;
padding: 10px 16px;
border-radius: 10px;
background: rgba(124, 77, 255, 0.12);
border: 1px dashed rgba(124, 77, 255, 0.5);
font-size: 0.82rem;
text-align: center;
}
Footer
The Shell.Footer surface replaces the contents of the footer bar. If you don't define it, the built-in footer is shown.
- HTML
- CSS
<div class="tcf">
{% component "health-indicator" %}
<a class="tcf-link" href="/support">Support</a>
<a class="tcf-link" href="https://status.example.com" target="_blank" rel="noopener">Status</a>
<span class="tcf-grow"></span>
<span class="tcf-brand">© 2026 {{ Branding.Title }}</span>
<span class="tcf-ver">v{{ AppVersion }}</span>
{% component "connection-indicator" %}
</div>
.tcf {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding-inline: 12px;
font-size: 0.78rem;
}
.tcf-grow { flex: 1; }
.tcf-brand { font-weight: 600; }
.tcf-ver { opacity: 0.6; }
.tcf-link { color: inherit; text-decoration: none; opacity: 0.85; }
.tcf-link:hover { opacity: 1; text-decoration: underline; }
Add {% component "footer-resources" %} to a custom footer to show the current page's live stats — a game or Docker server's disk, CPU, memory, network and players.
Menu links
The Shell.MenuLinks surface adds your own links to the end of the navigation menu, styled to match the built-in items. To look native, each link uses the same markup as the built-in items: an <a class="mud-nav-link"> containing a Font Awesome icon and a text label.
<a href="https://docs.tcadmin.com" target="_blank" rel="noopener noreferrer" class="mud-nav-link mud-ripple">
<i class="fa-solid fa-book mud-icon-root mud-nav-link-icon mud-nav-link-icon-default" style="font-size:24px;width:24px;text-align:center;"></i>
<div class="mud-nav-link-text">Documentation</div>
</a>
<a href="https://help.tcadmin.com" target="_blank" rel="noopener noreferrer" class="mud-nav-link mud-ripple">
<i class="fa-solid fa-life-ring mud-icon-root mud-nav-link-icon mud-nav-link-icon-default" style="font-size:24px;width:24px;text-align:center;"></i>
<div class="mud-nav-link-text">Support</div>
</a>
Use Font Awesome icons (<i class="fa-solid ...">). Keep the mud-icon-root class so the label collapses to an icon-only button when the drawer is minimized.
Service pages
Layout templates can also restyle the pages of an individual game or Docker server. Four surfaces cover them:
| Surface | What it replaces |
|---|---|
GameService.Home | A game server's home page — the content you see when you open the service. |
DockerService.Home | A Docker server's home page. |
GameService.Header | A game server's header strip — the title, connection cards and power buttons shown on every page of the service. |
DockerService.Header | A Docker server's header strip, on every page of the service. |
A Home surface replaces only the page's content — the panel's top bar, menu and bottom bar stay. A Header surface replaces the band that stays visible while you move between a service's pages (Home, Console, File Manager and so on). Voice servers use the GameService surfaces. These surfaces appear on the service's pages automatically — they are never placed inside another surface with {% surface %} — and the editor's Validate / Preview shows them with a sample service and every permission granted.
If a template doesn't define a service surface, the built-in page is shown — an unfinished template never leaves a service unusable.
A defined surface always wins — so to remove the header strip entirely, add a Header surface containing just an empty <div></div>. It renders nothing, and the built-in header stays hidden.
Targeting one game or blueprint
On their own, the service surfaces apply to every game or Docker server. To design a page for one specific game or Docker blueprint, add its numeric id to the end of the surface key — GameService.Home.526 applies only to servers of game 526 and wins over the general GameService.Home. When you add a surface, the editor's Limit to blueprint picker composes the key for you (typing the suffix by hand works too). If the specific surface is disabled, those servers fall back to the general one.
Service variables
Unlike the other surfaces, a service surface is rendered for the service and the person viewing it each time the page opens. Three extra variable groups are available alongside the standard ones:
Service.*— details of the service being viewed.Permissions.*— whether the viewer can see each feature of this service, matching the built-in page's rules exactly.Features.*— which features the game or blueprint has switched on at all, regardless of the viewer's permissions.
Use them like any other variable — {{ Service.Name }}, or in conditions and loops:
{% if Permissions.Console %}<div class="my-console">{% component "game-console" %}</div>{% endif %}
{% for p in Service.Ports %}{{ p.Name }}: {{ p.Port }}<br>{% endfor %}
Variable values are captured when the page loads. For numbers that update in real time — status, players, CPU, memory — place the service components instead.
| Variable | Type | Description |
|---|---|---|
Service.ServiceId | number | The service's id. |
Service.Name | string | The service's name. |
Service.ServiceType | string | GameService (also voice servers) or DockerService. |
Service.BlueprintId | number | The game or Docker blueprint id. |
Service.BlueprintName | string | The game or Docker blueprint name. |
Service.IpAddress | string | The service's IP address. |
Service.Hostname | string | The service's hostname, when one is set. |
Service.HostnameOrIp | string | The hostname if set, otherwise the IP address. |
Service.Url | string | The relative address of the service's home page — handy for links to sub-pages, e.g. {{ Service.Url }}/FileManager. |
Service.Status | string | The status name when the page loaded (for example Started, Stopped). |
Service.OwnerName | string | The name of the user who owns the service. |
Service.DiskSpace | number | The disk quota in bytes (empty when unlimited). |
Service.DiskUsage | number | The last known disk usage in bytes. |
Service.Slots | number | Player slots (game servers). |
Service.GamePort | number | The game port (game servers). |
Service.QueryPort | number | The query port (game servers). |
Service.RconPort | number | The RCON port (game servers). |
Service.VirtualServerName | string | The virtual server hosting the service, when there is one. |
Service.CpuLimit | number | The CPU limit, as a percentage. |
Service.MemoryLimit | number | The memory limit in bytes. |
Service.Image | string | The container image (Docker servers). |
Service.Tag | string | The image tag (Docker servers). |
Service.Ports | list | Every named port — loop with {% for p in Service.Ports %}. |
Each entry in Service.Ports has:
| Variable | Type | Description |
|---|---|---|
Name | string | The port's name (for example GamePort). |
Port | number | The port number. |
Description | string | The port's description, when one is set. |
Uri | string | A click-to-connect link when the game defines one (for example steam://connect/…). |
The Permissions.* values mirror what the built-in pages would show this viewer for this service — the stat values already include LiveStats, and Console and FastDL already include the matching feature toggle, so each one can be tested on its own. They control visibility only; every action is still checked when it's used.
| Variable | Type | Description |
|---|---|---|
Permissions.Control | boolean | Start, stop and restart the service. |
Permissions.KillService | boolean | Force-stop the service. |
Permissions.Console | boolean | Use the console (game) or the interactive terminal (Docker). |
Permissions.Logs | boolean | View the service's log files. |
Permissions.ServiceActivity | boolean | View the service's activity history. |
Permissions.LiveStats | boolean | See live usage statistics. |
Permissions.PlayerStats | boolean | See the players tile. |
Permissions.CpuStats | boolean | See the CPU tile. |
Permissions.MemoryStats | boolean | See the memory tile. |
Permissions.NetworkStats | boolean | See the network tile. |
Permissions.ServiceSettings | boolean | Open the service's settings. |
Permissions.CustomScripts | boolean | Manage the service's custom scripts. |
Permissions.Reinstall | boolean | Reinstall the service. |
Permissions.Delete | boolean | Delete the service. |
Permissions.Mods | boolean | Install mods. |
Permissions.FastDL | boolean | Manage Fast Downloads. |
Permissions.LogConsole | boolean | View the container log (Docker servers; always false for game servers). |
Permissions.FileManager | boolean | Use the File Manager. |
Permissions.ConfigFiles | boolean | Edit configuration files. |
Permissions.ScheduledTasks | boolean | Manage scheduled tasks. |
Permissions.Backups | boolean | Manage backups. |
Permissions.Ftp | boolean | See FTP connection details. |
The Features.* values tell you what the game or blueprint has enabled at all — useful to tell "this viewer can't see it" apart from "this service doesn't have it". All are booleans: Features.ConsoleEnabled, Features.FileManagerEnabled, Features.ConfigFilesEnabled, Features.BackupsEnabled, Features.ScheduledTasksEnabled, Features.UpdatesEnabled, Features.SteamUpdateEnabled and Features.FastDLEnabled.
Service components
These components drop live, interactive pieces of the service pages into your surface. Each one shows and hides itself using the same rules as the built-in page — a viewer without the right permission simply sees nothing — so wrap one in {% if Permissions.… %} only when your surrounding markup should disappear too. The service-… components work on both service types; game-… and docker-… components render only for their own type. Outside a service's pages (for example in a Shell surface) they render nothing.
| Component | What it adds |
|---|---|
service-title | The service's name, icon and address line (updates live). |
service-power | The Start / Stop / Restart / Kill buttons. |
service-advanced-actions | The extra action buttons — settings, scripts, reinstall, delete, move. |
service-details | The service details table. |
service-uptime-tile | The uptime tile. |
service-cpu-tile | The live CPU tile. |
service-memory-tile | The live memory tile. |
service-disk-tile | The disk usage tile. |
service-recent-logs | The recent log files panel. |
service-recent-activity | The recent activity timeline. |
game-status-alert | The warning shown while a game server's status is unknown. |
game-connection-info | The connection, FTPS and RCON info cards. |
game-players-tile | The live players tile. |
game-network-tile | The live network traffic tile. |
game-console | The game's web console (text or screen capture). |
docker-status-header | The container status header with uptime, ports and quick settings. |
docker-ports | The IP and port chips. |
docker-logs-terminal | The read-only container log terminal. |
docker-exec-terminal | The interactive container terminal. |
A console or terminal placed in a surface connects as soon as the page opens (the built-in pages wait until you open their tab).
In your surface CSS, always give game-console and the Docker terminals a fixed-height container (for example height: 480px; min-width: 0; overflow: hidden;), and use minmax(0, …) for any CSS grid tracks around them. In an auto-sized cell, the terminal and the cell resize each other in a loop and the browser tab can freeze.
Media queries are filtered out of shared templates' CSS, but the panel's built-in helper classes still work: add class="d-none d-md-flex" to an element to hide it on phones (it appears from medium screens up).
Example: a game home page
Saved as GameService.Home — a hero strip with the power buttons, a row of live tiles, the console, and the standard info blocks:
- HTML
- CSS
<div class="svc-hero">
<div>
<h2>{{ Service.Name }}</h2>
<p>{{ Service.BlueprintName }} — {{ Service.HostnameOrIp }}{% if Service.GamePort %}:{{ Service.GamePort }}{% endif %}</p>
</div>
<span class="svc-hero-spacer"></span>
{% component "service-power" %}
</div>
{% component "game-status-alert" %}
<div class="svc-tiles">
{% component "service-uptime-tile" %}
{% component "game-players-tile" %}
{% component "service-cpu-tile" %}
{% component "service-memory-tile" %}
</div>
{% if Permissions.Console %}
<div class="svc-console">{% component "game-console" %}</div>
{% endif %}
{% component "game-connection-info" %}
{% component "service-details" %}
.svc-hero {
display: flex;
align-items: center;
gap: 16px;
padding: 20px 24px;
border-radius: 14px;
background: linear-gradient(120deg, #1a1f3a, #2d1b4e);
color: #fff;
}
.svc-hero h2 { margin: 0; }
.svc-hero p { margin: 4px 0 0; opacity: 0.75; }
.svc-hero-spacer { flex: 1; }
.svc-tiles {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
margin: 16px 0;
}
/* Required: consoles and terminals need a fixed height */
.svc-console {
height: 480px;
min-width: 0;
overflow: hidden;
margin-bottom: 16px;
}
Example: a service header
Saved as GameService.Header — replaces the title / connection / power band on every page of the service. The connection cards are hidden on phones with the d-none d-md-flex helper classes:
- HTML
- CSS
<div class="svc-head">
{% component "service-title" %}
<span class="svc-head-spacer"></span>
<div class="d-none d-md-flex">{% component "game-connection-info" %}</div>
{% component "service-power" %}
</div>
.svc-head {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
border-radius: 12px;
background: rgba(124, 77, 255, 0.12);
}
.svc-head-spacer { flex: 1; }
Sharing templates
A finished template can move between installations:
- Export / Import — from a template's editor, Export downloads it as a file; Import on the Layout Templates list loads one back in.
- Plugin Repository — Share publishes a template to a connected plugin repository, where other installations can browse and install it. You can also share a theme together with its template as a single bundle, so the colors and the layout travel together.