Sticky Panel
<x-wirekit::sticky-panel> is a layout primitive for a companion column that pins itself beside an article while the page scrolls — a cart summary, a filter sidebar, a table of contents, a comparison builder. It has a non-scrolling header and footer with a scrollable body in between, and it is bounded to the viewport height so the footer never scrolls out of reach. It uses native CSS position: sticky — no JavaScript.
Basic Usage
The default slot is the scrollable body; header and footer are optional non-scrolling slots. (The preview pins a short max-height so the body scrolls inside the frame.) Each line is a realistic cart row — a product-image placeholder, the name on top with its variant + quantity below, and the price kept to the right and vertically centered. Long product names truncate with an ellipsis so the row height never jumps.
In a Two-Column Layout
Place the panel beside your article in a flex row. As the article scrolls, the panel stays pinned offset from the top. Below the hideBelow breakpoint it un-sticks and flows inline (set mobile-behavior="hide" to remove it on mobile instead).
Scroll inside the frame below — the order summary stays pinned while the (taller) article moves past it. On a real page the page itself is the scroll context; here the demo pins a fixed-height scroll region so you can see the stick without leaving the docs.
<div style="display: flex; gap: 2rem; align-items: flex-start;">
<article style="flex: 1 1 auto;">
{{-- your long-form content --}}
</article>
<x-wirekit::sticky-panel offset="2rem" width="20rem" label="Filters">
<x-slot:header>
<x-wirekit::heading :level="3" size="md">Filters</x-wirekit::heading>
</x-slot:header>
{{-- filter controls (scrollable) --}}
<x-slot:footer>
<x-wirekit::button surface="outline" class="w-full">Reset</x-wirekit::button>
</x-slot:footer>
</x-wirekit::sticky-panel>
</div>
position: sticky sticks relative to its nearest scrolling ancestor, not always the viewport. An ancestor with overflow: auto/scroll becomes that scroll context — the demo above relies on exactly this (the panel sticks inside the fixed-height scroll frame). The gotcha is an unintended one: if a wrapper you didn't mean to scroll sets overflow: hidden/auto/scroll, the panel silently sticks (or clips) there instead of where you expect, and overflow: hidden with no scroll room leaves it nowhere to stick. If your panel won't stick, check the ancestor chain for an unintended overflow.
Header-only / Footer-only
Both slots are optional — omit either and the layout collapses cleanly (the body always fills the remaining height).
Mobile sheet & collapse patterns
The panel keeps a single responsibility — a pinned, scrollable companion column. Two common "extras" are handled by composing an existing component rather than configuring this one, which keeps the markup explicit and the a11y model correct:
- Bottom sheet on mobile — set
mobileBehavior="hide"so the panel disappears below the breakpoint, then render a<x-wirekit::drawer position="bottom">holding the same content for small screens. The drawer already provides the focus trap, backdrop, and dialog semantics a bottom sheet needs. - Auto-collapse a tall panel — wrap the body content in a
<x-wirekit::collapsible>so the reader can fold it down when an article runs much longer than the panel. The collapsible carries its ownaria-expandedbutton and keyboard model.
The auto-collapse pattern composes directly inside the body slot — group the panel's sections into <x-wirekit::collapsible>s so a tall companion column folds down to just the parts the reader wants:
Props
| Prop | Type | Default | Description |
|---|---|---|---|
offset |
string | '2rem' |
Distance from the viewport top when pinned (any CSS length). Also drives the default max-height. |
maxHeight |
string|null | null |
Explicit max-height override; default is calc(100dvh - 2 × offset). |
width |
string | '20rem' |
Panel width on desktop (capped at 100%). |
hideBelow |
string | 'md' |
Breakpoint (sm/md/lg/xl) at/above which the panel sticks. |
mobileBehavior |
string | 'inline' |
Below the breakpoint: inline (un-stick, render in flow) or hide (remove). |
scrollShadow |
bool | true |
Show top/bottom shadows when the body overflows. They render as overlays above the content (so a hovered row never covers them) and auto-hide at the scroll extremes. Set false to disable. |
label |
string|null | null |
Accessible name for the panel + its scrollable body region. |
scope |
string|null | null |
Scoped personalization name. |
Slots
| Slot | Description |
|---|---|
| Default | The scrollable body region |
header |
Non-scrolling header pinned to the top of the panel |
footer |
Non-scrolling footer pinned to the bottom of the panel |
Accessibility
- Renders as a semantic
<aside>(acomplementarylandmark); passlabelto give it an accessible name. - The scrollable body is a keyboard-reachable region (
tabindex="0",role="region", anaria-label, and a visible focus ring) so keyboard and switch users can scroll it — WCAG 2.1.1. overscroll-behavior: containon the body stops scroll-chaining from jumping the whole page when the body reaches its scroll end.- Tab order follows DOM order: header → body → footer.
Keyboard Interaction
| Key | Action |
|---|---|
Tab |
Move focus through the header, the body region, then the footer (DOM order) |
Arrow keys / Page Up / Page Down |
Scroll the body region when it has keyboard focus |
Design Tokens
| Token | Used for |
|---|---|
--color-wk-bg-elevated |
Panel surface |
--color-wk-border |
Panel + header/footer dividers |
--radius-wk-lg |
Panel corner radius |
--shadow-wk-md |
Panel elevation |
--color-wk-ring |
Body-region focus ring |
--padding-wk-x-md / --padding-wk-y-md |
Header / body / footer padding |
Usage & Conventions
Composition. The panel is the <aside>; place it in your own two-column layout (a flex row with the article and the panel). The panel supplies the sticky behavior, the bounded height, and the scrollable body — your layout supplies the columns and the gutter.