Skip to content

CTA Countdown

Limited-offer CTA in an ink slab: uppercase offer headline, yellow claim button, dashed promo code, and a four-cell live countdown (days/hours/min/sec) with the first cell in yellow.

brutalist/section/cta-countdown
Open ↗

Source

"use client";

import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";

export interface CtaCountdownProps {
  kicker?: string;
  title?: string;
  description?: string;
  /** ISO date the offer ends. Defaults to 3 days after first render. */
  deadline?: string;
  cta?: { label: string; href: string };
  code?: string;
  className?: string;
}

function parts(ms: number): { label: string; value: string }[] {
  const s = Math.max(0, Math.floor(ms / 1000));
  return [
    { label: "Days", value: String(Math.floor(s / 86400)).padStart(2, "0") },
    { label: "Hours", value: String(Math.floor((s % 86400) / 3600)).padStart(2, "0") },
    { label: "Min", value: String(Math.floor((s % 3600) / 60)).padStart(2, "0") },
    { label: "Sec", value: String(s % 60).padStart(2, "0") },
  ];
}

export function CtaCountdown({
  kicker = "[ Launch week ]",
  title = "40% off Team, this week only.",
  description = "Celebrating v4.2 and AI summaries. Lock the price for your first year — applies to every project you add.",
  deadline,
  cta = { label: "Claim 40% off", href: "#pricing" },
  code = "SHIPIT40",
  className,
}: CtaCountdownProps) {
  const [end] = useState(() => (deadline ? new Date(deadline).getTime() : Date.now() + 3 * 86400000 + 7 * 3600000));
  const [now, setNow] = useState<number | null>(null);

  useEffect(() => {
    const tick = () => setNow(Date.now());
    const first = window.setTimeout(tick, 0);
    const id = window.setInterval(tick, 1000);
    return () => {
      window.clearTimeout(first);
      window.clearInterval(id);
    };
  }, []);

  // Render placeholders on the server / first paint to avoid hydration mismatch.
  const blocks = now === null ? parts(0).map((p) => ({ ...p, value: "--" })) : parts(end - now);
  const ended = now !== null && end - now <= 0;

  return (
    <section className={cn("da-section da-stroke-b bg-da-bg text-da-fg", className)}>
      <div className="da-container">
        <div className="da-stroke grid bg-da-fg text-da-bg shadow-da-lg lg:grid-cols-[1.2fr_1fr]">
          <div className="p-8 sm:p-12">
            <p className="font-da-mono text-xs font-bold tracking-da-label uppercase opacity-80">{kicker}</p>
            <h2 className="mt-4 font-da-display text-[clamp(2.25rem,5vw,4rem)] leading-[0.95] tracking-da-display uppercase">{title}</h2>
            <p className="mt-5 max-w-lg text-lg opacity-85">{description}</p>
            <div className="mt-8 flex flex-wrap items-center gap-4">
              <a
                href={cta.href}
                className="da-focus da-stroke bg-da-primary px-6 py-4 font-bold text-da-primary-fg [--da-border:var(--da-bg)] hover:bg-da-accent hover:text-da-accent-fg"
              >
                {cta.label}
              </a>
              <p className="font-da-mono text-sm">
                Code{" "}
                <span className="border-[length:var(--da-border-width)] border-dashed border-current px-2 py-1 font-bold">{code}</span>
              </p>
            </div>
          </div>
          <div className="grid grid-cols-4 border-t-[length:var(--da-border-width)] border-current lg:border-t-0 lg:border-l-[length:var(--da-border-width)]">
            <p className="sr-only" aria-live="polite">
              {ended ? "Offer ended" : now === null ? "" : `Offer ends in ${blocks.map((b) => `${b.value} ${b.label}`).join(", ")}`}
            </p>
            {blocks.map((b, i) => (
              <div
                key={b.label}
                aria-hidden
                className={cn("flex flex-col items-center justify-center gap-2 py-8", i > 0 && "border-l-[length:var(--da-border-width)] border-current", i === 0 && "bg-da-primary text-da-primary-fg")}
              >
                <span className="font-da-display text-[clamp(2.25rem,5vw,4rem)] leading-none tabular-nums">{b.value}</span>
                <span className="font-da-mono text-[11px] font-bold tracking-da-label uppercase">{b.label}</span>
              </div>
            ))}
          </div>
        </div>
      </div>
    </section>
  );
}

export default CtaCountdown;

modules/brutalist/section/cta-countdown/index.tsx

Props

