Skip to content
Components

Panel

The non-modal surface docked beside a live map, holding one subject — a parcel's report, a design being built — while the map behind it stays usable.

The panel

GdSidePanel wires its own GdPanelHeader and GdPanelFooter — you pass a title, a meta line and a footer slot. The body is yours: here, two GdPanelSections.
Code
<GdSidePanel title="42 Jarrah Street" @close="selected = null">
  <template #meta><GdCode>L14 RP80432</GdCode> · Millbrook</template>

  <GdPanelSection label="The lot">…</GdPanelSection>

  <GdPanelSection label="What is on the land">
    <template #action><GdButton variant="ghost" size="sm">See all</GdButton></template>
    …
  </GdPanelSection>

  <template #footer>
    <GdButton variant="ghost">Save</GdButton>
    <GdButton variant="primary">Design this site</GdButton>
  </template>
</GdSidePanel>

The parts

Header

42 Jarrah Street

L14 RP80432Millbrook
Pass the scrolling element as scroller and the hairline follows it. Call the exposed sync() after any scroll you set from code.
Code
<GdPanelHeader title="42 Jarrah Street" :scroller="bodyEl">
  <template #leading><GdButton icon variant="ghost" label="Back">…</GdButton></template>
  <template #meta><GdCode>L14 RP80432</GdCode> · Millbrook</template>
  <template #actions><GdButton icon variant="ghost" label="Save this parcel">…</GdButton></template>
</GdPanelHeader>

Section

Lot area
812m²
Frontage
15.2m
Creek floodingRear of lot · mapped overlay
The section does not draw its own rule — it draws the rule BETWEEN itself and the section before it.
Code
<GdPanelSection label="What is on the land">
  <template #action><GdButton variant="ghost" size="sm">See all</GdButton></template>
  <GdListRow title="Creek flooding" sub="Rear of lot" />
</GdPanelSection>

Footer

end, between, stretch — in that order below.
Code
<GdPanelFooter>
  <GdButton variant="ghost">Save</GdButton>
  <GdButton variant="primary">Design this site</GdButton>
</GdPanelFooter>

<GdPanelFooter layout="stretch">
  <GdButton variant="primary">Design this site</GdButton>
</GdPanelFooter>

API

GdSidePanel

Props

PropTypeDefault
titlestring—

The subject. Also names the panel's landmark, via `aria-labelledby`.

metastring—

The fact line under the title. The `meta` slot takes markup instead — chips and links.

side"right" | "left""right"

Which edge the panel docks against, and which edge it enters from.

widthstring—

Any CSS length. No default here — the component's own stylesheet sets the resting width.

labelstring—

The landmark's name when there is no title to borrow one from.

closablebooleantrue

Draws the close button and answers Escape. Off, the panel has no dismissal of its own and `close` never fires.

closeLabelstring"Close panel"

The close button's accessible name.

footerLayout"end" | "between" | "stretch""end"

Passed through to the `GdPanelFooter` the panel wires for you.

Slots

default

The body — usually a stack of `GdPanelSection`s. It is the scrolling element.

leading

Before the title in the header — a back button.

title

Markup in place of the `title` prop.

meta

Markup in place of the `meta` prop.

actions

The header's right-hand controls, beside the close button.

footer

The floor. Present it and the panel renders a `GdPanelFooter` around it.

Events

close[]

The close button, or Escape with focus inside the panel. The panel never removes itself — the caller owns whether it is mounted.

GdPanelHeader

Props

PropTypeDefault
titlestring—

The subject.

titleIdstring—

Id for the heading, so the panel can name itself by `aria-labelledby`.

titleAsstring"h2"

The heading LEVEL, which is the caller's — a panel beside a page's h1 is an h2, one nested deeper is deeper. The SIZE is not a prop at all.

metastring—

The fact line under the title. The `meta` slot takes markup instead.

scrollerHTMLElement | null—

The element whose scroll decides the rule — usually the panel's body. Without it the header simply never earns its hairline.

Slots

leading

Before the title — a back button.

title

Markup in place of the `title` prop.

meta

Markup in place of the `meta` prop.

actions

The right-hand controls.

GdPanelSection

Props

PropTypeDefault
labelstring—

The eyebrow. The `label` slot takes markup instead.

labelAsstring"h3"

The label's heading level — `h3` under `GdPanelHeader`'s `h2`. Pass it when the panel sits somewhere else in the outline.

