Counter Curtain

A curtain that covers the page while it loads, counting from 0 to 100 with the fonts and hero images, then lifts as the headline rises out of its masks underneath, on one clock.

  • Preloader
  • Hero
  • Text

Uses clsx, tailwind-merge

Open in ChatGPTOpen in Claude
okto.studio
Okto°Contact
Independent design studio, Oslo

Brands thatmove beforethey speak.

Identity, websites and motion for companies that would rather be felt than explained.See the work

Installation

Add the component on its own with the CLI, or copy the file in by hand.

The CLI writes components/ui/counter-curtain.tsx and installs clsx and tailwind-merge. It needs the @keyframeui registry in components.json; add it once per project.

npx shadcn@latest add @keyframeui/counter-curtain

Examples

Finished sections built with Counter Curtain. Each one installs whole, with the component included.

Launch Page

A custom counter in the drop's colors, with a padded number and a progress bar, on a curtain that falls away. `onComplete` starts the countdown as it goes, so the clock is already ticking when it's uncovered.

volt.run/drop-04
VOLTDrop 04 · 500 pairs

VoltTrainer

A flat, grippy sole and a heel that locks in for lifts. In lime, for one weekend only.

Drops in02d14h09m31s

Editorial

The default counter set in serif on a stone curtain that slides off to the left. It waits for the three photos to load and decode, so they're sharp from the first frame.

hollow-house.journal
Hollow HouseIssue 12 · Autumn

Rooms thathold thelight.

A 1930s terrace in Bergen, opened up from the front door to the garden and furnished one chair at a time.

Props

children

TypeReact.ReactNode

The hero. Wrap each line of the headline, and anything else that should rise in, in CounterCurtainLine. Lines rise as the curtain's edge passes them, a beat apart, in reading order.

minDuration

TypenumberDefault0.8

Shortest the count can take, in seconds, even when everything has already loaded. Keep it under a second or so: it's a first impression, not a wait.

maxDuration

TypenumberDefault3

Longest it waits for loading, in seconds, before lifting anyway. While it waits, the count creeps on its own so it never looks stuck. A click, key press or scroll lands it within a third of a second.

waitFor

Type("fonts" | "images")[]Default["fonts", "images"]

What the count follows: web fonts, and the images in the visible part of the hero, decoded so the reveal doesn't stutter on them. [] makes it a plain timed count.

once

TypebooleanDefaulttrue

Play once per browser session. Reloads, the back button and other pages that use it go straight to the content.

storageKey

TypestringDefault"counter-curtain"

The sessionStorage key that remembers it played. Curtains with the same key share it.

direction

Type"up" | "down" | "left" | "right"Default"up"

The way the curtain leaves. The lines always rise.

counter

TypeReact.ReactNode

What the curtain shows in place of the default counter. Put <CounterCurtainValue pad={3} /> where the number goes, and use var(--counter-progress), from 0 to 1, for bars, like scale-x-[var(--counter-progress)].

onComplete

Type() => void

Called when the count reaches 100 and the curtain starts to lift, or straight away when it's skipped. Start a background video here, so it's already playing as it's uncovered.

contained

TypebooleanDefaultfalse

Cover this element instead of the screen, and leave the page's scroll alone. It starts counting once it's on screen. For a single section, or a preview like the ones here.

curtainClassName

Typestring

Classes for the curtain, such as its color and font.

className

Typestring

Classes for the element that wraps the hero. Other props pass through to it.

nonce

Typestring

Your Content Security Policy's nonce, for the small inline script that puts the curtain up before the page is drawn. If the script is blocked, the curtain is skipped.