ScrollScenes
Pin one stage under the header while the page scrolls; scenes fade in and out in turn. Pure CSS, native scrolling.
Wraps: none (CSS scroll-driven animation)
Step 1 of 3
Plan the week
Harbor Studio starts with one list: what has to ship, and who owns it.
Step 2 of 3
Track the work
Progress updates as people tick tasks, so nobody asks for a status report.
Step 3 of 3
Ship with confidence
Everything is green, the checklist is done: publish in one click.
Use it
import { Heading, ProgressBar, ScrollScenes, Stack, Text } from "@zedtech/zedkit-ui";
const steps = [
{ id: "plan", title: "Plan the week", text: "One list: what has to ship, and who owns it.", value: 0 },
{ id: "track", title: "Track the work", text: "Progress updates as people tick tasks.", value: 55 },
{ id: "ship", title: "Ship with confidence", text: "Everything is green: publish in one click.", value: 100 },
];
// The caption says it all; the visual is decoration, hidden from assistive tech.
<ScrollScenes
scenes={steps.map((step, index) => ({
id: step.id,
caption: (
<Stack gap="sm">
<Text variant="muted">Step {index + 1} of {steps.length}</Text>
<Heading level={3}>{step.title}</Heading>
<Text variant="muted">{step.text}</Text>
</Stack>
),
children: (
<Stack aria-hidden="true" inert>
<ProgressBar value={step.value} label="Week 43 progress" />
</Stack>
),
}))}
/>Before and after
Before
One long form
Twelve fields on one screen: people left before the end.
After
Three short steps
The same questions, split in three steps with a progress bar.
import { Heading, ProgressBar, ScrollScenes, Stack, Text } from "@zedtech/zedkit-ui";
// Two scenes, a shorter scroll per scene (default 60svh).
<ScrollScenes
className="[--zk-scenes-step:45svh]"
scenes={[
{
id: "before",
caption: <Heading level={3}>Before: one long form</Heading>,
children: (
<Stack aria-hidden="true" inert>
<ProgressBar value={18} label="Completed sign-ups" />
<Text variant="muted">18% of visitors finished the form.</Text>
</Stack>
),
},
{
id: "after",
caption: <Heading level={3}>After: three short steps</Heading>,
children: (
<Stack aria-hidden="true" inert>
<ProgressBar value={64} label="Completed sign-ups" />
<Text variant="muted">64% of visitors finished the steps.</Text>
</Stack>
),
},
]}
/>Same product in three looks
Look 1 of 3
Mist
The stock look: quiet chrome and Inter. Good when the product should stay out of the way.
Look 2 of 3
Press
Editorial type. Same buttons and fields, a reading product.
Look 3 of 3
Tide
Warm color and a bit more space. Same screen, a different shop.
import { Heading, ScrollScenes, Stack, Text, ThemeScope } from "@zedtech/zedkit-ui";
const looks = [
{ id: "mist", title: "Mist", text: "The stock look: quiet chrome, Inter." },
{ id: "press", title: "Press", text: "Editorial type for a reading product." },
{ id: "tide", title: "Tide", text: "Warm colour and a bit more space." },
];
<ScrollScenes
scenes={looks.map((look) => ({
id: look.id,
caption: (
<Stack gap="sm">
<Heading level={3}>{look.title}</Heading>
<Text variant="muted">{look.text}</Text>
</Stack>
),
children: (
<ThemeScope identity={look.id} aria-hidden="true" inert>
<Text className="font-semibold">Harbor Studio</Text>
<Text variant="muted">Project tracker</Text>
</ThemeScope>
),
}))}
/>Pricing walkthrough
Plan 1 of 3
Solo
One person, one project. Enough to try the tracker on real work.
Plan 2 of 3
Crew
A small team, one workspace. Shared list, no extra tools.
Plan 3 of 3
Studio
Every workspace, with SSO. For a studio that already has clients.
import { Heading, ScrollScenes, Stack, Text } from "@zedtech/zedkit-ui";
const plans = [
{ id: "solo", title: "Solo", text: "One person, one project.", price: "$0" },
{ id: "crew", title: "Crew", text: "A small team, one workspace.", price: "$29" },
{ id: "studio", title: "Studio", text: "Every workspace, with SSO.", price: "$99" },
];
<ScrollScenes
scenes={plans.map((plan) => ({
id: plan.id,
caption: (
<Stack gap="sm">
<Heading level={3}>{plan.title}</Heading>
<Text variant="muted">{plan.text}</Text>
</Stack>
),
children: (
<Stack aria-hidden="true" inert>
<Text className="font-semibold">{plan.price}</Text>
<Text variant="muted">per month</Text>
</Stack>
),
}))}
/>Kit not installed yet?
This ships on npm only; there is no copy-source brick for it.
Install
Terminal pnpm add @zedtech/zedkit-ui @fontsource-variable/inter @fontsource-variable/space-groteskLoad the styles
Tailwind does not scan node_modules, so the @source line points it at the kit's class names. The path is relative to this file.
src/app/globals.cssReplace the contents@import "tailwindcss"; @import "@zedtech/zedkit-ui/styles.css"; @source "../../node_modules/@zedtech/zedkit-ui/src/**/*.{ts,tsx}"; @source not "../../node_modules/@zedtech/zedkit-ui/src/**/*.test.*";Pick a look
Set data-identity="glass" on <html> so the finished app matches the site. Fresh app: replace src/app/layout.tsx. Existing app: merge, and keep ThemeProvider and other providers.
src/app/layout.tsxReplace the contentsimport "./globals.css"; import "./fonts"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en" data-identity="glass"> <body>{children}</body> </html> ); }Load the fonts
The Glass look uses Inter and Space Grotesk; these imports self-host them.
src/app/fonts.tsCreate the fileimport "@fontsource-variable/inter"; import "@fontsource-variable/space-grotesk";Import and render
Pinned feature tour import { Heading, ProgressBar, ScrollScenes, Stack, Text } from "@zedtech/zedkit-ui"; const steps = [ { id: "plan", title: "Plan the week", text: "One list: what has to ship, and who owns it.", value: 0 }, { id: "track", title: "Track the work", text: "Progress updates as people tick tasks.", value: 55 }, { id: "ship", title: "Ship with confidence", text: "Everything is green: publish in one click.", value: 100 }, ]; // The caption says it all; the visual is decoration, hidden from assistive tech. <ScrollScenes scenes={steps.map((step, index) => ({ id: step.id, caption: ( <Stack gap="sm"> <Text variant="muted">Step {index + 1} of {steps.length}</Text> <Heading level={3}>{step.title}</Heading> <Text variant="muted">{step.text}</Text> </Stack> ), children: ( <Stack aria-hidden="true" inert> <ProgressBar value={step.value} label="Week 43 progress" /> </Stack> ), }))} />
Package version 2.0.1.
Props
Prop
Type
Show inherited HTML attributes
Standard attributes from the underlying element. See MDN.
Prop
Type
Accessibility
Captions are real text in a fixed order and are never hidden (opacity only), so put the message in the caption and treat the visual as decoration: mark it aria-hidden="true" and inert so it adds no tab stop and no duplicate announcement. Under prefers-reduced-motion: reduce, in browsers without scroll timelines (Firefox, Safari before 26) and in very short windows, the scenes are plain blocks one under the other with the same content. A decorative progress rail is aria-hidden. Give each caption a heading one level under the section title so screen reader users can jump scene by scene.
Troubleshooting
Nothing is pinned — you need two to eight scenes, the browser needs `animation-timeline: view()` and the window at least 34rem tall; otherwise you get the stacked layout by design. Stage hides under a fixed header — set `--zk-scenes-top` to the header height. Scenes change too fast or slowly — set `--zk-scenes-step` (scroll distance per scene, default 60svh). Content cut at the bottom — the stage is one screen tall; shorten the scene or hide secondary parts on small screens. Pinning stops inside a box — a parent with `overflow: hidden` or `auto` becomes the scroll container; keep the component in the normal page flow. The scenes stack in your own preview — `prefers-reduced-motion: reduce` is on in the OS or the browser; this is the intended fallback.
Add depth with parallax layers
Section-shared CSS view timeline; layers move with depth, axis, zoom, fade, and range. Static without scroll timelines. Install with npm.
Marketing
Marketing sections and brief-routed domain specimens (commerce, SaaS dashboard, editorial, booking). Glass stays the default identity; strings are props.