Web InnoventixFreeCode

Dark Card Accordion

Original · free

A dark, glassy card accordion.

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

import { useId, useState } from "react";

type FaqItem = {
  id: string;
  question: string;
  answer: string;
  tag: string;
};

const ITEMS: FaqItem[] = [
  {
    id: "billing",
    tag: "Billing",
    question: "When am I charged after the free trial ends?",
    answer:
      "Your card is charged on the 15th day, the morning your 14-day trial closes. We send a reminder email 72 hours before, and you can cancel from Settings → Plan with a single click until the moment the trial expires.",
  },
  {
    id: "seats",
    tag: "Teams",
    question: "Can I add or remove seats mid-cycle?",
    answer:
      "Yes. Add seats instantly and we prorate the difference to your next invoice. Removing a seat frees it at the end of the current billing period, so you keep paid access until the date you already paid for.",
  },
  {
    id: "export",
    tag: "Data",
    question: "Do I keep my data if I downgrade or leave?",
    answer:
      "Always. Export everything as CSV or JSON at any time, no lock-in. If you cancel, we keep your workspace in read-only cold storage for 30 days so you can reactivate or download before anything is deleted.",
  },
  {
    id: "security",
    tag: "Security",
    question: "How is my workspace protected?",
    answer:
      "Data is encrypted in transit with TLS 1.3 and at rest with AES-256. We run SOC 2 Type II controls, enforce optional SSO and 2FA, and scope every access token to the least privilege it needs.",
  },
];

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

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

  return (
    <section className="relative w-full overflow-hidden bg-slate-950 px-4 py-20 sm:px-6 sm:py-28">
      <style>{`
        @keyframes adc_rise {
          from { opacity: 0; transform: translateY(8px); }
          to { opacity: 1; transform: translateY(0); }
        }
        @keyframes adc_sheen {
          0% { transform: translateX(-120%); }
          100% { transform: translateX(120%); }
        }
        @media (prefers-reduced-motion: reduce) {
          .adc_animate { animation: none !important; }
          .adc_panel { transition: none !important; }
        }
      `}</style>

      {/* Ambient glow backdrop */}
      <div
        aria-hidden
        className="pointer-events-none absolute -top-32 left-1/2 h-72 w-[36rem] -translate-x-1/2 rounded-full bg-indigo-500/20 blur-3xl"
      />
      <div
        aria-hidden
        className="pointer-events-none absolute -bottom-40 right-0 h-72 w-72 rounded-full bg-violet-500/10 blur-3xl"
      />

      <div className="relative mx-auto max-w-2xl">
        <div className="mb-10 text-center">
          <span className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs font-medium text-indigo-200 backdrop-blur">
            <span className="h-1.5 w-1.5 rounded-full bg-emerald-400" />
            Support Center
          </span>
          <h2 className="mt-4 text-3xl font-semibold tracking-tight text-white sm:text-4xl">
            Questions, answered
          </h2>
          <p className="mt-3 text-sm text-slate-400 sm:text-base">
            Everything about billing, teams, and keeping your data yours.
          </p>
        </div>

        <div className="space-y-3">
          {ITEMS.map((item, index) => {
            const isOpen = openId === item.id;
            const btnId = `${baseId}-btn-${item.id}`;
            const panelId = `${baseId}-panel-${item.id}`;

            return (
              <div
                key={item.id}
                className={[
                  "adc_animate group relative overflow-hidden rounded-2xl border backdrop-blur-xl transition-colors duration-300",
                  isOpen
                    ? "border-indigo-400/40 bg-white/[0.07] shadow-lg shadow-indigo-950/40"
                    : "border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.05]",
                ].join(" ")}
                style={{
                  animation: `adc_rise 0.5s ease-out ${index * 0.07}s both`,
                }}
              >
                {/* top hairline highlight */}
                <span
                  aria-hidden
                  className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-white/25 to-transparent"
                />

                <h3>
                  <button
                    id={btnId}
                    type="button"
                    aria-expanded={isOpen}
                    aria-controls={panelId}
                    onClick={() => toggle(item.id)}
                    className="flex w-full items-center gap-4 px-5 py-4 text-left outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-400/70 sm:px-6 sm:py-5"
                  >
                    <span className="flex-1">
                      <span className="mb-1 block text-[0.7rem] font-medium uppercase tracking-wider text-indigo-300/80">
                        {item.tag}
                      </span>
                      <span className="block text-base font-medium text-slate-100 sm:text-lg">
                        {item.question}
                      </span>
                    </span>

                    <span
                      aria-hidden
                      className={[
                        "flex h-8 w-8 flex-none items-center justify-center rounded-full border transition-all duration-300",
                        isOpen
                          ? "rotate-180 border-indigo-400/50 bg-indigo-500/20 text-indigo-200"
                          : "border-white/15 bg-white/5 text-slate-300 group-hover:border-white/30",
                      ].join(" ")}
                    >
                      <svg
                        viewBox="0 0 24 24"
                        fill="none"
                        stroke="currentColor"
                        strokeWidth={2}
                        strokeLinecap="round"
                        strokeLinejoin="round"
                        className="h-4 w-4"
                      >
                        <path d="m6 9 6 6 6-6" />
                      </svg>
                    </span>
                  </button>
                </h3>

                {/* grid-rows height animation, no JS measuring */}
                <div
                  id={panelId}
                  role="region"
                  aria-labelledby={btnId}
                  className="adc_panel grid transition-[grid-template-rows] duration-300 ease-out"
                  style={{ gridTemplateRows: isOpen ? "1fr" : "0fr" }}
                >
                  <div className="overflow-hidden">
                    <div className="px-5 pb-5 sm:px-6 sm:pb-6">
                      <div className="mb-4 h-px w-full bg-gradient-to-r from-white/10 via-white/5 to-transparent" />
                      <p className="text-sm leading-relaxed text-slate-400 sm:text-[0.95rem]">
                        {item.answer}
                      </p>
                    </div>
                  </div>
                </div>
              </div>
            );
          })}
        </div>

        <p className="mt-8 text-center text-sm text-slate-500">
          Still stuck?{" "}
          <a
            href="#contact"
            className="font-medium text-indigo-300 underline decoration-indigo-400/40 underline-offset-4 outline-none transition-colors hover:text-indigo-200 focus-visible:ring-2 focus-visible:ring-indigo-400/70 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950"
          >
            Talk to a human
          </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 →