guias

FAQ + formulario: patrón de 2 columnas con CTA sticky

Cómo construir un módulo FAQ + contacto WhatsApp en 2 columnas con tarjeta sticky — el CTA siempre a la vista mientras el visitante lee las preguntas.

FAQ + formulario: patrón de 2 columnas con CTA sticky

Hay una trampa de diseño muy común en sitios de negocio local: la sección de preguntas frecuentes y el formulario de contacto viven en secciones separadas. El visitante lee las preguntas, llega al final del acordeón y el formulario ya quedó a varios scrolls de distancia. Hace el esfuerzo, vuelve a subir, llena el formulario, y —si tiene suerte— lo encuentra sin que la página haya cambiado de estado. Si en ese recorrido hay una sola fricción extra, el visitante se va.

El patrón que documenta este artículo resuelve ese problema de raíz: el acordeón y la tarjeta de contacto coexisten en la misma sección, en dos columnas, y la tarjeta es sticky. No importa en qué pregunta esté el visitante: el formulario siempre está a la vista.

El problema que resuelve

En una sección de FAQ estándar, el flujo del visitante es:

  1. Lee la primera pregunta — su duda principal, la que lo trajo.
  2. Sigue leyendo para ver si hay algo más que no sabía.
  3. Llega al final. Su duda no estaba, o sí estaba pero quiere confirmar.
  4. Tiene que buscar cómo contactar al negocio.

En el paso 4 se pierden visitas. No porque el negocio no tenga formulario, sino porque el formulario no está visible cuando el visitante decide que quiere usarlo.

Con FAQWithContact el formulario siempre está a la derecha, pegado al viewport mientras el visitante baja. En el momento exacto en que decide escribir, el formulario ya está ahí.

Estructura del componente

El componente produce una sección completa con su propio encabezado (no hay que importar SectionHeading desde la página) y dos columnas:

Columna izquierda: FAQAccordion en modo bare — sin padding propio, sin margen de sección, incrustado directamente en la columna. El primer ítem abre por defecto (openFirst).

Columna derecha: tarjeta blanca con sombra y borde redondeado. En desktop es position: sticky con top: calc(var(--stack-height) + var(--sp-5)), así viaja con el viewport mientras el visitante lee. En móvil sube al principio de la sección (order: -1) para que sea lo primero visible, no las preguntas.

---
import FAQWithContact from '@components/FAQWithContact.astro'
import { CONTACT } from '@config/site'

const faqs = [
  {
    question: '¿Qué equipo necesito para mi negocio?',
    answer: 'Depende del tipo de inmueble y lo que pueda quemarse. Para la mayoría de oficinas y comercios, el equipo base cubre el riesgo ordinario. Escríbenos con tu giro y tamaño y te decimos exactamente qué necesitas.',
  },
  {
    question: '¿Cada cuándo se da mantenimiento?',
    answer: 'El mantenimiento preventivo se recomienda una vez al año. La norma NOM-154 lo exige con esa frecuencia mínima para extintores. La prueba hidrostática aplica cada 5 años.',
  },
  // … más preguntas
]
---

<FAQWithContact
  items={faqs}
  waNumber={CONTACT.whatsapp}
/>

Dos props. No hay que ensamblar el encabezado, el layout, la tarjeta ni el script del contador.

El select de giro: pre-cualificar el lead

Uno de los detalles más importantes del componente es el select de tipo de negocio. No está por accesibilidad ni por formulario: está porque el mensaje que llega a WhatsApp ya viene pre-cualificado.

Sin select, el mensaje dice: «Hola, tengo una pregunta sobre sus servicios.»
Con select, el mensaje dice: «Hola, soy una empresa industrial mediana. Quiero saber: [mensaje del visitante].»

La respuesta al segundo mensaje puede ser inmediata y específica. La respuesta al primero necesita al menos una vuelta de preguntas de regreso. En negocios con volumen de mensajes medio-alto, la diferencia es decenas de minutos de atención ahorrados por día.

