Web InnoventixFreeCode

Filled Accordion

Original · free

Filled, tinted accordion panels; the active one is highlighted.

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

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

type Item = {
  id: string;
  question: string;
  answer: string;
  tint: "indigo" | "violet" | "emerald" | "amber";
};

const ITEMS: Item[] = [
  {
    id: "billing",
    question: "How does billing work after the free trial ends?",
    answer:
      "Your card is charged on the day the 14-day trial closes, then monthly on that same date. We email a receipt every cycle and you can switch to annual billing anytime to save two months.",
    tint: "indigo",
  },
  {
    id: "seats",
    question: "Can I add or remove teammates mid-cycle?",
    answer:
      "Yes. Add a seat and you are prorated for the days remaining in the current month. Remove one and the credit is applied to your next invoice automatically — no support ticket required.",
    tint: "violet",
  },
  {
    id: "export",
    question: "Do I own my data, and can I export it?",
    answer:
      "Every project you create is yours. Export a full JSON or CSV archive from Settings → Data at any time, including deleted items kept in the 30-day recovery window.",
    tint: "emerald",
  },
  {
    id: "cancel",
    question: "What happens to my content if I cancel?",
    answer:
      "Your workspace switches to read-only at the end of the paid period. Nothing is deleted for 90 days, so you can reactivate and pick up exactly where you left off.",
    tint: "amber",
  },
];

const TINTS: Record<
  Item["tint"],
  {
    idle: string;
    active: string;
    ring: string;
    dot: string;
    chevron: string;
    heading: string;
  }
> = {
  indigo: {
    idle: "border-indigo-200/70 bg-indigo-50/60 hover:bg-indigo-50 dark:border-indigo-400/20 dark:bg-indigo-500/10 dark:hover:bg-indigo-500/[0.15]",
    active:
      "border-indigo-300 bg-indigo-100/80 shadow-indigo-500/10 dark:border-indigo-400/40 dark:bg-indigo-500/20",
    ring: "focus-visible:ring-indigo-500/60",
    dot: "bg-indigo-500",
    chevron: "text-indigo-600 dark:text-indigo-300",
    heading: "text-indigo-950 dark:text-indigo-50",
  },
  violet: {
    idle: "border-violet-200/70 bg-violet-50/60 hover:bg-violet-50 dark:border-violet-400/20 dark:bg-violet-500/10 dark:hover:bg-violet-500/[0.15]",
    active:
      "border-violet-300 bg-violet-100/80 shadow-violet-500/10 dark:border-violet-400/40 dark:bg-violet-500/20",
    ring: "focus-visible:ring-violet-500/60",
    dot: "bg-violet-500",
    chevron: "text-violet-600 dark:text-violet-300",
    heading: "text-violet-950 dark:text-violet-50",
  },
  emerald: {
    idle: "border-emerald-200/70 bg-emerald-50/60 hover:bg-emerald-50 dark:border-emerald-400/20 dark:bg-emerald-500/10 dark:hover:bg-emerald-500/[0.15]",
    active:
      "border-emerald-300 bg-emerald-100/80 shadow-emerald-500/10 dark:border-emerald-400/40 dark:bg-emerald-500/20",
    ring: "focus-visible:ring-emerald-500/60",
    dot: "bg-emerald-500",
    chevron: "text-emerald-600 dark:text-emerald-300",
    heading: "text-emerald-950 dark:text-emerald-50",
  },
  amber: {
    idle: "border-amber-200/70 bg-amber-50/60 hover:bg-amber-50 dark:border-amber-400/20 dark:bg-amber-500/10 dark:hover:bg-amber-500/[0.15]",
    active:
      "border-amber-300 bg-amber-100/80 shadow-amber-500/10 dark:border-amber-400/40 dark:bg-amber-500/20",
    ring: "focus-visible:ring-amber-500/60",
    dot: "bg-amber-500",
    chevron: "text-amber-600 dark:text-amber-300",
    heading: "text-amber-950 dark:text-amber-50",
  },
};

