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

A shadcn/ui block from the [@keyframeui](https://keyframeui.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/skyline-hero).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/skyline-hero
- Screenshot: https://shadcn.exchange/shot/keyframeui/skyline-hero/light.png
- Registry item: https://keyframeui.com/r/skyline-hero.json
- Categories: blocks, hero-sections
- Listed: 2026-11-02, source last changed 2026-11-02

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/keyframeui/skyline-hero.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://keyframeui.com/r/dot-rail.json`, `https://keyframeui.com/r/skyline.json`

## Notes from the registry

Live preview and the components it's built with: https://keyframeui.com/blocks/hero-sections/skyline-hero

## Files

### `registry/blocks/skyline-hero.tsx`

```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>
  );
}
```