Slots

default

The block's content.

label

Markup in place of the `label` prop.

action

One control opposite the label — "See all".

GdPanelFooter

Props

PropTypeDefault
layout"end" | "between" | "stretch""end"

`end` is a row of buttons with the commit on the right; `between` puts a quiet action left and the commit right; `stretch` gives the children the full width.

Slots

default

The actions. Their arrangement is the layout prop.

Usage Guidelines

  • Use GdSidePanel when the thing behind it must stay usable — a report about a parcel while the user pans, clicks the next lot, toggles a layer.
  • Use GdDrawer when the thing behind it must NOT be touched — a settings sheet, a form that has to be finished or abandoned. It traps focus, scrims the background and answers Escape from anywhere.
  • Use GdPageHeader when you are opening a DOCUMENT rather than describing a subject. It is a full-bleed band and it carries the page's one <h1>.
  • Give the panel's container position: relative. The panel is positioned against it, and the map shell already is one.
  • Pass the body as scroller whenever you use GdPanelHeader on its own. GdSidePanel already does it for you.
  • Call sync() after any scroll you set from code — a new selection, a tab swap. GdSidePanel.scrollToTop() already does.
  • Use layout="stretch" when the panel's floor is one door. Use between when a quiet action has to sit opposite the commit; leave it on end otherwise.
  • Label every section. The label is a real heading, and it is how a screen-reader user navigates a panel with a dozen blocks in it.

Behavior & Anatomy

Panel, header or drawer

  • Not GdPageHeader — that is a full-bleed band opening a DOCUMENT, carrying the page's one <h1>. A panel floats over a canvas and its title is a subject, not a page.
  • Not GdDrawer — the drawer is a native <dialog> opened with showModal(): focus trapped, everything behind it scrimmed and inert, Escape answered from anywhere. Every one of those is right for a settings sheet and is the opposite of what a panel needs.
  • So: a plain <aside> — complementary to the map, which stays the main content. No trap, no scrim, no inert, nothing stacked above --gd-z-panel.

Non-modal is the whole point: the user reads the report AND pans, clicks the next lot, toggles a layer. A surface that makes the map unreachable while describing a piece of it has misunderstood the job.

The earned hairline

At rest GdPanelHeader has no bottom border. The rule and its scroll shadow arrive only once the body beneath has actually moved. A header that always carries a rule reads as detached from the panel it belongs to. The line is a claim that there is content above the fold, and at scrollTop: 0 that claim is false. GdPanelFooter's rule is unconditional for the same reason inverted: a footer always has content above it, so its line is a fact rather than a claim.

  • The header watches the scrolling element itself, passed as scroller — every panel that hand-rolled this wrote the same passive listener and then got the reset wrong.
  • A programmatic scrollTop = 0 — a new selection, a tab swap — fires no scroll event, which strands the rule on over a body that is back at the top. Call the exposed sync() after any scroll you set from code; GdSidePanel.scrollToTop() already does.
  • The threshold is 4px rather than 0: a trackpad flick back to the top leaves a pixel or two of scrollTop behind on the way, and a rule that flickers there is worse than no rule at all.
  • The border fades, the shadow does not: animating a shadow repaints the full width of the body's top edge every frame, for a mark nobody watches arrive.

The heading level is the caller's, the size is not

A panel beside a page's h1 is an h2, one nested deeper is deeper — so titleAs and labelAs exist. The SIZE is not a prop: a panel title is a panel title wherever it lands, or the outline and the type start disagreeing about which is the hierarchy. The section's label is a real heading and not a styled span for the same reason — a panel with twelve unlabelled blocks is a panel a screen-reader user has to read linearly to navigate.

Why the section draws the rule above it

The section does not draw its own rule — it draws the rule BETWEEN itself and the section before it. A border on every section puts a line above the first one, a few pixels under the header's hairline, and two rules that close together read as a mistake rather than as structure.

Why the footer pins two ways

As the last flex child of a panel column it is simply inert and the body scrolls past it; dropped inside a scrolling body it sticks to the bottom edge. Both arrangements exist already, and no caller should have to know which one the footer would have preferred. The safe-area inset is not decoration — on a phone this bar is the bottom of a sheet resting on the home indicator, and without it the panel's primary action sits under the gesture bar. stretch is for the panel whose floor is ONE door: a right-aligned button in a 400px bar reads as an afterthought rather than as the panel's entire point.

Navigate

Esc