PropTypeDefaultDescription
kicker / title / descriptionstring—Offer copy.
deadlinestringnow + 3 days 7 hoursISO date when the offer ends.
cta{ label: string; href: string }—Claim button.
codestring"SHIPIT40"Promo code shown next to the button.
classNamestring—Classes on the <section>.

Other cta variants in Brutalist

CTA in other art directions

Cta App

App download CTA in a glass panel: headline, ink store buttons and rating on the left; an HTML phone mock-up recording an in-person meeting (mic orb, waveform, AI chip) rising from the bottom on the right.

GlassCTA

Cta Gradient

Bold full-gradient CTA card with a masked grid overlay and top glow, white primary button, translucent secondary button and three frosted stat chips.

GlassCTA

CTA Orb

Large frosted panel with a slowly spinning, heavily blurred conic-gradient orb glowing through the glass, centered headline, ink + frosted CTAs and a reassurance line.

GlassCTA

Cta Waitlist

Frosted CTA panel haloed by a blurred gradient ring: headline, copy, pill email form with gradient button (success state) and three check perks.

GlassCTA

Cta Banner

Compact inverted ink banner on a single row: title and one-liner on the left, solid and outline actions on the right, with a faint grid fading in from the right edge.

MinimalCTA

CTA Glow

Closing call-to-action card: centered headline and copy, ink button with keyboard hint, ghost secondary link, an indigo glow rising from the bottom edge and a gradient hairline on top.

MinimalCTA

Cta Newsletter

Centered email capture card with an indigo hairline on top: headline, copy, email field and ink button, with client validation plus pending, success and error states.

MinimalCTA

Cta Split

Wide two-column CTA card: headline, copy and two buttons on the left; a tinted panel on the right with a numbered ‘first 10 minutes’ onboarding checklist and time estimates.

MinimalCTA

Cta Contact

Studio-style contact CTA: mono label, headline, the email address set huge as a link, and a row of contact channels (studio, press, careers) in mono under a hairline.

Mono CleanCTA

Cta Email

Underline-input signup: large headline, then an email field drawn as a single bottom rule with an inline “Get started →” button; confirmation replaces the form.

Mono CleanCTA

Cta Giant

Full-width CTA as a giant link: the whole headline is the link, an arrow slides in on hover and an ink underline wipes across; a mono note sits below.

Mono CleanCTA

Cta Split

Two-door CTA: the section is split into two equal halves (white | ink) with a mono label and a large title each; hovering a half inverts it and nudges the arrow.

Mono CleanCTA

Cta Band

Full-width primary-blue band with a faint grid pattern, headline + subline on the left and inverted white + outlined buttons on the right.

Neo CorporateCTA

Cta Demo

Book-a-demo CTA: benefits list on the left, bordered form card (work email, company, team size select) on the right with an inline success state.

Neo CorporateCTA

Cta Stats

Split CTA on a dark (fg-colored) panel: copy + buttons on the left, a 2×2 grid of large outcome stats divided by hairlines on the right.

Neo CorporateCTA

Cta Trust

Centered CTA inside a bordered panel: headline, two buttons, a line of compliance assurances with shield icons, and a strip of customer wordmarks below a hairline.

Neo CorporateCTA

Cta Blob

Closing CTA inside a large sage organic blob (SVG shape scaled to the content) with a clay pebble and two leaves around it; serif headline and cream pill buttons.

Organic SoftCTA

Cta Journal

Newsletter CTA styled like a paper journal: clay-tinted card with serif title, recent issue list with mono numbers, and a rounded email + button form with a warm thank-you state.

Organic SoftCTA

Cta Pledge

Interactive pledge CTA: pick the commitments your team is ready for (round check cards), a counter of companies who pledged, and a CTA that reflects how many you chose.

Organic SoftCTA

Cta Wave

Full-width sage band with soft wavy top and bottom edges (SVG), centered serif headline with an italic word, and two pill buttons; flows naturally between cream sections.

Organic SoftCTA

Cta App

App-download CTA on a peach block: headline and ink store buttons on the left, a flat white phone mock-up with a “Today” checklist (mint done items) rising from the bottom on the right.

Soft FlatCTA

Cta Block

Big periwinkle rounded block with pastel circles peeking from the corners, a centered white headline and two pill buttons (solid white and translucent).

Soft FlatCTA

Cta Illustrated

Split CTA in a white rounded card: headline, ink pill button and note on the left; a composition of pastel circle, pill, rotated square and a ✅ tile on the right.

Soft FlatCTA

Cta Newsletter

Newsletter CTA on a mint block: a flat envelope made of shapes with a notification badge, headline, pill email form and a cheerful success state.

Soft FlatCTA