Skip to content
Home Theme Gallery

App shell

The custom element is <hui-app-shell>.

Overview

The frame of an application page: a header, navigation down one side, the main area, an optional aside and a status bar, each scrolling on its own inside the height it is given. It adds a skip control and F6 between the regions and panels, and holds no state.

Example

Header, sidebar, two panels, an aside and a status bar
Source
<hui-app-shell style="--hui-app-shell-height: 22rem; --hui-app-shell-aside: 12rem; border: 1px solid var(--hui-border)">
<header slot="header"><strong>Files</strong><span style="flex:1"></span><hui-button size="sm" variant="outline">Upload</hui-button></header>
<hui-sidebar slot="nav" label="Destinations" collapsible="rail" breakpoint="30rem">
<ul class="hui-sidebar-menu">
<li><a class="hui-sidebar-item" href="#all" aria-current="page"><span class="hui-sidebar-item-label">All files</span></a></li>
<li><a class="hui-sidebar-item" href="#recent"><span class="hui-sidebar-item-label">Recent</span></a></li>
</ul>
</hui-sidebar>
<main style="display:flex;min-block-size:0">
<hui-panel heading="Folders" style="flex:1">
<hui-tree aria-label="Folders">
<div role="treeitem" data-value="photos" aria-selected="true"><span data-label>Photos</span></div>
<div role="treeitem" data-value="music"><span data-label>Music</span></div>
</hui-tree>
</hui-panel>
<hui-panel heading="Photos" inset style="flex:2;border-inline-start:1px solid var(--hui-border)"><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>
</main>
<hui-panel slot="aside" heading="Details" inset><p style="margin:0">Select a file.</p></hui-panel>
<footer slot="footer"><span>14 items</span><span>Synced</span></footer>
</hui-app-shell>

The Go template that renders it:

<hui-app-shell>
<header slot="header">{{ template "app-bar" . }}</header>
<hui-sidebar slot="nav" label="Destinations">{{ template "destinations" . }}</hui-sidebar>
<main>
<hui-panel heading="Folders">{{ template "folders" . }}</hui-panel>
<hui-panel heading="{{ .Folder.Name }}" id="files">{{ template "files" . }}</hui-panel>
</main>
<hui-panel slot="aside" heading="Details">{{ template "details" . }}</hui-panel>
<footer slot="footer">{{ .Count }} items</footer>
</hui-app-shell>

On a phone

Below 48rem of its own width the aside drops under the main area and shares the height with it, and a hui-sidebar in the navigation slot becomes a sheet, closed on load, which a header button opens with toggle().

On a phone 375 px wide
Source
<hui-app-shell style="--hui-app-shell-height: 24rem; border: 1px solid var(--hui-border)">
<header slot="header"><hui-button variant="ghost" size="sm" onclick="this.closest('hui-app-shell').querySelector('hui-sidebar').toggle()">Menu</hui-button><strong>Files</strong></header>
<hui-sidebar slot="nav" label="Destinations">
<ul class="hui-sidebar-menu">
<li><a class="hui-sidebar-item" href="#all" aria-current="page"><span class="hui-sidebar-item-label">All files</span></a></li>
</ul>
</hui-sidebar>
<main style="display:flex;min-block-size:0"><hui-panel heading="Photos" inset style="flex:1"><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></main>
<hui-panel slot="aside" heading="Details" inset><p style="margin:0">Select a file.</p></hui-panel>
<footer slot="footer">14 items</footer>
</hui-app-shell>

API

Attributes1
Attributes
NameTypeDefaultDescription
skip-labelstring"Skip to main content"The skip control’s text.
Properties1
Properties
NameTypeDefaultDescription
mainHTMLElement | null-Read-only. The main area’s element: the first element in the default slot.
Methods2
Methods
NameTypeDescription
regions()HTMLElement[]The stops F6 visits, in order: header, navigation, each panel in the main area and the aside (or the area itself when it has none), status bar. Hidden ones are skipped.
focusRegion(region)voidMoves focus into a region: a panel’s first control, a region’s first control, or the region itself.
Slots5
Slots
NameDescription
headerThe application bar, spanning the top. Styled as a bordered row with its content centred on one line.
navNavigation down the start side - usually a hui-sidebar, which handles its own rail and sheet.
(default)The main area. Its element takes the full height and should manage its own scrolling, as hui-panels do.
asideAn optional pane on the end side, --hui-app-shell-aside wide; below 48rem it drops under the main area.
footerA status bar along the bottom: small, muted text on one line.
CSS custom properties2
CSS custom properties
NameDefaultDescription
--hui-app-shell-height100dvhThe shell’s height. The viewport by default, for a page that is the application.
--hui-app-shell-aside18remThe aside’s width.
::part() hooks7
::part() hooks
NameDescription
skipThe skip control, off screen until it has focus.
frameThe grid that lays out the regions.
headerThe header region.
navThe navigation region.
mainThe main region.
asideThe aside region; hidden when nothing is slotted.
footerThe status bar region; hidden when nothing is slotted.

States

Moving between panes with F6

F6 moves focus to the next region - the header, the navigation, each hui-panel in the main area and the aside, the status bar - and Shift+F6 to the previous, as the panes of a desktop application do. A panel is entered at its first control, not its header actions; a region with nothing focusable is focused itself. The skip control, first in the tab order, lands in the main area the same way.

Moving between panes with F6
Source
<hui-app-shell style="--hui-app-shell-height: 14rem; border: 1px solid var(--hui-border)">
<header slot="header"><strong>Mail</strong><hui-button size="sm" variant="ghost">Compose</hui-button></header>
<main style="display:flex;min-block-size:0">
<hui-panel heading="Inbox" style="flex:1"><hui-button variant="ghost" size="sm">First message</hui-button></hui-panel>
<hui-panel heading="Reading" inset style="flex:2;border-inline-start:1px solid var(--hui-border)"><p style="margin:0">Press F6 to move here.</p></hui-panel>
</main>
<footer slot="footer">F6 moves between panes</footer>
</hui-app-shell>

Server-side mechanics

Nothing is owned here. The regions are the host’s elements in named slots, so HTMX can swap any one of them - the main area on a navigation, the status bar out of band - and the shell lays out whatever arrives. It stores nothing and announces nothing. What the regions hold - the sidebar’s state, a panel’s content - is governed by those elements’ own contracts.

Accessibility

  • The host writes the landmarks - <header>, a hui-sidebar or <nav>, <main>, <aside>, <footer> - so a screen reader lists them as the page’s own; the shell adds no roles of its own.
  • The skip control is a button rather than a link, because Safari leaves links out of Tab unless the reader turns them on.
  • F6 is the pane key of desktop applications and browsers; the shell handles it only when focus is inside it.

Keyboard

Keyboard
KeysAction
TabThe skip control is the first stop; Enter or Space on it moves focus into the main area.
F6The next region or panel.
Shift + F6The previous region or panel.

Gotchas

  • The shell is the height it is given: the viewport by default. Inside a page with other content, set --hui-app-shell-height.
  • The main area hands its element the full height. Content that should scroll belongs in a hui-panel, or in an element with its own overflow; otherwise the main area scrolls as a whole.
  • F6 is also a browser shortcut for moving between the address bar and the page. The shell takes it only while focus is inside the shell.