# Waitlist

> A waitlist that is an actual line: you step in at the end with your number over your head, and every friend who joins lets you hop past the person ahead.

A waitlist is usually an email field and a number in an email. This one puts you in a line. A green door stands at the left of the lit stage and a line of illustrated people waits for it; the spot at the end is a dashed outline that says You?. Join with your email and you step into it, lit, with your place rolling into the chip over your head. Every time a friend joins through your link you hop over the person ahead of you: you swap places mid-air, they wander off, the line closes up behind a newcomer at the door and your number rolls down. When you’re let in, the door swings open and you walk through. Underneath it is the whole waitlist: email validation, onJoin for your list, the invite link with a copy button, and position, referrals and admitted props that your server can update whenever things change.

## Install

```bash
npx shadcn@latest add https://hairlineui.com/r/waitlist.json
```

Needs React 19, Tailwind CSS v4 and shadcn/ui theme variables (any style, light and dark). Icons from lucide-react.

- npm: none
- Hairline UI components it needs (the CLI installs them too): `copy-button`, `text-morph`, `input`, `number-roll`, `status-button`
- Docs and live preview: https://hairlineui.com/ui/waitlist
- Registry item (source): https://hairlineui.com/r/waitlist.json

## Usage

```tsx
import { Waitlist } from "@/components/waitlist";

<Waitlist
  product="Relay"
  onJoin={(email) => fetch("/api/waitlist", { method: "POST", body: JSON.stringify({ email }) }).then((r) => r.json())}
/>
```

## Keeping the place live

Return the place from your API on join, then poll it so referrals move people up while they watch.

```tsx
"use client";
import { useEffect, useState } from "react";
import { Waitlist } from "@/components/waitlist";

export function LaunchWaitlist() {
  const [me, setMe] = useState<{ id: string; position: number; referrals: number; admitted: boolean } | null>(null);

  useEffect(() => {
    if (!me) return;
    const t = setInterval(async () => {
      const res = await fetch(`/api/waitlist/${me.id}`);
      if (res.ok) setMe(await res.json());
    }, 10000);
    return () => clearInterval(t);
  }, [me?.id]);

  return (
    <Waitlist
      product="Relay"
      position={me?.position}
      referrals={me?.referrals}
      admitted={me?.admitted}
      onJoin={async (email) => {
        const res = await fetch("/api/waitlist", { method: "POST", body: JSON.stringify({ email }) });
        if (!res.ok) throw new Error((await res.json()).message ?? "Couldn’t add you");
        const joined = await res.json(); // { id, position, referrals, admitted, link }
        setMe(joined);
        return { position: joined.position, link: joined.link };
      }}
    />
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `onJoin` | `(email: string) => Promise<{ position: number; link?: string }>` | – | Add the email; return their place and invite link. Throw an Error to show its message. |
| `position` | `number` | – | Their place after joining, from your server; every drop plays a hop. |
| `referrals` | `number` | `0` | Friends who have joined through their link. |
| `perFriend` | `number` | `50` | Places each friend moves them up, for the copy and the number of hops. |
| `admitted` | `boolean` | `false` | Let in: the door opens and they walk through. |
| `product` | `string` | `"early access"` | What they’re waiting for, in the copy. |

## States

- **line**: A green door at the left of the lit stage, five illustrated people waiting in profile, and a dashed outline at the end with a You? chip; “Join the line”.
- **join**: Join → Joining → Joined; the outline fades as you rise into the line, lit, with your number rolling into the chip; the form folds into “You’re on the list”.
- **skip**: position drops: you leap (520ms arc) over the person ahead as you swap places, they slide back and fade, the line steps up one place behind a newcomer at the door, and the number rolls down; “Excuse me, coming through”, then “Skipped 1 ahead”.
- **link**: The invite link sits in a pill with Copy link; “Each friend who joins moves you up 50 places. 2 friends so far.”
- **admitted**: The door swings open (crossfade to the lit doorway) and you walk to it and fade through; “You’re in”.
- **refused**: A bad email or a throw from onJoin: the field shakes and shows the reason.

## What it handles

Most waitlists take an email and go quiet. This block shows people where they stand, rewards every invite on the spot, and has a moment for being let in.

- **A place you can see**: Your position rolls in a chip over your head in the line.
- **Invites that move you**: Each drop in position plays a hop past the person ahead (up to three in a row), and the number rolls down.
- **Your server’s numbers**: position, referrals and admitted are props: update them from a poll, a socket or a page load.
- **The invite link**: onJoin can return a link, shown with a copy button.
- **Email checks**: An obviously wrong address is caught in place; onJoin can throw to say why it was refused.
- **Let in**: admitted opens the door and walks you through it.

## Replaces

a waitlist form, a position counter, a referral link with copy, a referral progress line, an admitted state

## Where it fits

- **Launch pages**: The waitlist under a pre-launch hero, where the line is the pitch.
- **Referral loops**: Invite links that visibly move people up.
- **Beta access**: Let people in batches and show them the door opening.
- **Drops and releases**: Limited releases where your place in line matters.

## Notes

- The email field is labelled and validated in place; Join is a submit button, so Enter works. A status region says the place in line whenever it changes.
- The line is decoration (aria-hidden). The people and the door are illustrations shipped inside queue-art.ts as WebP data URIs (55KB of images), so the install is complete; the hops, the line closing up and the door are code.
- The street is drawn at 364 × 176 and scaled to the stage; people move with transforms only.
- With reduced motion the numbers change in place and nobody hops or walks.
- Built from Copy button, Input, Number roll, Status button and Text morph, which the CLI installs with it. No animation library.

License: MIT (https://hairlineui.com/license#components)
