Skyline Hero
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.
Hero Section
Built with Dot Rail and Skyline
Uses motion, clsx, tailwind-merge
Inspired by Citylines on vibrant.design
"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>
);
}
Installation
The CLI writes components/skyline-hero.tsx, the components it's built with (components/ui/dot-rail.tsx and components/ui/skyline.tsx) and lib/utils.ts, and installs motion, clsx and tailwind-merge. It needs the @keyframeui registry in components.json; add it once per project.
npx shadcn@latest add @keyframeui/skyline-heroThen put it where the section goes. The copy, links and data sit in constants at the top of the file, so making it yours starts there.
import { SkylineHero } from "@/components/skyline-hero";
export default function Page() {
return <SkylineHero />;
}
Built with
Each of these is a component on its own, with its own examples and props. The block installs them for you.
- Dot Rail A strip of LED dots that shows where you are. A run of lit dots jumps under the tab or link you point at, counts up under an autoplaying tab like a timer, sits at how far you've read, or loops while something loads. The dots it leaves glow for a moment and fade, like an LED sign, so a quick move draws a short comet tail. @keyframeui/dot-rail
- Skyline A printed city along the foot of a section: halftone towers in the distance, ink towers in front with their windows knocked out, a flat sun and stippled trees. The first time it shows, the towers rise out of the ground and their windows switch on one by one. Give it your numbers and each one becomes a tower with a sign on its roof. Hover a tower and it lights up floor by floor. An elevated line can run across the front, with a train you can send from a button. @keyframeui/skyline