El select es required — es el único campo obligatorio junto con la textarea. El nombre del contacto es opcional porque agregarle obligatoriedad solo añade fricción sin mejorar la calidad del lead.

El contador de caracteres

La textarea tiene un límite de 500 caracteres con contador en tiempo real. El contador empieza en gris, cambia a ámbar cuando el visitante supera los 420. Es una señal visual: «vas a llegar al límite, revisa si ya dijiste lo importante».

// El script que vive dentro del componente:
const textarea = document.querySelector('[data-fwc-msg]')
const counter  = document.querySelector('[data-fwc-count]')

textarea.addEventListener('input', () => {
  const remaining = 500 - textarea.value.length
  counter.textContent = `${remaining} caracteres restantes`
  counter.classList.toggle('warn', remaining < 80)
})

El JS usa define:vars={{ waNumber }} de Astro para recibir el número desde la prop y construir el enlace sin hardcodearlo. Eso es lo único que pasa entre el componente y el script: el número de teléfono.

Cómo construye el mensaje de WhatsApp

Al presionar «Enviar mensaje» el componente:

  1. Lee el nombre (si existe), el giro y el mensaje.
  2. Construye el texto: Hola, soy [nombre].\nGiro: [giro]\n\n[mensaje]
  3. Codifica el texto con encodeURIComponent.
  4. Abre https://wa.me/${waNumber}?text=${texto} en una pestaña nueva.

Ningún dato se envía a un servidor. No hay backend, no hay base de datos, no hay correo. La conversación empieza directamente en WhatsApp, donde el negocio ya la está esperando.

El número desde CONTACT.whatsapp — regla D4

El número de WhatsApp nunca se escribe a mano en el componente. Siempre viene de CONTACT.whatsapp en site.ts, que es la única fuente de verdad para el dato de contacto.

El formato que espera el componente es E.164 sin el signo + — igual que wa.me lo requiere:

// site.ts
export const CONTACT = {
  whatsapp: '525512345678', // sin + · 52 = México, 55 = CDMX
  phone: '+52 55 1234 5678',
  email: 'hola@ejemplo.mx',
}

Si el número cambia, se cambia en site.ts y se actualiza en todo el sitio, incluido FAQWithContact, sin buscar ni reemplazar nada más.

Cuándo usar este módulo y cuándo no

Úsalo en:

  • La home, donde las FAQs son del negocio en general y el visitante aún no sabe qué quiere.
  • Landings de servicio con preguntas frecuentes sobre ese servicio específico.
  • Páginas de contacto donde quieres dar contexto antes del formulario.

No lo uses en:

  • Páginas de blog o artículos — el formulario de contacto es ruido fuera de contexto de venta.
  • Páginas de categoría de producto donde el FAQ es específico de un ítem (ahí, FAQAccordion solo es suficiente porque el CTA ya está en el layout de la página).
  • Cuando el negocio no usa WhatsApp para ventas — el componente entero está construido sobre wa.me.

Diferencia con FAQ solo

FAQAccordion en modo normal es útil cuando las preguntas son específicas de una entidad (un producto, un servicio) y el CTA de contacto ya vive en otro lugar de la página — el sidebar sticky del layout, el header fijo, el botón flotante de WhatsApp. En ese caso el acordeón no necesita su propio formulario.

FAQWithContact es para cuando el FAQ y el formulario son la sección de conversión de la página. El visitante llegó sin decisión tomada, leyó las preguntas para resolver sus dudas y, en el momento en que decide escribir, el formulario ya está a la vista.

La distinción no es de componente: es de dónde está el CTA de conversión en el flujo de la página.

Dónde vive en la plantilla

El componente está en src/components/FAQWithContact.astro. La página de módulo con documentación completa es src/pages/modulos/faq-with-contact.astro. En la home (src/pages/index.astro) reemplaza el antiguo bloque de FAQAccordion bare + ContactForm como instancia en vivo.

¿Listo para dar el siguiente paso?

Cuéntanos qué necesitas y te respondemos hoy mismo.

¿Necesitas ayuda?