Skip to content

IrisSectionsColonnade Beacon

Waitlist anatomy, over Colonnade

Get early access

Pill, claim, email field, countdown: the whole waitlist hero, framed by pillars that reach for your cursor.

Demo launch, 30 days out

--Days
--Hours
--Minutes
--Seconds
field

Colonnade Beacon

The anatomy `colonnade` was measured from, rebuilt around it: an eyebrow pill, one short claim, one line of support copy, an email field with its button inside the same pill-shaped track, and a four-unit countdown underneath. Everything sits in the centre the two pillar banks leave clear, so nothing has to fight the field for contrast.

Honest by construction. There's no waitlist behind this page, so the form says so when you submit it rather than pretending to sign you up. The reference's "700+ signups" avatar row is dropped instead of invented, and the countdown is labelled a demo, counting to a date 30 days from when the page opened. Move the cursor over either bank: the pillars reach for it, and their twins on the far side answer.

  • background
  • one-column
  • centered
  • waitlist
  • form
  • countdown
Family
field
Status
Available
Licence
Free

Install

No installation needed — self-contained, paste-in code.

Usage

Drop it straight into a page.

example.tsx
"use client";

import { useEffect, useState } from "react";
import { ColonnadeField } from "./ColonnadeField";

const LAUNCH = new Date("2026-12-01T00:00:00Z").getTime(); // your launch date

