Ir al contenido
Evaluar mi proyecto
Componentes · SeccionesDisponible

Preguntas frecuentes en dos columnas

Título y contacto a la izquierda y acordeón accesible a la derecha con apertura animada por grid-template-rows.

Preguntas frecuentes

Lo que más nos preguntan antes de empezar. Si no está aquí, escríbenos.

Escríbenos tu duda →
  • Depende del alcance. Un primer sistema funcional suele tomar de cuatro a doce semanas, trabajando en sprints de una a dos semanas con entregas visibles.

  • Lo cotizamos por separado y por escrito antes de tocar nada. Así el precio acordado no cambia por sorpresa.

  • Sí. Todo el código y los recursos del proyecto se entregan a tu nombre al finalizar el pago.

  • Hay 30 días de estabilización incluidos. Después puedes contratar mantenimiento mensual o acompañamiento estratégico.

  • Con gusto. Podemos firmar el tuyo o proponerte uno antes de la primera llamada.

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

seccion-faq.tsx81 líneas
Descargar
"use client";

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

/** Sección de preguntas frecuentes en dos columnas: título y contacto a la izquierda y acordeón exclusivo a la derecha, con apertura animada y accesible (aria-expanded). Minimalista. React + TypeScript, sin dependencias. */
export type SeccionFaqItem = { q: string; a: string };

export type SeccionFaqProps = {
  title?: string;
  intro?: string;
  items?: SeccionFaqItem[];
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcsfq-root { box-sizing: border-box; container-type: inline-size; width: 100%; padding: clamp(32px, 6cqi, 80px) clamp(18px, 5cqi, 72px); background: #fff; color: #18181b; font: 400 16px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
.gcsfq-root *, .gcsfq-root *::before, .gcsfq-root *::after { box-sizing: border-box; }
.gcsfq-root button { font: inherit; -webkit-tap-highlight-color: transparent; }
.gcsfq-root :focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .gcsfq-root * { transition-duration: .01ms !important; } }
.gcsfq-grid { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: 48px; max-width: 1100px; margin: 0 auto; }
.gcsfq-title { margin: 0 0 12px; font: 700 clamp(30px, 4.6cqi, 52px)/1.04 ui-sans-serif, system-ui, sans-serif; letter-spacing: -.04em; text-wrap: balance; }
.gcsfq-intro { margin: 0 0 20px; max-width: 34ch; color: #52525b; }
.gcsfq-link { color: #2563eb; font-weight: 600; text-underline-offset: 4px; }
.gcsfq-list { margin: 0; padding: 0; list-style: none; border-top: 1px solid #e4e4e7; }
.gcsfq-item { border-bottom: 1px solid #e4e4e7; }
.gcsfq-q { display: flex; align-items: center; justify-content: space-between; gap: 16px; width: 100%; padding: 20px 0; border: 0; background: none; color: inherit; font-size: 18px; font-weight: 600; text-align: left; cursor: pointer; }
.gcsfq-q:hover { color: #2563eb; }
.gcsfq-pm { position: relative; flex: none; width: 20px; height: 20px; }
.gcsfq-pm::before, .gcsfq-pm::after { content: ""; position: absolute; left: 3px; right: 3px; top: 50%; height: 2px; margin-top: -1px; border-radius: 1px; background: currentColor; transition: transform .35s cubic-bezier(.65, 0, .35, 1); }
.gcsfq-pm::after { transform: rotate(90deg); }
.gcsfq-q[aria-expanded="true"] .gcsfq-pm::after { transform: rotate(0); }
.gcsfq-panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .4s cubic-bezier(.16, 1, .3, 1); }
.gcsfq-panel[data-open="true"] { grid-template-rows: 1fr; }
.gcsfq-panel > div { overflow: hidden; }
.gcsfq-panel p { margin: 0 0 22px; max-width: 58ch; color: #52525b; }
@container (max-width: 760px) { .gcsfq-grid { grid-template-columns: minmax(0, 1fr); gap: 28px; } }`;

const DEFAULT: SeccionFaqItem[] = [
  { q: "¿Cuánto tarda un proyecto?", a: "Depende del alcance. Un primer sistema funcional suele tomar de cuatro a doce semanas, trabajando en sprints de una a dos semanas con entregas visibles." },
  { q: "¿Qué pasa si necesito algo fuera del alcance?", a: "Lo cotizamos por separado y por escrito antes de tocar nada. Así el precio acordado no cambia por sorpresa." },
  { q: "¿El código es mío al terminar?", a: "Sí. Todo el código y los recursos del proyecto se entregan a tu nombre al finalizar el pago." },
  { q: "¿Ofrecen soporte después del lanzamiento?", a: "Hay 30 días de estabilización incluidos. Después puedes contratar mantenimiento mensual o acompañamiento estratégico." },
  { q: "¿Firman acuerdos de confidencialidad?", a: "Con gusto. Podemos firmar el tuyo o proponerte uno antes de la primera llamada." },
];

export function SeccionFaq({ title = "Preguntas frecuentes", intro = "Lo que más nos preguntan antes de empezar. Si no está aquí, escríbenos.", items = DEFAULT, className = "", style }: SeccionFaqProps) {
  const id = useId();
  const [open, setOpen] = useState<number | null>(0);
  return (
    <section className={`gcsfq-root ${className}`} style={style} aria-labelledby={`${id}-t`}>
      <style>{CSS}</style>
      <div className="gcsfq-grid">
        <div>
          <h2 className="gcsfq-title" id={`${id}-t`}>{title}</h2>
          <p className="gcsfq-intro">{intro}</p>
          <a className="gcsfq-link" href="#" onClick={(event) => event.preventDefault()}>Escríbenos tu duda →</a>
        </div>
        <ul className="gcsfq-list">
          {items.map((item, i) => (
            <li key={item.q} className="gcsfq-item">
              <h3 style={{ margin: 0 }}>
                <button className="gcsfq-q" type="button" aria-expanded={open === i} aria-controls={`${id}-p${i}`} id={`${id}-b${i}`} onClick={() => setOpen(open === i ? null : i)}>
                  {item.q}
                  <span className="gcsfq-pm" aria-hidden="true" />
                </button>
              </h3>
              <div id={`${id}-p${i}`} className="gcsfq-panel" data-open={open === i} role="region" aria-labelledby={`${id}-b${i}`}>
                <div>
                  <p>{item.a}</p>
                </div>
              </div>
            </li>
          ))}
        </ul>
      </div>
    </section>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/seccion-faq

Ejemplo de uso de Preguntas frecuentes en dos columnas

Después de copiar seccion-faq.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { SeccionFaq } from "./seccion-faq";

export default function Ejemplo() {
  return <SeccionFaq title="Ejemplo" />;
}

Propiedades de SeccionFaq

SeccionFaq acepta 5 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
title?stringOpcional.
intro?stringOpcional.
items?SeccionFaqItem[]Opcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Accesibilidad y rendimiento

El archivo tiene 81 líneas (5 KB) y no necesita @keyframes. Esto es lo que hace Preguntas frecuentes en dos columnas por quien lo usa:

  • Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
  • Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
  • Declara roles ARIA para lectores de pantalla: region.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)