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
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
| Name | Type | Default | Description |
|---|---|---|---|
heading | string | - | The title, and the region’s accessible name unless the host writes aria-label or aria-labelledby. |
level | 1 - 6 | 2 | The heading’s level. |
busy | boolean | false | Content is on its way: aria-busy and a bar under the header. |
inset | boolean | false | Pads the body, for prose rather than content that runs to the edges. |
Methods1
| Name | Type | Description |
|---|---|---|
focusTarget() | HTMLElement | null | Where F6 lands: the first control in the body (not in the actions), or the body itself. |
Slots4
| Name | Description |
|---|---|
(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. |
heading | Rich heading content, in place of the heading text. |
actions | Buttons and controls at the end of the header row. |
footer | A small, muted line under the body; hidden when empty. |
::part() hooks6
| Name | Description |
|---|---|
header | The header row, which takes the accent along its top while focus is inside the panel. |
heading | The heading, role="heading" at level. |
actions | The actions’ container. |
progress | The busy bar. |
body | The scrolling body. |
footer | The 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.
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 byheading, so a screen reader lists the panes of the page; the heading itself is a real heading atlevel. - The accent while focus is inside says which pane the keys act on; in forced colours it is a
Highlightborder. - The busy bar is decoration;
aria-busycarries the state.
Gotchas
- A panel fills the height its container gives it. In a flex row, give it
flex; inhui-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.