Page Sections

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.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

3
1800
100
300
Active edited usageDefault props
<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.json

Or, 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

PropTypeDefaultDescription
titleReactNode—Main title (required); stays fixed for the whole scroll
eyebrowReactNode—Small label above the title
subtitleReactNode—Subtitle below the main title
as"h1" | "h2""h1"Heading level; use h2 when the page already has an h1
primaryActionSkylineScrollHeroAction—Primary CTA, filled with the accent color
secondaryActionSkylineScrollHeroAction—Secondary CTA, outlined
stagesSkylineScrollHeroStage[][]One entry per stage; the length sets how many segments the skyline splits into (an empty array gives three segments with no stage text)
seednumber3Seed for building generation; the same seed always draws the same city
scrollLengthnumber1800Total scroll height of the outer section (px); longer means each stage lingers longer
stageHeightnumber600Height of the sticky stage (px)
minHeightnumber560Minimum height (px) of the regular-height hero used under reduced motion
containerRefObject<HTMLElement | null>—Scroll container; omit to follow window scroll
durationnumber—Stage text switch duration (ms), written to --wb-skyline-scroll-hero-switch; 300ms when omitted
enterDurationnumber—Header entrance duration (ms), written to --wb-skyline-scroll-hero-duration; 600ms when omitted
stiffnessnumber—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
classNamestring—Classes merged into the root section

SkylineScrollHeroStage

FieldTypeDefaultDescription
titlestring—Title for this stage
subtitlestring—Supporting line for this stage

SkylineScrollHeroAction

FieldTypeDefaultDescription
labelstring—Button text
hrefstring—Link URL; renders an <a> when given, otherwise a type="button"
onClick() => void—Click handler

How it works

  • Scroll progress comes from useScroll with offset: ['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 into stages.length segments.
  • 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-clip rather than overflow-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 from scrollLength and stageHeight (px).

Accessibility

  • The root is a <section aria-labelledby> pointing at the main title; the skyline, sky, moon, and side progress dots are all aria-hidden decoration
  • 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> or type="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