Glass header
StableA header that is clear at the top of the page and frosts over the content once the page scrolls.
useScrolled() says whether the window has scrolled past a threshold. Put the answer on the header as data-scrolled and .lux-glass does the rest: a translucent ground with a 20px blur, a hairline and a soft shadow, eased in over 500ms. Without backdrop-filter, or with reduced transparency, the ground is solid instead.
Frosting on scroll
import { useState } from 'react'import { Button } from '@rchursin/product/ui'
/** * Scroll the panel: the header is clear at the top and frosts once content passes under it. * .lux-glass reads data-scrolled. This panel scrolls on its own, so it sets the attribute from * scrollTop; for the page itself, useScrolled() does it from the window. */const entries = [ { version: '2.4', date: 'Oct 6', title: 'Parallel scans', body: 'Skills are read in parallel, so a large repository finishes in seconds instead of minutes.' }, { version: '2.3', date: 'Sep 22', title: 'Overlap finder', body: 'Two skills that do the same job are listed side by side, with the one to keep marked.' }, { version: '2.2', date: 'Sep 8', title: 'Quieter reports', body: 'Reports lead with what to change. Raw numbers sit one click away.' }, { version: '2.1', date: 'Aug 25', title: 'Trigger check', body: 'Finds skills the agent never picks on its own and suggests a clearer description.' }, { version: '2.0', date: 'Aug 11', title: 'Team mode', body: 'Counts, durations and costs from every machine, in one weekly report.' },]
export default function GlassHeaderScrollPanel() { const [scrolled, setScrolled] = useState(false) const [following, setFollowing] = useState(false) return (Usage
import { useScrolled } from '@rchursin/components/motion'const scrolled = useScrolled()
return ( <header data-scrolled={scrolled ? '' : undefined} className="lux-glass sticky top-0 z-30"> … </header>)Examples
Reading the page scroll
useScrolled() on this very page: the bar is clear only when the page is at the very top. The site header above works the same way.
The page is at the top, so the bar is clear.
import { useScrolled } from '@rchursin/components/motion'
/** * useScrolled() is true once the window has scrolled past the threshold (8px by default). This * bar reads the real page: it is clear only at the very top of this page and frosted below it. * On a site you would put the same attribute on a sticky <header>. */export default function GlassHeaderWindow() { const scrolled = useScrolled() return ( <div className="relative w-full max-w-md overflow-hidden rounded-2xl bg-surface ring-1 ring-line"> <div data-scrolled={scrolled ? '' : undefined} className="lux-glass flex h-14 items-center justify-between gap-3 px-5"> <span className="text-[14px] font-semibold text-fg">Page header</span> <span className="font-mono text-[12px] text-fg-2">data-scrolled {scrolled ? 'on' : 'off'}</span> </div> <p className="px-5 pt-3 pb-5 text-[13.5px]/6 text-pretty text-fg-2"> {scrolled ? 'The page has scrolled, so the bar shows its hairline and frosted ground.' : 'The page is at the top, so the bar is clear.'} </p> </div> )}API
useScrolled(threshold)
| Prop | Type | Default | Description |
|---|---|---|---|
threshold | number | 8 | Pixels of window scroll before it returns true. Checked at most once per animation frame while scrolling. |
returns | boolean | – | false on the server and on the first render, then window.scrollY > threshold. |
CSS
| Class or variable | What it does |
|---|---|
lux-glass | Eases background, shadow and blur over 500ms. Nothing changes until data-scrolled is present. |
lux-glass[data-scrolled] | A --lux-glass ground (72% white, or 62% near-black in dark mode) with blur(20px) saturate(1.4), a hairline under it and a soft shadow. Solid (--lux-glass-solid, 96%) where backdrop-filter is missing or reduced transparency is on. |
--lux-glass, --lux-glass-solid | The frosted and the solid ground, per theme. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab | In the example, focuses the scrolling panel (it has tabindex="0" and a name, so it can be scrolled without a mouse). |
| Up arrowDown arrowPage UpPage DownHomeEnd | Scroll the focused panel; the header frosts as soon as it leaves the top. |
- It only changes the look: the header keeps its landmark and its links at every scroll position.
- The frosted ground is at least 62% opaque and blurs what is behind it, so header text stays readable over busy content.
- useScrolled reads the window. A panel that scrolls on its own sets data-scrolled from its own scrollTop, as the first example does; make such a panel focusable and give it a name.
Guidelines
Do
- Make the header sticky (sticky top-0) with a z-index above the content.
- Keep the default 8px threshold, so it frosts as soon as anything passes under it.
Don’t
- Don’t add background or backdrop utilities to the same element unless you mean to replace the glass: utilities win over the class.
- Don’t make the header tall; the frosted area should be a thin band.
In use
Used on this site
Related
- Site headerThe home pages’ sticky header in three styles: a full bar, a floating glass pill and a minimal mono row.
- HairlinesA 1px rule that fades out at both ends, and a lit top edge for dark panels.
- Easing & transitionsFour motion curves, a soft cross-fade for view changes, a reduced-motion hook and disclosures that ease open.
Source
The component’s own files, exactly as they run on this site.
/* RChursin_ finishing layer ("luxe"), ported from the RChursin_ UI kit (rchursin/styles/luxe.css). Needs the --ease-lux tokens in brand.css. Site changes would be marked "Site change:"; there are none, so `node .claude/skills/porting-ui-kit/scripts/kit.ts diff` should report this file in sync. */
/* RChursin_ · the finishing layer ("luxe")
The details that make the kit feel made, not generated: layered light on buttons, hairlines that catch the light, surfaces that lift, a whisper of film grain, and motion that is quick to answer and slow to arrive.
Rules this file follows - One light source, straight above: highlights on top edges, shades below. - Depth comes from hairlines and surface tone first; shadows stay faint. - Clicks answer in ≤200ms. Ambient arrivals take 600–1100ms. Exits are faster than entrances. - Movement only runs when the visitor allows motion; colour and opacity changes stay. - Nothing loops forever on its own. Everything sits in @layer components, so Tailwind utilities on the same element still win.*/