export default function AccordionFilled() {
  const [openId, setOpenId] = useState<string | null>(ITEMS[0].id);
  const baseId = useId();
  const btnRefs = useRef<Array<HTMLButtonElement | null>>([]);

  const toggle = (id: string) => {
    setOpenId((cur) => (cur === id ? null : id));
  };

  const onKeyDown = (e: KeyboardEvent<HTMLButtonElement>, index: number) => {
    const last = ITEMS.length - 1;
    let next = -1;
    if (e.key === "ArrowDown") next = index === last ? 0 : index + 1;
    else if (e.key === "ArrowUp") next = index === 0 ? last : index - 1;
    else if (e.key === "Home") next = 0;
    else if (e.key === "End") next = last;
    if (next < 0) return;
    e.preventDefault();
    btnRefs.current[next]?.focus();
  };

  return (
    <section className="relative w-full bg-white px-4 py-20 sm:px-6 sm:py-28 dark:bg-slate-950">
      <style>{`
        @keyframes accfilled_fade {
          from { opacity: 0; transform: translateY(-4px); }
          to { opacity: 1; transform: translateY(0); }
        }
        .accfilled-panel[data-open="true"] .accfilled-inner {
          animation: accfilled_fade 0.32s cubic-bezier(0.22, 1, 0.36, 1);
        }
        @media (prefers-reduced-motion: reduce) {
          .accfilled-panel { transition: none !important; }
          .accfilled-panel[data-open="true"] .accfilled-inner { animation: none !important; }
          .accfilled-chevron { transition: none !important; }
        }
      `}</style>

      <div className="mx-auto max-w-2xl">
        <div className="mb-10 text-center">
          <span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-medium tracking-wide text-slate-600 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-400">
            <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
            Billing &amp; accounts
          </span>
          <h2 className="mt-4 text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-white">
            Questions, answered
          </h2>
          <p className="mt-3 text-base text-slate-500 dark:text-slate-400">
            Everything about trials, seats, and your data — one panel at a time.
          </p>
        </div>

        <div className="flex flex-col gap-3">
          {ITEMS.map((item, i) => {
            const open = openId === item.id;
            const t = TINTS[item.tint];
            const headingId = `${baseId}-h-${item.id}`;
            const panelId = `${baseId}-p-${item.id}`;
            return (
              <div
                key={item.id}
                data-open={open}
                className={[
                  "accfilled-panel rounded-2xl border shadow-sm transition-colors duration-300",
                  open ? t.active : t.idle,
                ].join(" ")}
              >
                <h3 id={headingId} className="m-0">
                  <button
                    type="button"
                    ref={(el) => {
                      btnRefs.current[i] = el;
                    }}
                    aria-expanded={open}
                    aria-controls={panelId}
                    onClick={() => toggle(item.id)}
                    onKeyDown={(e) => onKeyDown(e, i)}
                    className={[
                      "flex w-full items-center gap-4 rounded-2xl px-5 py-4 text-left outline-none transition",
                      "focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950",
                      t.ring,
                    ].join(" ")}
                  >
                    <span
                      aria-hidden="true"
                      className={[
                        "h-2 w-2 shrink-0 rounded-full transition-transform duration-300",
                        t.dot,
                        open ? "scale-125" : "scale-100 opacity-60",
                      ].join(" ")}
                    />
                    <span
                      className={[
                        "flex-1 text-base font-medium leading-snug",
                        open ? t.heading : "text-slate-800 dark:text-slate-200",
                      ].join(" ")}
                    >
                      {item.question}
                    </span>
                    <svg
                      aria-hidden="true"
                      viewBox="0 0 24 24"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth={2}
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      className={[
                        "accfilled-chevron h-5 w-5 shrink-0 transition-transform duration-300 ease-out",
                        t.chevron,
                        open ? "rotate-180" : "rotate-0",
                      ].join(" ")}
                    >
                      <path d="m6 9 6 6 6-6" />
                    </svg>
                  </button>
                </h3>

                <div
                  id={panelId}
                  role="region"
                  aria-labelledby={headingId}
                  hidden={!open}
                  className="accfilled-inner px-5 pb-5"
                >
                  <div className="pl-6">
                    <p className="text-sm leading-relaxed text-slate-600 dark:text-slate-300">
                      {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 →