Web InnoventixFreeCode

Icon Left Accordion

Original · free

Accordion with a leading icon per item and a right chevron.

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

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

type Item = {
  id: string;
  title: string;
  meta: string;
  body: string;
  icon: ReactNode;
  accent: string;
  accentDark: string;
  tint: string;
  tintDark: string;
};

const items: Item[] = [
  {
    id: "billing",
    title: "How does billing work after the trial ends?",
    meta: "Plans & payments",
    body: "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 between monthly and annual at any time from Settings, with the difference prorated to the cent.",
    accent: "text-indigo-600",
    accentDark: "dark:text-indigo-400",
    tint: "bg-indigo-50 ring-indigo-100",
    tintDark: "dark:bg-indigo-500/10 dark:ring-indigo-500/20",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-5 w-5">
        <rect x="2.5" y="5" width="19" height="14" rx="2.5" />
        <path d="M2.5 9.5h19" />
        <path d="M6.5 15h4" />
      </svg>
    ),
  },
  {
    id: "security",
    title: "Where is my data stored and who can see it?",
    meta: "Security & privacy",
    body: "Everything lives in encrypted storage in the EU region by default, with AES-256 at rest and TLS 1.3 in transit. Only workspace members you invite can read your data, and admins can revoke a session instantly. We never sell or train on your content.",
    accent: "text-emerald-600",
    accentDark: "dark:text-emerald-400",
    tint: "bg-emerald-50 ring-emerald-100",
    tintDark: "dark:bg-emerald-500/10 dark:ring-emerald-500/20",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-5 w-5">
        <path d="M12 3l7 3v5c0 4.4-3 7.6-7 9-4-1.4-7-4.6-7-9V6l7-3Z" />
        <path d="M9.2 12l1.9 1.9L15 10" />
      </svg>
    ),
  },
  {
    id: "team",
    title: "Can I add teammates and set their permissions?",
    meta: "Workspace & roles",
    body: "Invite unlimited teammates by email on every paid plan. Each person gets one of four roles — Owner, Admin, Editor, or Viewer — and you can scope access down to a single project. Pending invites expire after seven days for safety.",
    accent: "text-violet-600",
    accentDark: "dark:text-violet-400",
    tint: "bg-violet-50 ring-violet-100",
    tintDark: "dark:bg-violet-500/10 dark:ring-violet-500/20",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-5 w-5">
        <circle cx="9" cy="8" r="3.2" />
        <path d="M3.5 19a5.5 5.5 0 0 1 11 0" />
        <path d="M16 5.2a3.2 3.2 0 0 1 0 5.6" />
        <path d="M17.5 13.6A5.5 5.5 0 0 1 20.5 19" />
      </svg>
    ),
  },
  {
    id: "support",
    title: "What happens if I need help at 2 a.m.?",
    meta: "Support & response times",
    body: "Live chat is staffed 9-6 in your local time zone, and urgent tickets after hours are answered within four hours by an on-call engineer. Growth and Scale plans include a private Slack channel with a named contact who actually knows your setup.",
    accent: "text-amber-600",
    accentDark: "dark:text-amber-400",
    tint: "bg-amber-50 ring-amber-100",
    tintDark: "dark:bg-amber-500/10 dark:ring-amber-500/20",
    icon: (
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-5 w-5">
        <path d="M4 12a8 8 0 0 1 16 0v4.5a2.5 2.5 0 0 1-2.5 2.5H16" />
        <rect x="3" y="12" width="3.5" height="6" rx="1.5" />
        <rect x="17.5" y="12" width="3.5" height="6" rx="1.5" />
      </svg>
    ),
  },
];

