UxStickyBand

components/ux/StickyBand.vue

The page's sticky band: its backing and its switch. While mounted, the band (useStickyBand) gathers the pinned members — the global Filter (band), NavJumpPills (joins on its own) and, with `title`, the page's UxPageTitle — and publishes --q-sticky-top so table headers pin under it. The backing spans the content column at the band's height, fades in (400ms) once anything pins, and carries a two-layer shadow. c104: filter and pills side by side. c106 (`title`): two columns — title and the pills docked under it, beside the filter — compacting once the title pins (title 15px with its hairline faded out, filter values only, outlined pills). Nothing in the band changes the page's flow.

The sticky band is the page's pinned header: the global Filter, the page title (UxPageTitle) and the sub nav (NavJumpPills) pin together over one backing, and table headers pin underneath it. It is page-level — one band per page, driven by the window's scroll — so the live demo runs in a frame: scroll inside it. Its panel (bottom right) switches between a detail page (title + pills) and a listing (title only), and logs every mode change with the page's height and content position, which must read Δ0.

Full page: /pages/sticky-title (c106) · the c104 layout: /pages/sticky-band

Set it up — once, in the layout

Mount UxStickyBand at the top of the content column, inside the relative wrapper that holds the global filter, with negative margins over the column's padding. Pass title for the two-column band. Pages need nothing: the title and the pills join on their own.

layouts/default.vue
<main class="flex-1 min-w-0 px-8 py-8">
  <div class="relative">
    <!-- the band: its backing and its switch (title = the c106 two-column band) -->
    <UxStickyBand title class="-mx-8" />
    <!-- the global filter, a band member -->
    <div class="pointer-events-none absolute inset-0 -top-10 z-40 flex flex-col items-end [&>*]:pointer-events-auto">
      <Filter band :stickyTop="8" v-model:keyValFilters="filters" … />
    </div>
    <slot />
  </div>
</main>
a page — nothing band-specific
<!-- a direct child of the page: no wrapper around the title -->
<UxPageTitle :breadcrumb="crumbs" :rowMinHeight="44">Payments</UxPageTitle>
<p>Who is on the Payments team…</p>
<TeamHeader … />                       <!-- not part of the band -->
<NavJumpPills :sections="SECTIONS" />  <!-- joins on its own -->
<section id="installs">…</section>

Two layouts

c104 — <UxStickyBand />

The filter and the sub nav pin side by side at the pin line (8px). The band is as tall as the taller of them; nothing restyles. The page title scrolls away.

c106 — <UxStickyBand title />

Two columns. Left: the page title (row 1) and, docked under it, the sub nav (row 2). Right: the filter. The band is as tall as the taller column, so a tall filter never opens space between title and pills. Once the title pins, the band compacts.

Organization/Teams/Payments
InstallsSessionsNotes
Payments ▾ · Past 7 days ▾ · Filter
EndpointHarnessSessions
pinned: title row 8–40 · pills docked 48–80 · band 90px (--q-sticky-top) · the header pins at 90 · jumps land at 106

Members

ComponentJoins whenPinned (c106, compact)Opt out
UxPageTitlea band runs with title15px, its hairline faded out; back link, #right, #far-right, #chin step aside; settles back in place on unpin. Drives the scroll mode.:sticky="false"
NavJumpPillsany band is mountedDocks under the title; no fill, grey-350 outline; black semibold; active purple bold with a 2px ring. Pill changes take 200ms.:sticky="false"
Filterit has bandValues only — the label rows and ADD step aside.:bandCompact="false" keeps the labels
UxTableListHeaderalways reads the bandPins at --q-sticky-top and eases with the band. compact tables pin at 0.—

Scrolling (c106)

  • rest — the title hasn't pinned: nothing restyles. The backing never shows at scroll 0.
  • down — the title has pinned: the band compacts (1s, quint-in-out). When the pills reach their line they dock and the band extends under them (200ms, ease-out) — until then it ends under the title, so the pills rise into open space.
  • up — tracked (24px of travel flips it) but drawn exactly as down while STICKY_BAND.upRestyles is false. On, it is an in-between set — title 20px, filter labels back — growing over 2s.
  • A pill jump holds the mode through its smooth scroll and lands 16px under the compact band.
  • Members style by look (the mode as drawn), never by mode. prefers-reduced-motion drops every transition.

Rules for consumers

  • Nothing in the band may change the page's flow. The title and the pills are sticky boxes that keep their rest height and restyle inside it; the filter and the backing take no flow space. That is why a mode change can't feed back into the scroll that caused it.
  • The title must be a direct child of a tall element (the page). A sticky box can't leave its parent — a wrapper around UxPageTitle traps it. Use rowMinHeight instead of a wrapper that sets the row's height.
  • One title drives the band. The first UxPageTitle to join holds the slot; others on the page stay plain.
  • Scroll boxes reset the band. A vertical scroll box holding a non-compact table sets --q-sticky-top: 0px inside (UxModal and UxOverflowX already do), or its header is pushed down within it.

Published on <html>

--q-sticky-topThe whole band (px) — table headers pin here.
--q-sticky-top-downThe band compact with the sub nav docked — where pill jumps land.
--q-sticky-row1Where the sub nav pins: under the title's row (c106 only; c104 pins at stickyTop).
--q-sticky-filter-wThe filter bar's width — the pinned title stops short of it.
--q-band-dur / --q-band-easeThe motion of the change in progress: 1s compacting, 2s growing, 200ms docking.
--q-band-fadeThe backing's fade (400ms), quicker than the motion.

STICKY_BAND (auto-imported)

Geometry and motion in one place — every row has a fixed height per look, so the band's height is known before it is drawn (a jump has to land under the compact band before the band has compacted).

pinTop8The pin line; rows are padded by the same.
titleRow32The pinned title's row.
titleSize{ down: 15, up: 20 }The pinned title's size per look.
navRow{ rest: 42, down: 30, up: 30 }The sub nav's row per look (its sticky box keeps the rest height).
navBottomPad16Space under the docked pills.
jumpGap16Between the band and a landed heading.
hysteresis24Travel before the scroll mode flips.
upRestylesfalseWhether scrolling up draws the in-between set.
motionquint-in-out · 1000 / 2000 · fade 400 · dock 200 (ease-out) · quick 200Compact / grow durations, the backing's fade, the dock, the pills.

useStickyBand() exposes the state for custom members and readouts: mode, look, top, topDown, titleBand, showBacking, and onFlip(fn) for instrumentation.

components/ux/StickyBand.vue

Usage

<UxStickyBand :title = "false" />

Props

PropTypeDefaultDescription
titleBooleanfalseRun the c106 two-column band: UxPageTitle joins and pins on row 1 beside the filter, NavJumpPills docks under it, and the band compacts while scrolling (scroll-up draws the same while STICKY_BAND.upRestyles is off). Off: the c104 band — filter and pills side by side at the pin line.

Related

Qpoint Brand Style Guide