Web InnoventixFreeCode

Flush Accordion

Original · free

A borderless, flush accordion separated by hairlines.

byWeb InnoventixReact + Tailwind
accordionflushaccordions
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-flush.json
accordion-flush.tsx
"use client";

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

type FlushItem = {
  question: string;
  answer: string;
};

const ITEMS: FlushItem[] = [
  {
    question: "How long does a typical website build take?",
    answer:
      "Most marketing sites ship in three to five weeks. We lock scope in week one, design and build in parallel, then reserve the final week for QA, accessibility passes, and a staged launch you sign off on before it goes live.",
  },
  {
    question: "Do you write the copy, or do we?",
    answer:
      "Either works. We can turn your rough notes into conversion-focused copy, or drop your existing text into a structure that actually guides visitors toward a decision. Most clients start with our draft and refine from there.",
  },
  {
    question: "Will the site be fast and pass Core Web Vitals?",
    answer:
      "Yes. Every build targets green Core Web Vitals on mobile. We ship optimized images, minimal JavaScript, and system-font fallbacks so the first paint lands well under a second on a mid-range phone.",
  },
  {
    question: "Can I edit the site myself after launch?",
    answer:
      "You get a lightweight editing setup and a short walkthrough recording. Change headlines, swap images, add case studies, or publish a blog post without touching code or waiting on us.",
  },
  {
    question: "What happens if something breaks after we go live?",
    answer:
      "Every launch includes 30 days of support at no extra cost. After that you can stay on a monthly care plan for updates, backups, and priority fixes, or take full ownership of the codebase, no strings attached.",
  },
];

function ChevronIcon({ open }: { open: boolean }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={
        "h-5 w-5 shrink-0 text-slate-400 transition-transform duration-300 ease-out group-hover:text-indigo-500 dark:text-slate-500 dark:group-hover:text-indigo-400 " +
        (open ? "-rotate-180" : "rotate-0")
      }
    >
      <path d="m6 9 6 6 6-6" />
    </svg>
  );
}

export default function AccordionFlush() {
  const [openIndex, setOpenIndex] = useState<number | null>(0);
  const uid = useId();
  const btnRefs = useRef<Array<HTMLButtonElement | null>>([]);

  const toggle = (index: number) => {
    setOpenIndex((current) => (current === index ? null : index));
  };

  const onKeyNav = (event: KeyboardEvent<HTMLButtonElement>, index: number) => {
    const count = ITEMS.length;
    let next: number | null = null;

    if (event.key === "ArrowDown") next = (index + 1) % count;
    else if (event.key === "ArrowUp") next = (index - 1 + count) % count;
    else if (event.key === "Home") next = 0;
    else if (event.key === "End") next = count - 1;

    if (next !== null) {
      event.preventDefault();
      btnRefs.current[next]?.focus();
    }
  };

  return (
    <section className="relative w-full bg-white px-6 py-20 sm:py-28 dark:bg-slate-950">
      <style>{`
        @media (prefers-reduced-motion: reduce) {
          .accflush_panel,
          .accflush_inner { transition: none !important; }
        }
      `}</style>

      <div className="mx-auto max-w-2xl">
        <div className="mb-12 text-center">
          <span className="inline-block rounded-full bg-indigo-50 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:bg-indigo-500/10 dark:text-indigo-300">
            FAQ
          </span>
          <h2 className="mt-5 text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-white">
            Questions, answered
          </h2>
          <p className="mt-4 text-base leading-relaxed text-slate-500 dark:text-slate-400">
            Everything you need to know before we start building. Still unsure?
            Reach out and we&rsquo;ll walk you through it.
          </p>
        </div>

        <div className="divide-y divide-slate-200 border-y border-slate-200 dark:divide-slate-800 dark:border-slate-800">
          {ITEMS.map((item, index) => {
            const open = openIndex === index;
            const headerId = `${uid}-h-${index}`;
            const panelId = `${uid}-p-${index}`;

            return (
              <div key={item.question}>
                <h3>
                  <button
                    type="button"
                    ref={(el) => {
                      btnRefs.current[index] = el;
                    }}
                    id={headerId}
                    aria-expanded={open}
                    aria-controls={panelId}
                    onClick={() => toggle(index)}
                    onKeyDown={(e) => onKeyNav(e, index)}
                    className="group flex w-full items-center justify-between gap-5 py-5 text-left outline-none transition-colors focus-visible:ring-2 focus-visible:ring-indigo-500/60 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950"
                  >
                    <span
                      className={
                        "text-base font-medium transition-colors sm:text-lg " +
                        (open
                          ? "text-indigo-600 dark:text-indigo-400"
                          : "text-slate-800 group-hover:text-slate-950 dark:text-slate-200 dark:group-hover:text-white")
                      }
                    >
                      {item.question}
                    </span>
                    <ChevronIcon open={open} />
                  </button>
                </h3>

                <div
                  id={panelId}
                  role="region"
                  aria-labelledby={headerId}
                  className={
                    "accflush_panel grid transition-[grid-template-rows] duration-300 ease-out " +
                    (open ? "grid-rows-[1fr]" : "grid-rows-[0fr]")
                  }
                >
                  <div className="overflow-hidden">
                    <p
                      className={
                        "accflush_inner pb-6 pr-8 text-[15px] leading-relaxed text-slate-600 transition-opacity duration-300 dark:text-slate-400 " +
                        (open ? "opacity-100" : "opacity-0")
                      }
                    >
                      {item.answer}
                    </p>
                  </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 →