export default function AccordionIconLeft() {
  const [open, setOpen] = useState<string | null>("billing");
  const uid = useId();
  const btnRefs = useRef<Array<HTMLButtonElement | null>>([]);

  const toggle = (id: string) => setOpen((cur) => (cur === id ? null : id));

  const onKeyNav = (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) {
      e.preventDefault();
      btnRefs.current[next]?.focus();
    }
  };

  return (
    <section className="relative w-full bg-slate-50 px-4 py-20 dark:bg-slate-950 sm:px-6 lg:py-28">
      <style>{`
        @keyframes acil-reveal {
          from { opacity: 0; transform: translateY(-4px); }
          to   { opacity: 1; transform: translateY(0); }
        }
        .acil-panel-inner { animation: acil-reveal .28s cubic-bezier(.16,1,.3,1); }
        @media (prefers-reduced-motion: reduce) {
          .acil-panel-inner { animation: none; }
          .acil-grid, .acil-chevron, .acil-icon { 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 bg-indigo-50 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-indigo-600 ring-1 ring-inset ring-indigo-100 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-500/20">
            Answers
          </span>
          <h2 className="mt-4 text-3xl font-semibold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
            Frequently asked questions
          </h2>
          <p className="mt-3 text-base text-slate-600 dark:text-slate-400">
            Everything you need to know before you start. Can&apos;t find it here? Chat with us.
          </p>
        </div>

        <div className="space-y-3">
          {items.map((item, i) => {
            const isOpen = open === item.id;
            const panelId = `${uid}-panel-${item.id}`;
            const btnId = `${uid}-btn-${item.id}`;
            return (
              <div
                key={item.id}
                className={`overflow-hidden rounded-2xl border bg-white shadow-sm transition-colors duration-200 dark:bg-slate-900 ${
                  isOpen
                    ? "border-indigo-200 dark:border-indigo-500/40"
                    : "border-slate-200 dark:border-slate-800"
                }`}
              >
                <h3>
                  <button
                    id={btnId}
                    ref={(el) => {
                      btnRefs.current[i] = el;
                    }}
                    type="button"
                    aria-expanded={isOpen}
                    aria-controls={panelId}
                    onClick={() => toggle(item.id)}
                    onKeyDown={(e) => onKeyNav(e, i)}
                    className="group flex w-full items-center gap-4 px-4 py-4 text-left outline-none transition-colors hover:bg-slate-50 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:hover:bg-slate-800/50 dark:focus-visible:ring-offset-slate-900 sm:px-5"
                  >
                    <span
                      className={`acil-icon flex h-10 w-10 shrink-0 items-center justify-center rounded-xl ring-1 ring-inset transition-transform duration-300 ${item.accent} ${item.accentDark} ${item.tint} ${item.tintDark} ${
                        isOpen ? "scale-105" : "group-hover:scale-105"
                      }`}
                    >
                      {item.icon}
                    </span>

                    <span className="min-w-0 flex-1">
                      <span className="block text-[11px] font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500">
                        {item.meta}
                      </span>
                      <span className="mt-0.5 block text-[15px] font-medium leading-snug text-slate-900 dark:text-white sm:text-base">
                        {item.title}
                      </span>
                    </span>

                    <span
                      className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-slate-400 transition-colors dark:text-slate-500 ${
                        isOpen
                          ? "bg-indigo-50 text-indigo-600 dark:bg-indigo-500/15 dark:text-indigo-300"
                          : "group-hover:bg-slate-100 dark:group-hover:bg-slate-800"
                      }`}
                    >
                      <svg
                        viewBox="0 0 24 24"
                        fill="none"
                        stroke="currentColor"
                        strokeWidth="2"
                        strokeLinecap="round"
                        strokeLinejoin="round"
                        aria-hidden="true"
                        className={`acil-chevron h-4.5 w-4.5 transition-transform duration-300 ease-out ${
                          isOpen ? "rotate-180" : "rotate-0"
                        }`}
                      >
                        <path d="m6 9 6 6 6-6" />
                      </svg>
                    </span>
                  </button>
                </h3>

                <div
                  id={panelId}
                  role="region"
                  aria-labelledby={btnId}
                  hidden={!isOpen}
                  className="acil-grid"
                >
                  <div className="acil-panel-inner px-4 pb-5 pl-[72px] pr-5 sm:pl-[76px]">
                    <p className="text-sm leading-relaxed text-slate-600 dark:text-slate-400">
                      {item.body}
                    </p>
                  </div>
                </div>
              </div>
            );
          })}
        </div>

        <p className="mt-8 text-center text-sm text-slate-500 dark:text-slate-400">
          Still stuck?{" "}
          <a
            href="#contact"
            className="font-medium text-indigo-600 underline-offset-4 hover:underline dark:text-indigo-400"
          >
            Talk to our team
          </a>
          .
        </p>
      </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 →