Data display
Panel
A region of an application: it stands on the screen permanently and has a name of its own. A panel is not a card: it cannot be dragged, because it IS the place.
The header draws no rule under itself. A panel already encloses its contents once, with its own edge; a line across it thirty pixels in is a second enclosure of the same object. The title is a rung under its data and set in medium, which is what makes it read as a header. The one exception is a toolbar directly under the header — a strip of controls, whose own bottom edge is what parts it from the data.
Markup
<div class="inst-panel">
<div class="inst-panel-header">
<span class="inst-panel-title">Runs</span>
<div class="inst-panel-actions">
<button class="inst-btn inst-btn--sm" type="button">Refresh</button>
</div>
</div>
<div class="inst-panel-body">
The content of the region.
</div>
</div>
Markup
<div class="inst-panel inst-panel--plain">
<div class="inst-panel-header"><span class="inst-panel-title">Runs</span></div>
<div class="inst-panel-body">No edge: the step from the page to the sheet is the
boundary. Large regions only.</div>
</div>
Contract
| What | Required | Why |
|---|---|---|
--plain only on a large region |
yes | The boundary then rests on a step of 1.07 — measured to hold across 201 000 px² and to be lost across 36 000. A small panel owes its edge |
role="region" and aria-labelledby |
yes, if the panel is a landmark | Otherwise it will not appear in a screen reader's list of regions |
tabindex="0" on the body |
yes, if there is nothing focusable inside | A scrolling region has to be able to take focus from the keyboard |
| The width comes from outside | yes | A panel is declared a container; container-type switches off the intrinsic size and will collapse an element that takes its width from its content |
Accessibility
| The heading | inst-panel-title is visual. If the panel is a landmark, give it role="region" and an aria-labelledby pointing at the heading, otherwise it will not appear in the list of regions |
| Scrolling | The body scrolls by itself. A scrolling region has to be able to take focus from the keyboard — browsers give that to a tabindex="0" when there is nothing focusable inside |
| Depth | Carried by the surface rather than by a shadow: --surface-raised over --surface-page. In forced-colors the surfaces collapse, so a panel gets an explicit CanvasText border |
Anatomy
The body of a panel
<div class="inst-panel-body inst-panel-body--list">…</div>
| Class | When |
|---|---|
inst-panel-body |
The default: inner padding, scrolling of its own |
inst-panel-body--flush |
The padding is removed — the content knows about the edges itself (a table, a diff) |
inst-panel-body--list |
For a list of rows: padding on the vertical but not on the horizontal |
Variants
A panel has no -- modifiers and takes an attribute instead, because what
changes is not the panel but its place: the same panel is the point of one
screen and a footnote on another. The vocabulary is closed at three, and the
full contract is in
design principles.
Markup
<div class="inst-stack">
<div class="inst-panel" data-rank="lead">
<div class="inst-panel-header"><span class="inst-panel-title">Current run</span></div>
<div class="inst-panel-body">
<div class="inst-card">
<div class="inst-card-title">worldgen-01</div>
<div class="inst-card-sub">A card inside a lead panel is an ordinary card</div>
</div>
</div>
</div>
<div class="inst-panel" data-rank="default">
<div class="inst-panel-header"><span class="inst-panel-title">Recent runs</span></div>
<div class="inst-panel-body">Four finished in the last hour.</div>
</div>
<div class="inst-panel" data-rank="support">
<div class="inst-panel-header"><span class="inst-panel-title">Retention</span></div>
<div class="inst-panel-body">Transcripts are kept for thirty days.</div>
</div>
</div>
Rank does not descend. The card above carries no rank of its own, so it is an ordinary card even though the panel holding it leads the screen. A rank that inherited would be a dimmer with a nicer name; what a lead region leads is the screen, not its own contents.
Lead moves one thing. Its name takes one rung up the type ladder and nothing else: same ground, same edge. There is nowhere to lift a panel to — the raised surface is the top of the ramp in every light theme — so presence is spent on size, which the kit's ban on heavier weights already makes the only carrier of large-scale hierarchy.
Support may spend more, because withdrawing is the cheap direction: a rung down, an ink tier down, a recess film and no edge at all.
Rank is not tone. A lead panel is the principal one, not the good one. The
two combine — a data-rank="lead" data-tone="error" panel is an incident that
owns its screen.
Behavior
A panel is a container
A panel is declared a container (container: panel / inline-size), so the
components inside answer to its width. One and the same panel stands both
in the narrow column of an inspector and across the whole width of a
dashboard.
/* A rule of your own for a narrow panel */
@container panel (inline-size < 20rem) {
.my-thing { grid-template-columns: 1fr; }
}
The width always comes to a panel from outside, so declaring it a container is
safe. Declaring something a container when it takes its width from its content
is not allowed: container-type switches off the intrinsic size on the inline
axis and will collapse the element.
Something popping up inside a panel
A popover inside a panel will be clipped. A panel has overflow: hidden
for the sake of its radius. The library's contract: what pops up is drawn in
the top layer — [popover] or a portal — rather than inside the flow of the
panel. The same goes for the tooltip: inside a panel
a popover is wanted in its place.
API
| Name | What it does |
|---|---|
| class | |
inst-panel | The region. A container on the inline axis |
inst-panel-header | The header: a name and actions |
inst-panel-title | The name of the region |
inst-panel-actions | The actions in the header, pushed to the end |
inst-panel-body | The body, with scrolling |
| modifier | |
inst-panel-body--flush | With no inner padding |
inst-panel-body--list | Padding on the vertical only |
inst-panel--plain | No edge: the boundary is carried by the step of surface alone. For large regions |
| Name | Value | What it does |
|---|---|---|
| attribute | ||
data-rank | — | How much attention this region asks for: lead / default / support |
| Name | Value | Where from |
|---|---|---|
| token | ||
--surface-raised | light-dark(var(--n-0), var(--n-12)) | base +1: theme |
--border | light-dark(oklch(0 0 0 / 0.12), oklch(1 0 0 / 0.11)) | one across every cell |
--hairline | 1px | base +1: screen |
--radius-lg | 12px | base +4: scale |
--pad-panel | var(--space-5) | base +14: density · scale |
--text-sm | 0.875rem | base +4: scale |
--region-title-lead | var(--text-sm) | one across every cell |
--region-title-default | var(--text-xs) | one across every cell |
--region-title-support | var(--text-2xs) | one across every cell |
--region-ground-support | var(--surface-recessed) | one across every cell |
--region-edge-support | transparent | one across every cell |
Why the kit works this way — the constitution · Open source under MIT, github.com/keshon/instrument