Skip to content
Home Theme Gallery

Panel

The custom element is <hui-panel>.

Overview

A titled pane: a heading and the host’s actions in a header row, a body that scrolls on its own, and an optional footer. It is a region named by its heading, takes an accent while focus is inside it, and shows a busy bar while its content is on its way.

Example

A pane with actions, a tree and a footer
Source
<hui-panel heading="Folders" style="block-size: 15rem; border: 1px solid var(--hui-border); border-radius: var(--hui-radius-lg); overflow: hidden">
<hui-button slot="actions" variant="ghost" size="icon-sm" aria-label="New folder">+</hui-button>
<hui-tree aria-label="Folders">
<div role="treeitem" data-value="photos" aria-expanded="true"><span data-label>Photos</span>
<div role="group">
<div role="treeitem" data-value="photos/2024" aria-selected="true"><span data-label>2024</span></div>
<div role="treeitem" data-value="photos/2025"><span data-label>2025</span></div>
</div>
</div>
<div role="treeitem" data-value="music"><span data-label>Music</span></div>
<div role="treeitem" data-value="videos"><span data-label>Videos</span></div>
</hui-tree>
<span slot="footer">5 folders</span>
</hui-panel>

The Go template that renders it:

<hui-panel heading="Folders" id="folders"{{ if .Loading }} busy{{ end }}>
<hui-button slot="actions" variant="ghost" size="icon-sm" aria-label="New folder">+</hui-button>
<hui-tree aria-label="Folders">{{ template "folder-node" .Root }}</hui-tree>
<span slot="footer">{{ .Count }} folders</span>
</hui-panel>

API

Attributes4
Attributes
NameTypeDefaultDescription
headingstring-The title, and the region’s accessible name unless the host writes aria-label or aria-labelledby.
level1 - 62The heading’s level.
busybooleanfalseContent is on its way: aria-busy and a bar under the header.
insetbooleanfalsePads the body, for prose rather than content that runs to the edges.
Methods1
Methods
NameTypeDescription
focusTarget()HTMLElement | nullWhere F6 lands: the first control in the body (not in the actions), or the body itself.
Slots4
Slots
NameDescription
(default)The body. A hui-tree, hui-listbox, hui-table or hui-data-grid straight in it fills the body and drops its own border.
headingRich heading content, in place of the heading text.
actionsButtons and controls at the end of the header row.
footerA small, muted line under the body; hidden when empty.
::part() hooks6
::part() hooks
NameDescription
headerThe header row, which takes the accent along its top while focus is inside the panel.
headingThe heading, role="heading" at level.
actionsThe actions’ container.
progressThe busy bar.
bodyThe scrolling body.
footerThe footer row.

States

Loading, and prose

busy sets aria-busy and runs a bar along the foot of the header - point an hx-indicator at it, or set it from the server while a slow part renders. inset pads the body for prose; a list, tree, table or grid placed straight in the body instead fills it edge to edge and scrolls inside itself. A body that scrolls with nothing focusable inside becomes a named Tab stop so a keyboard can scroll it.

Loading, and prose
Source
<hui-panel heading="Release notes" busy inset style="block-size: 12rem; border: 1px solid var(--hui-border); border-radius: var(--hui-radius-lg); overflow: hidden">
<p style="margin:0 0 .5rem">Entry 1</p><p style="margin:0 0 .5rem">Entry 2</p><p style="margin:0 0 .5rem">Entry 3</p><p style="margin:0 0 .5rem">Entry 4</p><p style="margin:0 0 .5rem">Entry 5</p><p style="margin:0 0 .5rem">Entry 6</p><p style="margin:0 0 .5rem">Entry 7</p><p style="margin:0 0 .5rem">Entry 8</p><p style="margin:0 0 .5rem">Entry 9</p><p style="margin:0 0 .5rem">Entry 10</p><p style="margin:0 0 .5rem">Entry 11</p><p style="margin:0 0 .5rem">Entry 12</p><p style="margin:0 0 .5rem">Entry 13</p><p style="margin:0 0 .5rem">Entry 14</p>
</hui-panel>

Server-side mechanics

The heading, the actions and the body are the host’s markup, and busy is the server’s or an hx-indicator’s to set. Give a panel an id and an HTMX response can replace its body, or the whole panel out of band; nothing inside the element needs to be reconciled. It stores nothing.

Accessibility

  • A role="region" landmark named by heading, so a screen reader lists the panes of the page; the heading itself is a real heading at level.
  • The accent while focus is inside says which pane the keys act on; in forced colours it is a Highlight border.
  • The busy bar is decoration; aria-busy carries the state.

Gotchas

  • A panel fills the height its container gives it. In a flex row, give it flex; in hui-resizable, it takes its pane’s size.
  • The accent follows :focus-within, so a panel whose content has no focusable element never shows it; F6 still focuses the body.