export default function Example() {
  const [left, setLeft] = useState<number | null>(null);
  useEffect(() => {
    const tick = () => setLeft(Math.max(LAUNCH - Date.now(), 0));
    tick();
    const id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, []);
  const s = Math.floor((left ?? 0) / 1000);
  const units = [
    ["Days", Math.floor(s / 86400)],
    ["Hours", Math.floor((s % 86400) / 3600)],
    ["Minutes", Math.floor((s % 3600) / 60)],
    ["Seconds", s % 60],
  ] as const;

  return (
    <header className="relative isolate min-h-[40rem] overflow-hidden bg-[#0b0908] flex flex-col items-center justify-center text-center">
      <div className="absolute inset-0 -z-10">
        <ColonnadeField />
      </div>
      <div className="mx-auto max-w-xl px-8 text-[#f5f5f5]">
        <span className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3.5 py-1.5 text-sm text-white/80">
          <span className="h-2 w-2 rounded-[3px] bg-[#f26a1b]" />
          Early access opens soon
        </span>
        <h1 className="mt-5 text-6xl font-medium tracking-tight">Get early access</h1>
        <p className="mt-5 text-white/75">One line on what people get for signing up early.</p>
        <form
          action="/api/waitlist" // point at your own list
          method="post"
          className="mx-auto mt-9 flex max-w-md items-center gap-2 rounded-full border border-white/10 bg-[#1a1716]/80 p-1.5 pl-5"
        >
          <label htmlFor="email" className="sr-only">Email address</label>
          <input id="email" name="email" type="email" required placeholder="Enter your best email…" className="min-w-0 flex-1 bg-transparent outline-none placeholder:text-white/45" />
          <button className="rounded-full border border-[#f26a1b]/60 bg-[#f26a1b]/15 px-5 py-2.5 text-sm font-medium text-[#ffb27a]">
            Join waitlist
          </button>
        </form>
        <div className="mt-10 flex justify-center gap-10" role="timer">
          {units.map(([label, value]) => (
            <div key={label} className="flex flex-col items-center gap-1.5">
              <span className="text-3xl tabular-nums">{left == null ? "--" : String(value).padStart(2, "0")}</span>
              <span className="text-xs uppercase tracking-widest text-white/50">{label}</span>
            </div>
          ))}
        </div>
      </div>
    </header>
  );
}

Component

The real source, exactly as it ships — multiple files, kept together.

"use client";

import Link from "next/link";
import { useEffect, useId, useState, type FormEvent } from "react";
import type { CategoryDef, CreationCard } from "@/lib/iris";
import { ColonnadeField } from "../visuals/backgrounds";

/**
 * The real `colonnade-beacon` section, full scale — the waitlist-hero
 * anatomy `colonnade` itself was measured from: an eyebrow pill, one short
 * claim, one line of support copy, an inline email field with its button,
 * and a live countdown underneath, all held in the empty centre the two
 * pillar banks leave clear.
 *
 * Honest by construction (product-spec.md §3.1 / iris-dna.md's Honesty
 * rule): there's no waitlist behind this page, so the form says so on
 * submit instead of pretending to sign anyone up; the reference's "700+
 * signups" avatar row is dropped rather than invented; and the countdown
 * is labelled a demo, counting to a date 30 days from when the page opened.
 */

const DEMO_DAYS = 30;

function useCountdown(days: number) {
  const [left, setLeft] = useState<number | null>(null);

  useEffect(() => {
    /* Set on mount, not at render, so the server and client never disagree
       about the clock. */
    const target = Date.now() + days * 86_400_000;
    const tick = () => setLeft(Math.max(target - Date.now(), 0));
    tick();
    const id = window.setInterval(tick, 1000);
    return () => window.clearInterval(id);
  }, [days]);

  if (left == null) return null;
  const s = Math.floor(left / 1000);
  return {
    days: Math.floor(s / 86_400),
    hours: Math.floor((s % 86_400) / 3600),
    minutes: Math.floor((s % 3600) / 60),
    seconds: s % 60,
  };
}

export function ColonnadeBeacon({
  category,
  creation,
}: {
  category: CategoryDef;
  creation: CreationCard;
}) {
  const emailId = useId();
  const statusId = useId();
  const [sent, setSent] = useState(false);
  const left = useCountdown(DEMO_DAYS);

  const onSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setSent(true);
  };

  const units = [
    { label: "Days", value: left?.days },
    { label: "Hours", value: left?.hours },
    { label: "Minutes", value: left?.minutes },
    { label: "Seconds", value: left?.seconds },
  ];

  return (
    <header className="iris-hero iris-hero--field relative isolate [overflow:clip] mt-[calc(-3.5rem_-_1px)] min-h-[min(92vh,820px)] flex flex-col justify-center [padding-block:calc(clamp(3rem,8vh,6rem)+3.5rem)_clamp(4rem,10vh,7rem)]">
      <div className="absolute inset-0 z-[-1] overflow-hidden bg-[oklch(0.1_0.004_50)]" aria-hidden="true">
        <ColonnadeField />
      </div>

      <div className="iris-shell flex flex-col items-center relative text-center">
        <p className="flex flex-wrap items-center gap-[0.4rem] text-[length:0.8125rem] text-[var(--field-ink-dim)] [&_a]:text-inherit [&_a:hover]:text-[color:var(--field-ink)]">
          <Link href="/iris">Iris</Link>
          <span aria-hidden="true">/</span>
          <Link href={`/iris/${category.slug}`}>{category.label}</Link>
          <span aria-hidden="true">/</span>
          <span>{creation.title}</span>
        </p>

        <span className="mt-6 inline-flex items-center gap-2 rounded-[var(--iris-radius-pill)] border border-[oklch(1_0_0_/_0.1)] bg-[oklch(1_0_0_/_0.04)] px-3.5 py-1.5 text-[length:0.8125rem] font-medium text-[color:var(--field-ink-dim)]">
          <span className="h-2 w-2 rounded-[3px] bg-[oklch(0.7_0.19_45)] shadow-[0_0_10px_oklch(0.7_0.19_45_/_0.8)]" aria-hidden="true" />
          Waitlist anatomy, over Colonnade
        </span>

        <h1 className="mt-5 font-[family-name:var(--font-iris-display)] font-normal text-[length:clamp(2.75rem,1.6rem+5vw,5.5rem)] leading-[1] tracking-[-0.03em] text-balance text-[color:var(--field-ink)]">
          Get early access
        </h1>
        <p className="mt-5 max-w-[44ch] text-[length:1.0625rem] leading-[1.55] text-[color:var(--field-ink-dim)]">
          Pill, claim, email field, countdown: the whole waitlist hero, framed
          by pillars that reach for your cursor.
        </p>

        <form
          onSubmit={onSubmit}
          className="mt-9 flex w-full max-w-[27rem] items-center gap-2 rounded-[var(--iris-radius-pill)] border border-[oklch(1_0_0_/_0.08)] bg-[oklch(0.16_0.004_50_/_0.8)] p-1.5 pl-5 focus-within:border-[oklch(0.7_0.19_45_/_0.6)]"
        >
          <label htmlFor={emailId} className="sr-only">
            Email address
          </label>
          <input
            id={emailId}
            type="email"
            required
            autoComplete="email"
            placeholder="Enter your best email…"
            aria-describedby={statusId}
            onChange={() => setSent(false)}
            className="min-w-0 flex-1 bg-transparent text-[length:0.9375rem] text-[color:var(--field-ink)] placeholder:text-[oklch(0.98_0.004_90_/_0.45)] outline-none"
          />
          <button
            type="submit"
            className="shrink-0 rounded-[var(--iris-radius-pill)] border border-[oklch(0.7_0.19_45_/_0.55)] bg-[oklch(0.3_0.09_45_/_0.35)] px-5 py-2.5 text-[length:0.875rem] font-medium leading-none text-[oklch(0.82_0.14_55)] transition-[background-color,border-color] duration-[120ms] hover:bg-[oklch(0.38_0.12_45_/_0.5)] hover:border-[oklch(0.75_0.19_50)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[oklch(0.75_0.19_50)]"
          >
            Join waitlist
          </button>
        </form>
        <p id={statusId} aria-live="polite" className="mt-3 min-h-[1.25rem] text-[length:0.8125rem] text-[color:var(--field-ink-dim)]">
          {sent ? "Demo only: nothing was sent. Point this form at your own list." : ""}
        </p>

        <div className="mt-8 flex flex-col items-center gap-3">
          <p className="text-[length:0.6875rem] font-medium uppercase tracking-[0.14em] text-[oklch(0.98_0.004_90_/_0.5)]">
            Demo launch, {DEMO_DAYS} days out
          </p>
          <div className="flex items-start gap-8 sm:gap-12" role="timer" aria-live="off">
            {units.map((unit) => (
              <div key={unit.label} className="flex min-w-[3.25rem] flex-col items-center gap-1.5">
                <span className="font-[family-name:var(--font-iris-display)] text-[length:clamp(1.5rem,1.1rem+1.4vw,2.125rem)] leading-none tabular-nums tracking-[-0.02em] text-[color:var(--field-ink)]">
                  {unit.value == null ? "--" : String(unit.value).padStart(2, "0")}
                </span>
                <span className="text-[length:0.6875rem] uppercase tracking-[0.12em] text-[oklch(0.98_0.004_90_/_0.5)]">
                  {unit.label}
                </span>
              </div>
            ))}
          </div>
        </div>
      </div>
    </header>
  );
}

More sections

Start with a background.

The backgrounds are the most finished corner of the catalogue. Take one and drop it under your own copy.

Browse backgrounds