Skyline Scroll Hero
A scroll-driven React hero with a segmented city skyline — as you scroll the sky shifts from dusk to night, buildings light their windows section by section, the stage headline switches, and the moon rises in the last stage.
A scroll-narrative hero for landing pages. The outer section provides scrollLength of scroll distance while the inner stage pins to the top with sticky positioning; scroll progress, smoothed by a spring, drives the whole scene — the sky gradient moves from apricot dusk through blue hour into deep night, and the skyline silhouette along the bottom darkens with it. Buildings are generated deterministically from seed and split into as many groups as there are stages. Whichever stage you are in, that group's windows light up cell by cell in a fixed shuffled order, the stage text fades out and in, and in the last stage the moon rises over the horizon.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add skyline-scroll-hero.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<SkylineScrollHero/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/skyline-scroll-hero.en.jsonOr, once registries are configured in components.json, install it as @webberui/skyline-scroll-hero.
Usage
import { SkylineScrollHero } from "@/components/ui/skyline-scroll-hero";
<SkylineScrollHero
eyebrow="港灣夜行 · 城市散步路線"
title="一座城,三段夜色。"
stages={[
{ title: "六點,西區先亮起來", subtitle: "老街與巷口小店是路線的起點。" },
{ title: "七點半,河岸的辦公樓下班了" },
{ title: "九點,東區的塔樓與月亮同框" },
]}
primaryAction={{ label: "預約今晚的路線", href: "/booking" }}
secondaryAction={{ label: "看路線地圖", href: "/map" }}
/>(The sample copy reads: "Harbor Night Walks · City routes", "One city, three shades of night.", and stages "6 pm — the west side lights up first", "7:30 pm — the riverside offices clock out", "9 pm — the east towers share the frame with the moon".)
When the component lives inside a custom scroll container (for example a fixed-height overflow-y-auto block), pass the container's ref as container so scroll progress is measured against it:
const scrollerRef = React.useRef<HTMLDivElement>(null);
<div ref={scrollerRef} className="relative h-[420px] overflow-y-auto">
<SkylineScrollHero container={scrollerRef} stageHeight={420} title="一座城,三段夜色。" />
</div>Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | Main title (required); stays fixed for the whole scroll |
eyebrow | ReactNode | — | Small label above the title |
subtitle | ReactNode | — | Subtitle below the main title |
as | "h1" | "h2" | "h1" | Heading level; use h2 when the page already has an h1 |
primaryAction | SkylineScrollHeroAction | — | Primary CTA, filled with the accent color |
secondaryAction | SkylineScrollHeroAction | — | Secondary CTA, outlined |
stages | SkylineScrollHeroStage[] | [] | One entry per stage; the length sets how many segments the skyline splits into (an empty array gives three segments with no stage text) |
seed | number | 3 | Seed for building generation; the same seed always draws the same city |
scrollLength | number | 1800 | Total scroll height of the outer section (px); longer means each stage lingers longer |
stageHeight | number | 600 | Height of the sticky stage (px) |
minHeight | number | 560 | Minimum height (px) of the regular-height hero used under reduced motion |
container | RefObject<HTMLElement | null> | — | Scroll container; omit to follow window scroll |
duration | number | — | Stage text switch duration (ms), written to --wb-skyline-scroll-hero-switch; 300ms when omitted |
enterDuration | number | — | Header entrance duration (ms), written to --wb-skyline-scroll-hero-duration; 600ms when omitted |
stiffness | number | — | Progress spring stiffness, written to --wb-skyline-scroll-hero-stiffness; 100 when omitted (damping is fixed at 30) |
onStageChange | (index: number) => void | — | Called when the active stage changes, with the zero-based stage index |
className | string | — | Classes merged into the root section |
SkylineScrollHeroStage
| Field | Type | Default | Description |
|---|---|---|---|
title | string | — | Title for this stage |
subtitle | string | — | Supporting line for this stage |
SkylineScrollHeroAction
| Field | Type | Default | Description |
|---|---|---|---|
label | string | — | Button text |
href | string | — | Link URL; renders an <a> when given, otherwise a type="button" |
onClick | () => void | — | Click handler |
How it works
- Scroll progress comes from
useScrollwithoffset: ['start start', 'end end']: it is 0 when the top of the section meets the top of the viewport and 1 when its bottom meets the bottom, split evenly intostages.lengthsegments. - Buildings come from a deterministic PRNG seeded by
seed— 24–36 of them (width 30–90, height 80–320) with flat, spire, stepped, or antenna roofs. Server and browser draw exactly the same city, so there is no hydration mismatch. - Each building re-renders only when its lit-window count changes, so thousands of windows are not recomputed every frame; the lighting order is also a seeded shuffle, so it reads like residents switching their own lights on.
- In scroll mode the root uses
overflow-cliprather thanoverflow-hidden: the latter would make the section its own scroll container and break the sticky stage. - Overridable CSS variables: the four sky stops
--wb-skyline-scroll-hero-dusk(#f5c99b),--wb-skyline-scroll-hero-twilight(#e8a0a0),--wb-skyline-scroll-hero-blue(#4b5f8a),--wb-skyline-scroll-hero-night(#0b1220); window light--wb-skyline-scroll-hero-window(#fde68a); silhouettes--wb-skyline-scroll-hero-building-from/-to; accent--wb-skyline-scroll-hero-accent(#0d9488 light, #2dd4bf dark); timing--wb-skyline-scroll-hero-duration,--wb-skyline-scroll-hero-switch,--wb-skyline-scroll-hero-stiffness,--wb-skyline-scroll-hero-ease. - The component never uses
vh/h-screen; its height comes entirely fromscrollLengthandstageHeight(px).
Accessibility
- The root is a
<section aria-labelledby>pointing at the main title; the skyline, sky, moon, and side progress dots are allaria-hiddendecoration - Every stage's text stays in the DOM as an sr-only list (the active one marked
aria-current="step"), so screen readers never miss a stage because of scroll position; the visual switch animation is hidden from assistive tech - CTAs are
<a>ortype="button"with a focus-visible ring - Text color follows the sky — dark text at dusk, white once night falls — to keep contrast against the background
- When the user has "reduce motion" enabled at the system level, the sticky scroll narrative is dropped in favor of a regular-height hero showing the final night scene (all windows lit, moon risen) with the first stage's text, and the header entrance becomes a 150ms fade
Browser Frame Hero
A hero with one large browser-style device frame filling the right side, which straightens and scales up from a slight tilt as the page scrolls, holding any product screen you put inside.
Magnetic Lattice Hero
A dark tech-grid hero whose lattice of nodes and lines bends towards the pointer like a magnet, sending out a ripple wave from wherever you click.