Skip to content
Home Theme Gallery

Go templates

Home-UI is designed for a Go host that renders HTML with html/template. There is no component runtime to initialise and no build step on the host side. A page declares two files and is done:

<!doctype html>
<html lang="en" class="{{ .Theme }}">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="https://home-ui.harriverse.com/dist/latest/home-ui.css" />
<script type="module" src="https://home-ui.harriverse.com/dist/latest/home-ui.js"></script>
</head>
<body class="hui-surface">
{{ template "content" . }}
</body>
</html>

{{ .Theme }} is "light" or "dark", read from the cookie server-side so there is no flash of the wrong theme. hui-surface opts the page into the tokens; see Dark mode.

Those URLs load the current build from this site. For something you ship, download the files and serve them yourself - see Getting the files.

A component is configured entirely by attributes, so a Go template writes them the same way it writes any other tag:

<hui-field>
<span data-label>Project name</span>
<hui-input
data-control
name="project"
value="{{ .Project.Name }}"
{{ if .Errors.Project }}aria-invalid="true"{{ end }}></hui-input>
{{ with .Errors.Project }}
<span data-error>{{ . }}</span>
{{ end }}
</hui-field>

Two things are worth noticing. The control’s name and value are plain template data. And the invalid state is aria-invalid — there is no error attribute and no error property, because invalid is a state every control already understands. hui-field wires the label, description and error to the control for you.

Business validation is the server’s. A POST handler re-renders the form with the submitted values and the error text, and the same template above produces a control marked invalid and an error message that is wired to it. The client does not have to reconstruct that on its own.

type ProjectForm struct {
Name string
Errors map[string]string
}

An hx-post returning that partial is swapped in place; the controls are re-created and re-associated with the form. See HTMX forms and HTMX swaps.

Every form-associated control takes name, value and disabled:

<hui-combobox name="courseId" value="{{ .CourseID }}" label="Course"></hui-combobox>
<hui-date-picker name="raceDate" value="{{ .RaceDate }}"></hui-date-picker>
<hui-slider name="volume" min="0" max="100" value="{{ .Volume }}"></hui-slider>
<hui-checkbox name="agree" value="yes" {{ if .Agree }}checked{{ end }}></hui-checkbox>

hui-button type="submit" drives the ancestor form, so a template that wants the component’s styling can use it in place of <button type="submit">.

The package ships a Remix Icon sprite as dist/hui-icons.svg. Copy it beside the other two files and reference a symbol with <use>; no JavaScript is involved:

<svg width="16" height="16" aria-hidden="true">
<use href="/static/hui-icons.svg#ri-check-line"></use>
</svg>

Do not put endpoints, tokens or application state into a component’s attributes. There is no attribute for them, and that is deliberate:

<!-- rejected -->
<trainer-selector api="/api/trainers" auth-token="…"></trainer-selector>

The host owns fetching, routing, validation and persistence. A combobox does not search; it emits events and the host searches with HTMX. Keeping that boundary is what lets the same library serve a Go host and a static preview like this one.