Light & grain
StableA ceiling halo for heroes, film grain for flat grounds, a fade for product shots and a shadow that follows torn paper.
Four still touches that make a flat page feel lit. lux-halo hangs a soft pool of light from the top of a section, lux-grain lays a faint film grain under the content, lux-fade-bottom sinks an image into the page, and lux-paper-shadow is a drop-shadow that follows any outline.
Updated View source
A lit hero
/** * A hero lit from above: lux-halo is a soft pool of ceiling light behind the content, * lux-grain lays a faint film grain over the flat ground, and lux-fade-bottom lets the * product shot sink into the page instead of ending on a hard edge. */export default function LightGrainHero() { return ( <section className="lux-grain relative isolate overflow-hidden bg-surface px-6 pt-14 text-center"> <div aria-hidden="true" className="lux-halo" /> <p className="font-mono text-[11px] tracking-[0.14em] text-fg-3 uppercase">Skill analytics</p> <h3 className="mx-auto mt-3 max-w-md text-[28px]/[1.15] font-semibold tracking-[-0.03em] text-balance text-fg">Know which skills your agents use</h3> <p className="mx-auto mt-3 max-w-sm text-[14px]/6 text-pretty text-fg-2">One scan lists every skill, how often it runs and which ones never do.</p>
<div className="lux-fade-bottom mx-auto mt-10 max-w-md" aria-hidden="true"> <div className="lux-card flex flex-col gap-2 rounded-t-2xl bg-raised p-4 text-left"> {['test-fixer', 'release-notes', 'pr-review', 'db-migrate', 'i18n-sync'].map((name, i) => ( <div key={name} className="flex items-center gap-3 rounded-lg px-2 py-1.5 text-[12.5px]"> <span className="w-28 shrink-0 font-mono text-fg-2">{name}</span> <span className="h-1.5 rounded-full bg-fg/70" style={{ width: `${88 - i * 17}%` }} /> </div> ))} </div> </div> </section> )}Usage
// Once, in your app entry: Tailwind, the brand tokens, the effects and the finishing layerimport '@rchursin/product.css'<section className="lux-grain relative isolate overflow-hidden"> <div aria-hidden="true" className="lux-halo" /> …</section>Examples
Paper shadow
A receipt with torn edges. The shadow follows the tears because it is a drop-shadow on a wrapper.
/** * lux-paper-shadow is a drop-shadow, so it follows the outline of whatever it wraps, torn edges * included. Put it on a wrapper: a mask on the paper itself would cut a box-shadow away. */const rows = [ ['Sessions read', '1,284'], ['Skills found', '14'], ['Never used', '3'],]
export default function LightGrainPaper() { return ( <div className="lux-paper-shadow w-full max-w-64"> <figure className="lux-grain relative bg-paper px-6 pt-8 pb-10 font-mono text-[12px]/[1.8] text-ink [mask-image:radial-gradient(circle_at_6px_0,transparent_5px,#000_5.5px),radial-gradient(circle_at_6px_100%,transparent_5px,#000_5.5px)] [mask-position:top,bottom] [mask-repeat:repeat-x] [mask-size:12px_51%]"> <figcaption className="flex items-center justify-between text-[10px] tracking-[0.12em] text-stone uppercase"> <span>Weekly scan</span> <span>Sample</span> </figcaption> <dl className="mt-4 flex flex-col"> {rows.map(([k, v]) => ( <div key={k} className="flex items-baseline justify-between gap-2"> <dt>{k}</dt> <dd className="tabular-nums">{v}</dd> </div> ))} </dl> <p className="mt-4 border-t border-dashed border-ink/30 pt-3 font-semibold">Verdict: cut 3</p> </figure> </div> )}API
CSS
| Class or variable | What it does |
|---|---|
lux-halo | An absolute layer across the top of its section (44rem tall, z-index -1) with a radial glow of --lux-halo. Put it inside a positioned, isolated, overflow-hidden section. |
lux-grain | A static 180px noise tile under the element’s content, at --lux-grain-opacity (3.5%, or 5% inverted in dark mode). Positions and isolates the element. |
lux-fade-bottom | A mask that keeps the top 62% solid and fades the rest to transparent. |
lux-paper-shadow | Stacked drop-shadows (three in light mode, two deeper ones in dark) that follow the painted outline, masked and torn edges included. |
--lux-halo, --lux-grain-opacity | The halo colour (5% black, or 10% white in dark mode) and the grain strength. |
Accessibility
- All four are decoration made of pseudo-elements, masks and filters, so they add nothing for screen readers. Mark the halo layer aria-hidden.
- The grain is at most 5% strong and sits under the content, and the halo at most 10%, so text keeps its contrast.
- lux-fade-bottom fades content out: keep text and controls out of the faded part.
- None of them move, so there is nothing to switch off for reduced motion.
Guidelines
Do
- Use one halo per page, at the top of the hero.
- Put grain on large flat areas: heroes, footers, sign-in pages.
- Put lux-paper-shadow on a wrapper when the paper itself is masked.
Don’t
- Don’t fade anything people need to read or press.
- Don’t nest grain inside grain; one layer per section is enough.
In use
Used on this site
Related
- Cards & elevationCards on one four-step elevation scale: lux-card, a lower lux-card-flat, lux-lift on hover and lux-float for overlays.
- BackgroundsDot and grid patterns, masks and vignettes to fade them, a retro floor grid, a spotlight, a lamp and a softly lit card.
- ReceiptA paper receipt for one agent task: torn edges, dotted leaders, a total, and a FIX stamp that lands.
- Auth layoutThe frame for every sign-in page: a dark brand panel when there is room, one calm form column, the logo on top and legal links below.
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.*/