Frontend Overview
HyperAdmin's frontend is built on three complementary technologies — HTMX, Alpine.js, and plain CSS — with everything rendered server-side by Jinja2. There are no JavaScript frameworks, no build pipelines for the UI, and no third-party widget libraries.
Design Philosophy
Server is the source of truth. Every piece of HTML — including form fields, error messages, paginated tables, and option lists — is rendered by the server. The browser receives HTML, not JSON. This keeps client-side logic minimal and the system auditable.
Progressive enhancement. Forms and links work without JavaScript. HTMX layers partial-page updates on top. Alpine.js handles the handful of interactions that are purely local (dropdown toggles, toast dismiss timers).
Atomic CSS, no build step. Styling uses modular CSS partials imported via @layer from a single entry file (hyperadmin.css). CSS custom properties provide the design token layer. There is no Tailwind, Bootstrap, or Sass compilation. Deployment is uv run fastapi dev and nothing else.
Stack
| Layer | Technology | Responsibility |
|---|---|---|
| Templates | Jinja2 | Server-side HTML rendering |
| Server interactions | HTMX 1.9 | Partial-page updates, form submission, search, pagination |
| Client-side behaviour | Alpine.js 3 | Dropdowns, modals, focus traps |
| Styling | Custom CSS (ha-*) |
Design tokens, layout, components |
| Forms | Pydantic v2 | Validation, error collection |
Template Hierarchy
Templates use three-level inheritance:
_base.html ← HTML shell: head, HTMX/Alpine scripts, CSS link
├── list_layout.html ← list page frame (navbar, sidebar, search bar, table)
│ └── (rendered by list view per model)
├── form_layout.html ← form page frame (card wrapper, default form_body block)
│ ├── create.html ← sets hx-post, "Create" button
│ └── update.html ← sets hx-put, "Update" button
└── detail_layout.html ← read-only field display
└── detail.html
Partials are included into _base.html:
_navbar.html— top navigation with Alpine.js user menu dropdown_sidebar.html— left nav with registered model links_messages.html— toast container (Alpine.js, auto-dismiss after 5 s)
Components are reusable snippets called from layout templates:
components/table.html— data table with HTMX sortable headerscomponents/search_input.html— search-as-you-type with HTMXcomponents/pagination.html— page controls with HTMX linkscomponents/filter_bar.html— filter dropdown bar with HTMXcomponents/sortable_header.html— sortable column header with HTMXcomponents/fieldset.html— collapsible fieldset group with Alpine.jscomponents/_macros.html— central macro library (see below)
Macro Library (components/_macros.html)
The _macros.html file provides reusable Jinja2 macros that eliminate boilerplate across widget templates:
field_wrapper(field)— wraps any form field with the standardha-form-groupdiv, label, help text, and error list. Used via the Jinja2{% raw %}{% call %}{% endraw %}pattern:
{% raw %}
{%- from "components/_macros.html" import field_wrapper -%}
{%- call field_wrapper(field) -%}
<input id="{{ field.name }}" name="{{ field.name }}" type="text"
value="{{ field.value or '' }}" class="ha-input" />
{%- endcall -%}
All widget templates (except checkbox_input.html which has a different label pattern) use this macro.
CSS Architecture
File Structure
CSS is organized as modular partials imported from a single entry file using @import with @layer for explicit cascade control. Browser requirement: Chrome 99+, Firefox 97+, Safari 15.4+.
src/hyperadmin/static/css/
hyperadmin.css → entry file (@layer declaration + @import only)
_tokens.css → :root design tokens (colors, spacing, typography, shadows)
_dark-mode.css → dark mode token overrides (three-way: class/data-attr/OS)
_reset.css → base reset (box-sizing, body defaults)
_layout.css → page, container, layout, content, toolbar
_navbar.css → navbar + dropdown
_sidebar.css → sidebar navigation
_page-header.css → page/section titles
_card.css → card component
_table.css → data table + sort + row actions
_pagination.css → pagination controls
_search.css → search input
_forms.css → form fields (input, select, textarea, checkbox, errors)
_buttons.css → buttons + variants + form actions bar
_alerts.css → alerts + toasts
_login.css → login page
_filter.css → filter bar
_fieldsets.css → fieldset groups + form grid layouts
_theme-toggle.css → theme toggle button + icon visibility
_utilities.css → links, icons, rotation transforms, HTMX indicator
_accessibility.css → skip-link, focus, touch targets, reduced motion, high contrast
_responsive.css → mobile/tablet media queries
CSS Naming Convention
All classes use the ha- prefix to avoid conflicts with user-provided CSS:
- Block:
ha-{block}— e.g.,ha-table,ha-btn,ha-sidebar - Element:
ha-{block}-{element}— e.g.,ha-table-th,ha-sidebar-link - Modifier:
ha-{block}--{modifier}— e.g.,ha-select--multi,ha-rotate--180 - Utility:
ha-{utility}— e.g.,ha-text-muted,ha-text-link
Design Tokens
Tokens are CSS custom properties on :root. Override them to theme HyperAdmin without touching markup:
:root {
/* Colours */
--ha-color-primary: #2563eb;
--ha-color-primary-hover: #1d4ed8;
--ha-color-primary-light: #dbeafe;
--ha-color-danger: #dc2626;
--ha-color-success: #16a34a;
--ha-color-warning: #ca8a04;
--ha-color-text: #374151;
--ha-color-text-muted: #6b7280;
--ha-color-text-strong: #111827;
--ha-color-bg: #f3f4f6;
--ha-color-surface: #ffffff;
--ha-color-border: #d1d5db;
/* Typography */
--ha-font-family: 'Inter', system-ui, -apple-system, sans-serif;
--ha-font-size-sm: 0.875rem;
--ha-font-size-base: 1rem;
--ha-font-size-lg: 1.125rem;
/* Shape */
--ha-radius: 0.375rem;
--ha-radius-lg: 0.5rem;
/* Layout */
--ha-sidebar-width: 16rem;
/* Motion */
--ha-transition: 150ms ease-in-out;
}
Dark Mode
Dark mode uses a three-way system:
- Explicit class:
.ha-theme-darkon<html> - Data attribute:
[data-theme="dark"]on<html> - OS preference:
@media (prefers-color-scheme: dark)with.ha-theme-lightguard
Dark values are defined once as --_dark-* private tokens, then mapped to semantic tokens in both the explicit and OS-preference selectors. This eliminates value duplication.
Class Catalogue
Layout
| Class | Element | Purpose |
|---|---|---|
ha-page |
<body> |
Background colour, font, min-height |
ha-container |
<div> |
Max-width wrapper (1280 px), centred |
ha-layout |
<div> |
Flex row: sidebar + content |
ha-content |
<main> |
Flex-grow content area |
ha-list-toolbar |
<div> |
Spacing wrapper for create button |
ha-detail-container |
<div> |
Detail view wrapper |
ha-detail-fields |
<div> |
Detail fields container |
Navigation
| Class | Element | Purpose |
|---|---|---|
ha-navbar |
<nav> |
Fixed-height top bar with shadow |
ha-navbar-brand |
<a> |
Logo / brand link |
ha-navbar-actions |
<div> |
Flex container for theme toggle + user menu |
ha-navbar-user |
<div> |
User menu container (Alpine.js) |
ha-dropdown |
<div> |
Absolutely-positioned dropdown panel |
ha-dropdown-item |
<a> |
Menu item with hover state |
ha-sidebar |
<aside> |
Fixed-width sidebar |
ha-sidebar-nav |
<ul> |
Navigation list |
ha-sidebar-link |
<a> |
Nav link with active/hover state |
Forms
| Class | Element | Purpose |
|---|---|---|
ha-form-group |
<div> |
Field wrapper (margin, stacking) |
ha-label |
<label> |
Field label (bold, block) |
ha-required |
<span> |
Red asterisk for required fields |
ha-input |
<input> |
Text / number / date / email inputs |
ha-select |
<select> |
Select dropdown |
ha-select--multi |
<select> |
Multi-select height override |
ha-select--relation |
<select> |
Semantic hook for relation selects |
ha-textarea |
<textarea> |
Multi-line text |
ha-checkbox |
<input[type=checkbox]> |
Checkbox (accent-color: primary) |
ha-checkbox-label |
<label> |
Flex wrapper for checkbox + text |
ha-checkbox-text |
<span> |
Label text beside checkbox |
ha-help-text |
<p> |
Helper text below field (muted) |
ha-field-errors |
<ul> |
Validation error list (italic, danger) |
ha-field-value |
<div> |
Read-only field value in detail view |
Buttons
| Class | Modifier | Appearance |
|---|---|---|
ha-btn |
— | Base: padding, bold, border-radius, transition |
ha-btn |
ha-btn-primary |
Primary background, white text |
ha-btn |
ha-btn-danger |
Red background, white text |
ha-btn |
ha-btn-secondary |
Outline button |
ha-btn |
ha-btn-action |
Action button (detail view) |
ha-btn |
ha-btn-ghost |
Transparent background |
ha-btn |
ha-btn-link |
No background, coloured text |
ha-action-buttons |
<div> |
Flex container for action buttons |
Tables
| Class | Element | Purpose |
|---|---|---|
ha-table-wrapper |
<div> |
Card shell with shadow |
ha-table |
<table> |
Full-width, border-collapse |
ha-table-header |
<thead> |
Muted background |
ha-table-th |
<th> |
Header cell padding, uppercase label |
ha-table-row |
<tr> |
Row with hover highlight |
ha-table-cell |
<td> |
Cell padding |
ha-table-sort-link |
<a> |
Sort toggle link in header |
ha-action-link |
<a> |
Inline action links (View, Edit) |
ha-action-delete |
<button> |
Inline delete button |
Feedback
| Class | Purpose |
|---|---|
ha-alert |
Base alert (padding, border, radius) |
ha-alert-info / -success / -warning / -error |
Colour variants |
ha-toast-container |
Fixed top-right toast stack |
ha-toast |
Individual toast message |
htmx-indicator |
Hidden by default; shown during .htmx-request |
Utilities
| Class | Purpose |
|---|---|
ha-text-link |
Styled link with hover transition |
ha-text-muted |
Muted text colour |
ha-icon / ha-icon-sm |
SVG icon sizing |
ha-rotate--0 / ha-rotate--180 |
Rotation transforms with transition |
HTMX Integration
HyperAdmin uses HTMX for every server round-trip that should not trigger a full page reload.
How 422 Validation Errors Work
By default HTMX does not swap content for 4xx responses. _base.html includes a small event listener that opts 422 back in:
document.addEventListener('htmx:beforeSwap', function (evt) {
if (evt.detail.xhr && evt.detail.xhr.status === 422) {
evt.detail.shouldSwap = true;
evt.detail.isError = false;
}
});
The server re-renders the form partial with inline errors and returns it with status=422. HTMX swaps it in place of the current form.
Keyboard Shortcuts
_base.html also registers two search focus shortcuts: Cmd/Ctrl+K and /.
Alpine.js Usage
Alpine.js is used sparingly, only for interactions that require no server round-trip:
| Location | Usage |
|---|---|
_navbar.html |
User dropdown toggle (x-data, @click, @click.away) |
_messages.html |
Toast auto-dismiss after 5 s (x-data, setTimeout) |
components/filter_bar.html |
Filter collapse/expand toggle |
components/fieldset.html |
Collapsible fieldset groups |
Server events (via HX-Trigger response headers) can bridge HTMX and Alpine. See htmx_patterns.md for the pattern.