A printed hero for a release-train tool: an editorial serif headline over a halftone city whose towers rise and light up on load. A Dot Rail under the nav runs on its own until a link is hovered, then jumps under it, and "Send a test train" sends a train across the elevated line at the foot of the city.
registry/blocks/skyline-hero.tsx
"use client";
import * as React from "react";
import { DotRail } from "@/components/ui/dot-rail";
import { Skyline } from "@/components/ui/skyline";
/* ── Content ─────────────────────────────────────────────────
* Everything you'd change to make the hero yours: the palette,
* the nav, the copy and the city. The dotted rail under the nav
* runs on its own until a link is hovered or focused, then
* jumps under it. "Send a test train" sends a train across the
* line at the foot of the city.
* ─────────────────────────────────────────────────────────── */
/**
* Crisp paper and ink, an electric blue (the headline's italic and the far
* city's halftone) and one hot signal color (the sun and the rail's run).
* In the dark the city is at night: dark towers with their windows lit.
*/
const PALETTE = {
light: { paper: "#fafaf9", ink: "#0b0b0d", accent: "#2747ff", signal: "#ff5a1f", tint: "#4562ff" },
dark: { paper: "#07080c", ink: "#f4f4f6", accent: "#8292ff", signal: "#ff6a2b", tint: "#3149f0" },
};
const NIGHT = { towers: "#1b1f2e", windows: "#ffd27a" };
const BRAND = { name: "Milepost", href: "#" };
const NAV_LINKS = [
{ label: "Timetable", href: "#timetable" },
{ label: "Stations", href: "#stations" },
{ label: "Pricing", href: "#pricing" },
{ label: "Changelog", href: "#changelog" },
];
const SIGN_IN = { label: "Sign in", href: "#sign-in" };
const HEADLINE = { before: "Ship on a", accent: "timetable", after: "not a hunch" };
const SUBLINE =
"Milepost cuts a release train every Tuesday at ten, runs your checks on the way and rolls it out one station at a time. Anything that misses it catches the next one.";
const PRIMARY_CTA = { label: "Start a timetable", href: "#start" };
const TRAIN_CTA = "Send a test train";
const NEXT_TRAIN = ["Train 214", "leaves Tue 10:00", "38 changes aboard"];
/** The city at the foot: change the seed for a different skyline. */
const CITY = { seed: 3, sun: 0.7, headway: 8 };
/* ── Section ─────────────────────────────────────────────── */
export function SkylineHero() {
const navRef = React.useRef<HTMLElement>(null);
const [trains, setTrains] = React.useState(0);
return (
<section
style={
{
"--paper-light": PALETTE.light.paper,
"--ink-light": PALETTE.light.ink,
"--accent-light": PALETTE.light.accent,
"--signal-light": PALETTE.light.signal,
"--tint-light": PALETTE.light.tint,
"--paper-dark": PALETTE.dark.paper,
"--ink-dark": PALETTE.dark.ink,
"--accent-dark": PALETTE.dark.accent,
"--signal-dark": PALETTE.dark.signal,
"--tint-dark": PALETTE.dark.tint,
"--night-towers": NIGHT.towers,
"--night-windows": NIGHT.windows,
} as React.CSSProperties
}
className="relative overflow-hidden bg-(--paper) text-(--ink) [--accent:var(--accent-light)] [--dot-rail-accent:var(--signal)] [--ink:var(--ink-light)] [--paper:var(--paper-light)] [--signal:var(--signal-light)] [--skyline-sun:var(--signal)] [--skyline-tint:var(--tint)] [--tint:var(--tint-light)] dark:[--accent:var(--accent-dark)] dark:[--ink:var(--ink-dark)] dark:[--paper:var(--paper-dark)] dark:[--signal:var(--signal-dark)] dark:[--tint:var(--tint-dark)]"
>
<header className="mx-auto max-w-6xl px-6 pt-4">
<div className="flex h-11 items-center justify-between gap-6">
<a
href={BRAND.href}
className="flex items-center gap-2 rounded-md text-[15px] font-semibold tracking-tight focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-(--accent)"
>
<MilepostMark className="h-5 w-auto" />
{BRAND.name}
</a>
<nav ref={navRef} aria-label="Main" className="hidden items-center gap-1 md:flex">
{NAV_LINKS.map((link) => (
<a
key={link.label}
href={link.href}
className="rounded-full px-3 py-1.5 text-[13px] font-medium opacity-65 transition-opacity duration-150 hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-(--accent)"
>
{link.label}
</a>
))}
</nav>
<a
href={SIGN_IN.href}
className="rounded-full px-4 py-1.5 text-[13px] font-medium ring-1 ring-current/25 transition-colors duration-150 hover:bg-current/5 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent)"
>
{SIGN_IN.label}
</a>
</div>
<DotRail trackRef={navRef} follow="loop" className="mt-2" />
</header>
<div className="mx-auto flex max-w-3xl flex-col items-center px-6 pt-10 text-center sm:pt-14">
<h1 className="font-serif text-[2.6rem] leading-[1.02] font-normal tracking-[-0.025em] text-balance sm:text-6xl lg:text-[4.25rem]">
{HEADLINE.before} <em className="text-(--accent)">{HEADLINE.accent}</em>, {HEADLINE.after}
</h1>
<p className="mt-4 max-w-xl text-[15px] leading-relaxed text-pretty opacity-70 sm:text-[17px]">{SUBLINE}</p>
<div className="mt-7 flex flex-wrap items-center justify-center gap-3">
<a
href={PRIMARY_CTA.href}
className="inline-flex h-11 items-center gap-2 rounded-full bg-(--ink) px-6 text-sm font-medium text-(--paper) transition-[opacity,scale] duration-150 hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent) active:scale-[0.97]"
>
{PRIMARY_CTA.label}
<svg viewBox="0 0 16 16" fill="none" aria-hidden className="size-3.5">
<path d="M3 8h10m-4-4 4 4-4 4" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</a>
<button
type="button"
onClick={() => setTrains((n) => n + 1)}
className="inline-flex h-11 items-center gap-2 rounded-full px-6 text-sm font-medium ring-1 ring-current/25 transition-[background-color,scale] duration-150 hover:bg-current/5 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent) active:scale-[0.97]"
>
<TrainGlyph className="h-3 w-auto" />
{TRAIN_CTA}
</button>
</div>
<p className="mt-5 flex flex-wrap justify-center gap-x-2 font-mono text-[11px] tracking-[0.14em] uppercase tabular-nums opacity-55">
{NEXT_TRAIN.map((part, i) => (
// On a phone the last fact drops, so the line never wraps on a dot.
<span key={part} className={i === NEXT_TRAIN.length - 1 ? "hidden whitespace-nowrap sm:inline" : "whitespace-nowrap"}>
{i > 0 && <span aria-hidden className="text-(--signal)">· </span>}
{part}
</span>
))}
</p>
</div>
<Skyline
seed={CITY.seed}
sun={CITY.sun}
train
headway={CITY.headway}
departures={trains}
className="mt-6 h-[clamp(13rem,38svh,21rem)] sm:mt-4 dark:text-(--night-towers) dark:[--skyline-light:var(--night-windows)]"
/>
</section>
);
}
/* ── Marks ───────────────────────────────────────────────── */
/** A milepost: a plate on a short post. */
function MilepostMark({ className }: { className?: string }) {
return (
<svg viewBox="0 0 18 22" fill="none" aria-hidden className={className}>
<rect x="1" y="1" width="16" height="11" rx="2" stroke="currentColor" strokeWidth="1.6" />
<path d="M5 6.5h8" stroke="var(--signal)" strokeWidth="2.2" strokeLinecap="round" />
<path d="M9 12v9" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
</svg>
);
}
function TrainGlyph({ className }: { className?: string }) {
return (
<svg viewBox="0 0 22 12" fill="none" aria-hidden className={className}>
<path
d="M1.5 2.5a1.5 1.5 0 0 1 1.5-1.5h12c3 0 5.5 2.6 5.5 5.5v1A1.5 1.5 0 0 1 19 9H3a1.5 1.5 0 0 1-1.5-1.5Z"
stroke="currentColor"
strokeWidth="1.4"
/>
<path d="M4 4.5h7" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
<path d="M5 11h3m6 0h3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
</svg>
);
}
Live preview and the components it's built with: https://keyframeui.com/blocks/hero-sections/skyline-hero
