Web InnoventixFreeCode

Numbered Accordion

Original · free

A numbered-step accordion (01, 02, 03...).

byWeb InnoventixReact + Tailwind
accordionnumberedaccordions
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/accordion-numbered.json
accordion-numbered.tsx
"use client";

import { useId, useState, type KeyboardEvent } from "react";

type Step = {
  title: string;
  body: string;
  meta: string;
};

const STEPS: Step[] = [
  {
    title: "Discovery & scope",
    meta: "Week 1",
    body: "We map your goals, audience and constraints in a single working session, then translate them into a written brief with success metrics everyone signs off on. No build starts until the scope is unambiguous.",
  },
  {
    title: "Wireframes & flow",
    meta: "Week 1–2",
    body: "Low-fidelity screens establish structure, navigation and content hierarchy before a single pixel is styled. You review clickable flows early, so layout debates happen when they are cheap to resolve.",
  },
  {
    title: "Visual design",
    meta: "Week 2–3",
    body: "We apply your brand into a responsive design system: type scale, colour tokens, spacing and reusable components. Every screen is delivered in both light and dark, ready for handoff.",
  },
  {
    title: "Build & integrate",
    meta: "Week 3–5",
    body: "Front-end is built as accessible, component-driven code and wired to your CMS or APIs. We commit in small, reviewable increments with a staging URL you can open at any time.",
  },
  {
    title: "QA & launch",
    meta: "Week 5–6",
    body: "Cross-browser testing, Lighthouse passes and an accessibility audit run before go-live. We ship behind a checklist, then monitor the first 48 hours and hand over full documentation.",
  },
];

export default function AccordionNumbered() {
  const [open, setOpen] = useState<number | null>(0);
  const baseId = useId();

  const toggle = (i: number) => setOpen((cur) => (cur === i ? null : i));

  const onKey = (e: KeyboardEvent<HTMLButtonElement>, i: number) => {
    if (e.key === "ArrowDown" || e.key === "ArrowUp") {
      e.preventDefault();
      const dir = e.key === "ArrowDown" ? 1 : -1;
      const next = (i + dir + STEPS.length) % STEPS.length;
      const el = document.getElementById(`${baseId}-btn-${next}`);
      el?.focus();
    } else if (e.key === "Home") {
      e.preventDefault();
      document.getElementById(`${baseId}-btn-0`)?.focus();
    } else if (e.key === "End") {
      e.preventDefault();
      document.getElementById(`${baseId}-btn-${STEPS.length - 1}`)?.focus();
    }
  };

  return (
    <section className="relative w-full bg-white px-5 py-20 text-slate-900 sm:px-8 sm:py-28 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        @keyframes accnum-reveal {
          from { opacity: 0; transform: translateY(-4px); }
          to { opacity: 1; transform: translateY(0); }
        }
        @media (prefers-reduced-motion: reduce) {
          .accnum-panel-inner { animation: none !important; }
          .accnum-chevron { transition: none !important; }
          .accnum-grid { transition: none !important; }
        }
      `}</style>

      <div className="mx-auto max-w-2xl">
        <div className="mb-10 sm:mb-14">
          <span className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-medium tracking-wide text-indigo-700 dark:border-indigo-400/20 dark:bg-indigo-400/10 dark:text-indigo-300">
            <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
            How we work
          </span>
          <h2 className="mt-4 text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-white">
            A five-step path to launch
          </h2>
          <p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-400">
            Each engagement follows the same predictable rhythm. Expand any
            step to see exactly what happens and when.
          </p>
        </div>

        <div className="flex flex-col gap-3">
          {STEPS.map((step, i) => {
            const isOpen = open === i;
            const num = String(i + 1).padStart(2, "0");
            return (
              <div
                key={step.title}
                className={`group overflow-hidden rounded-2xl border transition-colors ${
                  isOpen
                    ? "border-indigo-300 bg-indigo-50/40 dark:border-indigo-400/30 dark:bg-indigo-400/5"
                    : "border-slate-200 bg-slate-50/60 hover:border-slate-300 dark:border-slate-800 dark:bg-slate-900/40 dark:hover:border-slate-700"
                }`}
              >
                <h3>
                  <button
                    id={`${baseId}-btn-${i}`}
                    type="button"
                    aria-expanded={isOpen}
                    aria-controls={`${baseId}-panel-${i}`}
                    onClick={() => toggle(i)}
                    onKeyDown={(e) => onKey(e, i)}
                    className="flex w-full items-center gap-4 rounded-2xl px-4 py-4 text-left outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white sm:px-5 dark:focus-visible:ring-offset-slate-950"
                  >
                    <span
                      aria-hidden="true"
                      className={`flex h-11 w-11 flex-none items-center justify-center rounded-xl text-sm font-semibold tabular-nums transition-colors ${
                        isOpen
                          ? "bg-indigo-600 text-white dark:bg-indigo-500"
                          : "bg-white text-slate-500 ring-1 ring-slate-200 group-hover:text-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:ring-slate-700 dark:group-hover:text-slate-200"
                      }`}
                    >
                      {num}
                    </span>

                    <span className="min-w-0 flex-1">
                      <span className="block truncate text-base font-medium text-slate-900 dark:text-white">
                        {step.title}
                      </span>
                      <span className="mt-0.5 block text-xs font-medium text-slate-500 dark:text-slate-400">
                        {step.meta}
                      </span>
                    </span>

                    <svg
                      aria-hidden="true"
                      viewBox="0 0 24 24"
                      className={`accnum-chevron h-5 w-5 flex-none text-slate-400 transition-transform duration-300 ${
                        isOpen ? "rotate-180 text-indigo-600 dark:text-indigo-400" : ""
                      }`}
                      fill="none"
                      stroke="currentColor"
                      strokeWidth="2"
                      strokeLinecap="round"
                      strokeLinejoin="round"
                    >
                      <path d="m6 9 6 6 6-6" />
                    </svg>
                  </button>
                </h3>

                <div
                  id={`${baseId}-panel-${i}`}
                  role="region"
                  aria-labelledby={`${baseId}-btn-${i}`}
                  aria-hidden={!isOpen}
                  className="accnum-grid grid transition-[grid-template-rows] duration-300 ease-out"
                  style={{ gridTemplateRows: isOpen ? "1fr" : "0fr" }}
                >
                  <div className="overflow-hidden">
                    <div
                      className="accnum-panel-inner pl-[3.75rem] pr-4 pb-5 sm:pr-5"
                      style={{ animation: "accnum-reveal 0.35s ease both" }}
                    >
                      <p className="text-sm leading-relaxed text-slate-600 dark:text-slate-300">
                        {step.body}
                      </p>
                    </div>
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </section>
  );
}

Dependencies

None (React + Tailwind only).

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →