Slots

GoHT composes templates with named slots using @slot, @ifslot, @noslot, and @eachslot.

  • @slot renders named slot content, optionally with default content.
  • @ifslot renders its body only when the caller assigned that slot.
  • @noslot renders its body only when the caller did not assign that slot.
  • @eachslot binds each template assigned to a slot for iteration.

These directives work in Haml, Slim, and EGO templates. EGO uses command tags such as <%@slot ... %>.

For the single built-in children region and @render/@children composition, see Render and Children.

Named slots

Named slots are useful for layouts that have several independent regions. A slot renders content that the caller passes with a generated With<Slot> method.

@haml DashboardLayout(title string) {
	!!!
	%html
		%head
			%title= title
		%body
			%header
				=@slot header
					%h1= title
			.layout
				%aside
					=@slot sidebar
				%main
					=@slot main
						%p Choose an item to get started.
			.notifications
				=@slot notifications
			%footer
				=@slot footer
					%small Copyright 2026
}

The header, main, and footer slots above include default content. GoHT renders default slot content only when the caller does not provide content for that slot. The sidebar and notifications slots render nothing unless the caller provides matching slotted templates.

Slim slot declarations follow the same shape:

@slim DashboardLayoutSlim(title string) {
	header
		= @slot header
			h1= title
	main
		= @slot main
			p Choose an item to get started.
}

EGO slots use <%@slot ... %> or a block form when they have defaults:

@ego DashboardLayoutEgo(title string) {
	<header>
		<%@slot header { %>
			<h1><%= title %></h1>
		<% } %>
	</header>
	<main>
		<%@slot main %>
	</main>
}

Passing slot content

For each @slot a template declares, GoHT generates a fluent With<Slot> method on that template’s generated type. Call it with the template to render in that slot, then render the completed result:

page := DashboardLayout("Dashboard").
	WithHeader(Header(user)).
	WithSidebar(Sidebar(navItems)).
	WithMain(DashboardHome(stats)).
	WithNotifications(Notifications(alerts)).
	WithFooter(Footer())

if err := page.Render(ctx, w); err != nil {
	return err
}

Render now only takes context.Context and io.Writer; slot content is assigned beforehand with With<Slot>, not passed as extra arguments to Render.

Each With<Slot> call returns a new template value rather than mutating the receiver. This lets you prepare a common starting point and reuse it for several pages:

base := DashboardLayout("Dashboard").WithHeader(Header(user))

overview := base.WithMain(DashboardHome(stats))
archive := base.WithMain(ArchiveList(items))

base still has only its header; overview and archive each keep that header and add their own main content.

Slotted templates can contain their own slots. This lets a page fill a layout slot and still expose nested regions inside that slot.

projectPage := ProjectPage(project).
	WithSummary(ProjectSummary(project)).
	WithActivity(ProjectActivity(events))

page := DashboardLayout("Project").
	WithHeader(Header(user)).
	WithSidebar(ProjectSidebar(project)).
	WithMain(projectPage).
	WithNotifications(Notifications(alerts)).
	WithFooter(Footer())

In this example, ProjectPage(project) fills the layout’s main slot. The ProjectSummary and ProjectActivity templates fill slots that are declared inside ProjectPage.

Multiple templates in one slot

Every With<Slot> method is variadic, so pass several templates directly when you have them individually:

list := List().WithItems(
	Item("one"),
	Item("two"),
)

When the items are already collected in a slice, wrap them in goht.Fragment and pass that as the slot value. A Fragment renders its templates in order and satisfies goht.Template itself:

items := goht.Fragment{
	Item("one"),
	Item("two"),
}
list := List().WithItems(items)

Dynamic slot names

Generated templates also have a Slot(name string, templates ...goht.Template) method for callers that only know a slot’s name at runtime. It returns the same type as the matching With<Slot> method, so it can be chained the same way. Assigning a name that the template did not declare with @slot does not panic; it records the error and Render returns it before writing any output.

page := DashboardLayout("Dashboard").Slot("header", Header(user))

@ifslot, @noslot, and @eachslot

@ifslot <name> renders its body only when the caller assigned that slot, including an explicitly empty assignment. @noslot <name> is its opposite: it renders its body only when the caller did not assign that slot. @eachslot <template> in <slot> binds each template assigned to a slot to a loop variable; render it explicitly with @render.

@ifslot is not limited to guarding a bare @slot call; anything nested inside it, such as a heading, only renders when the slot was assigned. @noslot is a sibling directive, not a child of @ifslot — pairing an @ifslot <name> with a @noslot <name> right after it behaves like an if/else for that slot.

@haml Panel() {
	%section
		=@ifslot notes
			%p Some Title
			=@slot notes
		=@noslot notes
			%p No notes yet.
}
withNotes := Panel().WithNotes(Note("A note."))
withoutNotes := Panel()

Rendered output with the slot assigned — @ifslot renders and @noslot is skipped:

<section>
<p>Some Title</p>
<span>A note.</span>
</section>

Rendered output when notes is never assigned — @ifslot skips its whole body, and @noslot renders in its place:

<section>
<p>No notes yet.</p>
</section>

A bare @slot can carry its own default content, as shown under Named slots, and that default renders whenever the caller doesn’t assign the slot. But once @slot is nested inside @ifslot — as notes is above — there’s no bare @slot call left to hang a default on: the unassigned case belongs entirely to @ifslot’s sibling now. The same is true once you reach for @eachslot, which has no default content of its own either. Reach for a sibling @noslot any time you need fallback content alongside @ifslot or @eachslot.

If you don’t need extra content solely for the assigned case, you don’t need @ifslot/@noslot at all — a bare @slot with nested default content already covers both cases:

@haml ItemList() {
	%ul
		=@slot items
			%p No items
}
ItemList().Render(ctx, w)                        // renders "No items"
ItemList().WithItems(Item("one")).Render(ctx, w)  // renders Item("one")

@eachslot stands on its own; it doesn’t need @ifslot to guard it. If the slot wasn’t assigned, the loop simply runs zero times, so an unguarded @eachslot is safe:

@haml List() {
	%ul
		=@eachslot item in items
			=@render item
}
list := List().WithItems(Item("one"), Item("two"))

Slim uses the same directive:

@slim ListSlim() {
	ul
		= @eachslot item in items
			= @render item
}

EGO uses a block-form command tag:

@ego ListEgo() {
	<ul>
		<%@eachslot item in items { %>
			<%@render item %>
		<% } %>
	</ul>
}

An unassigned items slot renders an empty <ul></ul> this way. As with any slot, pair @eachslot with a sibling @noslot items if you want fallback content for that case instead — the same pairing shown for @ifslot above.