Componente FAQ con Pestañas
Un componente FAQ moderno con pestañas que organiza las preguntas por categorías con transiciones suaves
Diseño Responsivo
Sí
Soporte para Modo Oscuro
No
líneas
3
Compatibilidad del Navegador
No
Vista Previa en Vivo
Interactúa con el componente sin salir de la página.
Componente FAQ con Pestañas
Un componente FAQ moderno con pestañas que organiza las preguntas por categorías con transiciones suaves, estados activos y diseño responsivo.
Características
- Organización por Categorías: Preguntas agrupadas por categorías relevantes
- Transiciones Suaves: Cambio de pestañas animado y carga de contenido
- Gestión de Estados Activos: Indicadores visuales claros para pestañas activas
- Diseño Responsivo: Funciona perfectamente en todos los tamaños de dispositivo
- Navegación por Teclado: Soporte completo de accesibilidad por teclado
- Integración de Búsqueda: Búsqueda opcional dentro de la pestaña activa
- Carga Perezosa: Contenido cargado solo cuando se activan las pestañas
- Estilo Personalizable: Fácil de tematizar y personalizar
- Soporte de Hash URL: Enlace directo a pestañas específicas
- Indicadores de Insignia: Mostrar conteo de preguntas por categoría
Estructura HTML
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Componente FAQ con Pestañas</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="faq-tabs-container">
<!-- Encabezado -->
<div class="faq-header">
<h1>Preguntas Frecuentes</h1>
<p>Encuentra respuestas a preguntas comunes organizadas por categoría</p>
</div>
<!-- Navegación de Pestañas -->
<div class="tab-navigation">
<div class="tab-list" role="tablist">
<button class="tab-button active"
role="tab"
aria-selected="true"
aria-controls="general-panel"
id="general-tab"
data-tab="general">
<span class="tab-icon">🏠</span>
<span class="tab-text">General</span>
<span class="tab-badge">6</span>
</button>
<button class="tab-button"
role="tab"
aria-selected="false"
aria-controls="facturacion-panel"
id="facturacion-tab"
data-tab="facturacion">
<span class="tab-icon">💳</span>
<span class="tab-text">Facturación</span>
<span class="tab-badge">4</span>
</button>
<button class="tab-button"
role="tab"
aria-selected="false"
aria-controls="tecnico-panel"
id="tecnico-tab"
data-tab="tecnico">
<span class="tab-icon">⚙️</span>
<span class="tab-text">Técnico</span>
<span class="tab-badge">5</span>
</button>
<button class="tab-button"
role="tab"
aria-selected="false"
aria-controls="soporte-panel"
id="soporte-tab"
data-tab="soporte">
<span class="tab-icon">🎧</span>
<span class="tab-text">Soporte</span>
<span class="tab-badge">3</span>
</button>
</div>
<!-- Indicador de Pestaña -->
<div class="tab-indicator"></div>
</div>
<!-- Contenido de Pestañas -->
<div class="tab-content">
<!-- Panel de Pestaña General -->
<div class="tab-panel active"
role="tabpanel"
id="general-panel"
aria-labelledby="general-tab">
<div class="panel-search">
<input type="text"
class="search-input"
placeholder="Buscar preguntas generales..."
data-search-target="general">
<svg class="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
</div>
<div class="questions-list">
<div class="question-item" data-category="general">
<div class="question-header">
<h3>¿Cuál es su política de devoluciones?</h3>
<button class="toggle-btn" aria-expanded="false">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</button>
</div>
<div class="question-answer">
<p>Ofrecemos una política de devolución de 30 días para todos los artículos no utilizados en su empaque original. Simplemente contacte a nuestro equipo de servicio al cliente para iniciar una devolución, y le proporcionaremos una etiqueta de envío prepagada.</p>
<ul>
<li>Los artículos deben estar sin usar y en condición original</li>
<li>Se requiere el empaque original</li>
<li>Reembolso procesado dentro de 5-7 días hábiles</li>
</ul>
</div>
</div>
<div class="question-item" data-category="general">
<div class="question-header">
<h3>¿Envían internacionalmente?</h3>
<button class="toggle-btn" aria-expanded="false">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</button>
</div>
<div class="question-answer">
<p>¡Sí! Enviamos a más de 50 países en todo el mundo. Las tarifas de envío internacional y los tiempos de entrega varían según el destino.</p>
<div class="info-grid">
<div class="info-item">
<strong>Costos de Envío:</strong>
<span>Calculados al finalizar la compra basados en peso y destino</span>
</div>
<div class="info-item">
<strong>Tiempo de Entrega:</strong>
<span>7-21 días hábiles dependiendo de la ubicación</span>
</div>
</div>
</div>
</div>
<div class="question-item" data-category="general">
<div class="question-header">
<h3>¿Cómo puedo rastrear mi pedido?</h3>
<button class="toggle-btn" aria-expanded="false">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</button>
</div>
<div class="question-answer">
<p>Una vez que su pedido sea enviado, recibirá un número de seguimiento por correo electrónico. Puede usar este número para rastrear su paquete en nuestro sitio web o en el sitio web del transportista.</p>
<div class="tracking-steps">
<div class="step">1. Revise su correo electrónico para información de seguimiento</div>
<div class="step">2. Visite nuestra página de seguimiento de pedidos</div>
<div class="step">3. Ingrese su número de seguimiento</div>
<div class="step">4. Vea actualizaciones en tiempo real</div>
</div>
</div>
</div>
</div>
</div>
<!-- Panel de Pestaña Facturación -->
<div class="tab-panel"
role="tabpanel"
id="facturacion-panel"
aria-labelledby="facturacion-tab">
<div class="panel-search">
<input type="text"
class="search-input"
placeholder="Buscar preguntas de facturación..."
data-search-target="facturacion">
<svg class="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
</div>
<div class="questions-list">
<div class="question-item" data-category="facturacion">
<div class="question-header">
<h3>¿Qué métodos de pago aceptan?</h3>
<button class="toggle-btn" aria-expanded="false">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</button>
</div>
<div class="question-answer">
<p>Aceptamos todos los métodos de pago principales para hacer su experiencia de compra conveniente:</p>
<div class="payment-grid">
<div class="payment-category">
<h4>Tarjetas de Crédito</h4>
<p>Visa, MasterCard, American Express, Discover</p>
</div>
<div class="payment-category">
<h4>Billeteras Digitales</h4>
<p>PayPal, Apple Pay, Google Pay</p>
</div>
<div class="payment-category">
<h4>Transferencia Bancaria</h4>
<p>Transferencias ACH y wire aceptadas</p>
</div>
</div>
</div>
</div>
<div class="question-item" data-category="facturacion">
<div class="question-header">
<h3>¿Cuándo se me cobrará?</h3>
<button class="toggle-btn" aria-expanded="false">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</button>
</div>
<div class="question-answer">
<p>Su método de pago será cobrado cuando su pedido sea procesado y esté listo para enviar. Esto típicamente ocurre dentro de 24 horas de realizar su pedido.</p>
<div class="billing-timeline">
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<strong>Pedido Realizado</strong>
<span>Pago autorizado</span>
</div>
</div>
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<strong>Pedido Procesado</strong>
<span>Pago cobrado</span>
</div>
</div>
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<strong>Pedido Enviado</strong>
<span>Información de seguimiento enviada</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Panel de Pestaña Técnico -->
<div class="tab-panel"
role="tabpanel"
id="tecnico-panel"
aria-labelledby="tecnico-tab">
<div class="panel-search">
<input type="text"
class="search-input"
placeholder="Buscar preguntas técnicas..."
data-search-target="tecnico">
<svg class="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
</div>
<div class="questions-list">
<div class="question-item" data-category="tecnico">
<div class="question-header">
<h3>¿Cuáles son los requisitos del sistema?</h3>
<button class="toggle-btn" aria-expanded="false">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</button>
</div>
<div class="question-answer">
<p>Nuestra plataforma está diseñada para funcionar en dispositivos y navegadores modernos:</p>
<div class="requirements-grid">
<div class="req-section">
<h4>Navegadores Compatibles</h4>
<ul>
<li>Chrome 80+</li>
<li>Firefox 75+</li>
<li>Safari 13+</li>
<li>Edge 80+</li>
</ul>
</div>
<div class="req-section">
<h4>Dispositivos Móviles</h4>
<ul>
<li>iOS 13+</li>
<li>Android 8+</li>
<li>Diseño responsivo</li>
<li>Optimizado para táctil</li>
</ul>
</div>
</div>
</div>
</div>
<div class="question-item" data-category="tecnico">
<div class="question-header">
<h3>¿Cómo restablezco mi contraseña?</h3>
<button class="toggle-btn" aria-expanded="false">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</button>
</div>
<div class="question-answer">
<p>Siga estos pasos para restablecer su contraseña:</p>
<ol class="reset-steps">
<li>Vaya a la página de inicio de sesión y haga clic en "Olvidé mi contraseña"</li>
<li>Ingrese su dirección de correo electrónico</li>
<li>Revise su correo electrónico para un enlace de restablecimiento</li>
<li>Haga clic en el enlace y cree una nueva contraseña</li>
<li>Inicie sesión con su nueva contraseña</li>
</ol>
<div class="note">
<strong>Nota:</strong> Los enlaces de restablecimiento expiran después de 24 horas por razones de seguridad.
</div>
</div>
</div>
</div>
</div>
<!-- Panel de Pestaña Soporte -->
<div class="tab-panel"
role="tabpanel"
id="soporte-panel"
aria-labelledby="soporte-tab">
<div class="panel-search">
<input type="text"
class="search-input"
placeholder="Buscar preguntas de soporte..."
data-search-target="soporte">
<svg class="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
</div>
<div class="questions-list">
<div class="question-item" data-category="soporte">
<div class="question-header">
<h3>¿Cómo puedo contactar al soporte al cliente?</h3>
<button class="toggle-btn" aria-expanded="false">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</button>
</div>
<div class="question-answer">
<p>¡Estamos aquí para ayudar! Contacte a nuestro equipo de soporte a través de múltiples canales:</p>
<div class="contact-grid">
<div class="contact-method">
<div class="contact-icon">📧</div>
<h4>Soporte por Email</h4>
<p>soporte@ejemplo.com</p>
<span>Respuesta dentro de 24 horas</span>
</div>
<div class="contact-method">
<div class="contact-icon">💬</div>
<h4>Chat en Vivo</h4>
<p>Disponible en nuestro sitio web</p>
<span>Lun-Vie 9AM-6PM EST</span>
</div>
<div class="contact-method">
<div class="contact-icon">📞</div>
<h4>Soporte Telefónico</h4>
<p>1-800-123-4567</p>
<span>Lun-Vie 9AM-6PM EST</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Estilos CSS
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.faq-tabs-container {
max-width: 1000px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
/* Estilos del Encabezado */
.faq-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px 30px;
text-align: center;
}
.faq-header h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 8px;
}
.faq-header p {
font-size: 1.1rem;
opacity: 0.9;
}
/* Navegación de Pestañas */
.tab-navigation {
position: relative;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
}
.tab-list {
display: flex;
overflow-x: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
.tab-list::-webkit-scrollbar {
display: none;
}
.tab-button {
background: none;
border: none;
padding: 20px 24px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 500;
color: #64748b;
transition: all 0.3s ease;
white-space: nowrap;
position: relative;
min-width: 140px;
justify-content: center;
}
.tab-button:hover {
color: #475569;
background: rgba(102, 126, 234, 0.05);
}
.tab-button.active {
color: #667eea;
background: rgba(102, 126, 234, 0.1);
}
.tab-icon {
font-size: 18px;
}
.tab-text {
font-weight: 600;
}
.tab-badge {
background: #667eea;
color: white;
font-size: 12px;
padding: 2px 6px;
border-radius: 10px;
min-width: 20px;
text-align: center;
}
.tab-button.active .tab-badge {
background: #4f46e5;
}
/* Indicador de Pestaña */
.tab-indicator {
position: absolute;
bottom: 0;
left: 0;
height: 3px;
background: linear-gradient(90deg, #667eea, #764ba2);
transition: all 0.3s ease;
border-radius: 3px 3px 0 0;
}
/* Contenido de Pestañas */
.tab-content {
position: relative;
min-height: 400px;
}
.tab-panel {
display: none;
padding: 30px;
animation: fadeIn 0.3s ease;
}
.tab-panel.active {
display: block;
}
/* Búsqueda del Panel */
.panel-search {
position: relative;
margin-bottom: 30px;
max-width: 400px;
}
.search-input {
width: 100%;
padding: 12px 20px 12px 50px;
border: 2px solid #e2e8f0;
border-radius: 25px;
font-size: 16px;
background: #f8fafc;
transition: all 0.3s ease;
}
.search-input:focus {
outline: none;
border-color: #667eea;
background: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.search-icon {
position: absolute;
left: 18px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
}
/* Lista de Preguntas */
.questions-list {
space-y: 16px;
}
.question-item {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 12px;
margin-bottom: 16px;
transition: all 0.3s ease;
overflow: hidden;
}
.question-item:hover {
border-color: #cbd5e1;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.question-header {
padding: 20px 24px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
transition: background 0.3s ease;
}
.question-header:hover {
background: #f1f5f9;
}
.question-header h3 {
font-size: 1.1rem;
font-weight: 600;
color: #1e293b;
margin: 0;
flex: 1;
}
.toggle-btn {
background: #667eea;
border: none;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
}
.toggle-btn:hover {
background: #5a67d8;
transform: scale(1.05);
}
.toggle-btn svg {
color: white;
transition: transform 0.3s ease;
}
.question-item.active .toggle-btn svg {
transform: rotate(180deg);
}
.question-answer {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
background: white;
}
.question-item.active .question-answer {
max-height: 1000px;
padding: 0 24px 24px;
}
.question-answer p {
color: #64748b;
line-height: 1.6;
margin-bottom: 16px;
}
.question-answer ul,
.question-answer ol {
color: #64748b;
line-height: 1.6;
margin: 16px 0;
padding-left: 20px;
}
.question-answer li {
margin-bottom: 8px;
}
/* Cuadrícula de Información */
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
margin: 16px 0;
}
.info-item {
background: #f8fafc;
padding: 16px;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.info-item strong {
display: block;
color: #374151;
margin-bottom: 4px;
}
.info-item span {
color: #64748b;
font-size: 14px;
}
/* Pasos de Seguimiento */
.tracking-steps {
margin: 20px 0;
}
.step {
background: #f8fafc;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 8px;
border-left: 4px solid #667eea;
color: #374151;
font-weight: 500;
}
/* Cuadrícula de Pagos */
.payment-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin: 20px 0;
}
.payment-category {
background: #f8fafc;
padding: 20px;
border-radius: 12px;
text-align: center;
border: 2px solid #e2e8f0;
transition: all 0.3s ease;
}
.payment-category:hover {
border-color: #667eea;
transform: translateY(-2px);
}
.payment-category h4 {
color: #374151;
margin-bottom: 8px;
font-size: 16px;
}
.payment-category p {
color: #64748b;
font-size: 14px;
margin: 0;
}
/* Cronología de Facturación */
.billing-timeline {
margin: 20px 0;
}
.timeline-item {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
}
.timeline-dot {
width: 12px;
height: 12px;
background: #667eea;
border-radius: 50%;
flex-shrink: 0;
}
.timeline-content strong {
display: block;
color: #374151;
margin-bottom: 4px;
}
.timeline-content span {
color: #64748b;
font-size: 14px;
}
/* Cuadrícula de Requisitos */
.requirements-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin: 20px 0;
}
.req-section {
background: #f8fafc;
padding: 20px;
border-radius: 12px;
border: 1px solid #e2e8f0;
}
.req-section h4 {
color: #374151;
margin-bottom: 12px;
font-size: 16px;
}
.req-section ul {
margin: 0;
padding-left: 16px;
}
.req-section li {
color: #64748b;
margin-bottom: 6px;
}
/* Pasos de Restablecimiento */
.reset-steps {
background: #f8fafc;
padding: 20px;
border-radius: 12px;
margin: 16px 0;
}
.reset-steps li {
margin-bottom: 12px;
color: #374151;
font-weight: 500;
}
.note {
background: #fef3cd;
border: 1px solid #fde68a;
padding: 12px 16px;
border-radius: 8px;
margin: 16px 0;
color: #92400e;
}
/* Cuadrícula de Contacto */
.contact-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin: 20px 0;
}
.contact-method {
background: #f8fafc;
padding: 24px;
border-radius: 12px;
text-align: center;
border: 2px solid #e2e8f0;
transition: all 0.3s ease;
}
.contact-method:hover {
border-color: #667eea;
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.15);
}
.contact-icon {
font-size: 32px;
margin-bottom: 12px;
}
.contact-method h4 {
color: #374151;
margin-bottom: 8px;
font-size: 16px;
}
.contact-method p {
color: #667eea;
font-weight: 600;
margin-bottom: 4px;
}
.contact-method span {
color: #64748b;
font-size: 14px;
}
/* Animaciones */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.question-item {
animation: slideIn 0.3s ease;
}
/* Diseño Responsivo */
@media (max-width: 768px) {
body {
padding: 10px;
}
.faq-tabs-container {
border-radius: 12px;
}
.faq-header {
padding: 30px 20px;
}
.faq-header h1 {
font-size: 2rem;
}
.tab-button {
padding: 16px 20px;
min-width: 120px;
}
.tab-panel {
padding: 20px;
}
.question-header {
padding: 16px 20px;
}
.question-item.active .question-answer {
padding: 0 20px 20px;
}
.info-grid,
.payment-grid,
.requirements-grid,
.contact-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.faq-header h1 {
font-size: 1.75rem;
}
.tab-button {
padding: 12px 16px;
min-width: 100px;
font-size: 13px;
}
.tab-icon {
font-size: 16px;
}
.question-header h3 {
font-size: 1rem;
}
}Funcionalidad JavaScript
class TabsFAQ {
constructor() {
this.tabButtons = document.querySelectorAll('.tab-button');
this.tabPanels = document.querySelectorAll('.tab-panel');
this.tabIndicator = document.querySelector('.tab-indicator');
this.searchInputs = document.querySelectorAll('.search-input');
this.questionItems = document.querySelectorAll('.question-item');
this.init();
}
init() {
this.bindEvents();
this.setupKeyboardNavigation();
this.updateIndicator();
this.handleURLHash();
}
bindEvents() {
// Cambio de pestañas
this.tabButtons.forEach(button => {
button.addEventListener('click', (e) => {
this.switchTab(e.target.closest('.tab-button'));
});
});
// Alternar preguntas
document.addEventListener('click', (e) => {
const questionHeader = e.target.closest('.question-header');
if (questionHeader) {
this.toggleQuestion(questionHeader.parentElement);
}
});
// Funcionalidad de búsqueda
this.searchInputs.forEach(input => {
input.addEventListener('input', (e) => {
this.filterQuestions(e.target.value, e.target.dataset.searchTarget);
});
});
// Manejar navegador atrás/adelante
window.addEventListener('hashchange', () => {
this.handleURLHash();
});
}
setupKeyboardNavigation() {
// Navegación de pestañas
document.addEventListener('keydown', (e) => {
if (e.target.classList.contains('tab-button')) {
this.handleTabKeyboard(e);
} else if (e.target.closest('.question-header')) {
this.handleQuestionKeyboard(e);
}
});
}
handleTabKeyboard(e) {
const currentTab = e.target;
const tabList = Array.from(this.tabButtons);
const currentIndex = tabList.indexOf(currentTab);
switch (e.key) {
case 'ArrowLeft':
e.preventDefault();
const prevIndex = currentIndex === 0 ? tabList.length - 1 : currentIndex - 1;
this.switchTab(tabList[prevIndex]);
tabList[prevIndex].focus();
break;
case 'ArrowRight':
e.preventDefault();
const nextIndex = (currentIndex + 1) % tabList.length;
this.switchTab(tabList[nextIndex]);
tabList[nextIndex].focus();
break;
case 'Home':
e.preventDefault();
this.switchTab(tabList[0]);
tabList[0].focus();
break;
case 'End':
e.preventDefault();
this.switchTab(tabList[tabList.length - 1]);
tabList[tabList.length - 1].focus();
break;
}
}
handleQuestionKeyboard(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
const questionItem = e.target.closest('.question-item');
this.toggleQuestion(questionItem);
}
}
switchTab(targetButton) {
const targetTab = targetButton.dataset.tab;
// Actualizar estados de botones
this.tabButtons.forEach(button => {
button.classList.remove('active');
button.setAttribute('aria-selected', 'false');
});
targetButton.classList.add('active');
targetButton.setAttribute('aria-selected', 'true');
// Actualizar estados de paneles
this.tabPanels.forEach(panel => {
panel.classList.remove('active');
});
const targetPanel = document.getElementById(`${targetTab}-panel`);
if (targetPanel) {
targetPanel.classList.add('active');
}
// Actualizar posición del indicador
this.updateIndicator();
// Actualizar hash URL
history.replaceState(null, null, `#${targetTab}`);
// Limpiar búsqueda en nueva pestaña
const searchInput = targetPanel.querySelector('.search-input');
if (searchInput) {
searchInput.value = '';
this.filterQuestions('', targetTab);
}
}
updateIndicator() {
const activeButton = document.querySelector('.tab-button.active');
if (activeButton && this.tabIndicator) {
const buttonRect = activeButton.getBoundingClientRect();
const containerRect = activeButton.parentElement.getBoundingClientRect();
this.tabIndicator.style.left = `${activeButton.offsetLeft}px`;
this.tabIndicator.style.width = `${activeButton.offsetWidth}px`;
}
}
toggleQuestion(questionItem) {
const isActive = questionItem.classList.contains('active');
const toggleBtn = questionItem.querySelector('.toggle-btn');
const answer = questionItem.querySelector('.question-answer');
if (isActive) {
questionItem.classList.remove('active');
toggleBtn.setAttribute('aria-expanded', 'false');
answer.style.maxHeight = '0';
} else {
questionItem.classList.add('active');
toggleBtn.setAttribute('aria-expanded', 'true');
answer.style.maxHeight = answer.scrollHeight + 'px';
// Desplazamiento suave a la pregunta
setTimeout(() => {
questionItem.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}, 150);
}
}
filterQuestions(searchTerm, category) {
const term = searchTerm.toLowerCase().trim();
const categoryQuestions = document.querySelectorAll(`[data-category="${category}"]`);
let visibleCount = 0;
categoryQuestions.forEach(item => {
const question = item.querySelector('h3').textContent.toLowerCase();
const answer = item.querySelector('.question-answer').textContent.toLowerCase();
const matches = question.includes(term) || answer.includes(term);
if (matches || term === '') {
item.style.display = 'block';
visibleCount++;
// Resaltar términos de búsqueda
if (term !== '') {
this.highlightSearchTerm(item, term);
} else {
this.removeHighlights(item);
}
} else {
item.style.display = 'none';
// Cerrar elementos ocultos
if (item.classList.contains('active')) {
this.toggleQuestion(item);
}
}
});
// Mostrar mensaje sin resultados si es necesario
const activePanel = document.querySelector('.tab-panel.active');
const questionsList = activePanel.querySelector('.questions-list');
let noResultsMsg = activePanel.querySelector('.no-results-message');
if (visibleCount === 0 && term !== '') {
if (!noResultsMsg) {
noResultsMsg = document.createElement('div');
noResultsMsg.className = 'no-results-message';
noResultsMsg.innerHTML = `
<div style="text-align: center; padding: 40px; color: #64748b;">
<div style="font-size: 48px; margin-bottom: 16px; opacity: 0.5;">🔍</div>
<h3 style="margin-bottom: 8px; color: #374151;">No se encontraron preguntas</h3>
<p>Intente ajustar sus términos de búsqueda o navegue todas las preguntas arriba.</p>
</div>
`;
questionsList.appendChild(noResultsMsg);
}
noResultsMsg.style.display = 'block';
} else if (noResultsMsg) {
noResultsMsg.style.display = 'none';
}
}
highlightSearchTerm(item, term) {
const question = item.querySelector('h3');
const originalText = question.textContent;
const regex = new RegExp(`(${term})`, 'gi');
const highlightedText = originalText.replace(regex, '<mark style="background: #fef08a; padding: 2px 4px; border-radius: 3px;">$1</mark>');
if (originalText !== highlightedText) {
question.innerHTML = highlightedText;
}
}
removeHighlights(item) {
const question = item.querySelector('h3');
const marks = question.querySelectorAll('mark');
marks.forEach(mark => {
mark.outerHTML = mark.textContent;
});
}
handleURLHash() {
const hash = window.location.hash.substring(1);
if (hash) {
const targetButton = document.querySelector(`[data-tab="${hash}"]`);
if (targetButton) {
this.switchTab(targetButton);
}
}
}
}
// Inicializar las pestañas FAQ cuando el DOM esté cargado
document.addEventListener('DOMContentLoaded', () => {
new TabsFAQ();
// Agregar desplazamiento suave
document.documentElement.style.scrollBehavior = 'smooth';
// Agregar animación de carga
const questionItems = document.querySelectorAll('.question-item');
questionItems.forEach((item, index) => {
item.style.opacity = '0';
item.style.transform = 'translateY(20px)';
setTimeout(() => {
item.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
item.style.opacity = '1';
item.style.transform = 'translateY(0)';
}, index * 100);
});
// Manejar redimensionamiento de ventana para indicador
window.addEventListener('resize', () => {
const faq = new TabsFAQ();
faq.updateIndicator();
});
});Ejemplos de Uso
Implementación Básica
// Inicializar las pestañas FAQ
const faq = new TabsFAQ();
// Cambiar programáticamente a una pestaña específica
faq.switchTab(document.querySelector('[data-tab="facturacion"]'));
// Buscar programáticamente dentro de una categoría
faq.filterQuestions('pago', 'facturacion');Configuración Personalizada
// Opciones de configuración extendidas
const faq = new TabsFAQ({
autoCollapse: true, // Auto-colapsar otras preguntas al abrir una
searchHighlight: true, // Habilitar resaltado de términos de búsqueda
urlHash: true, // Habilitar navegación por hash URL
animationDuration: 300, // Duración de animación en ms
scrollOffset: 100 // Desplazamiento al abrir preguntas
});Integración con Analytics
// Rastrear cambios de pestañas e interacciones de preguntas
class AnalyticsTabsFAQ extends TabsFAQ {
switchTab(targetButton) {
super.switchTab(targetButton);
// Rastrear cambio de pestaña
gtag('event', 'faq_tab_switch', {
'tab_name': targetButton.dataset.tab
});
}
toggleQuestion(questionItem) {
super.toggleQuestion(questionItem);
const question = questionItem.querySelector('h3').textContent;
const isOpening = questionItem.classList.contains('active');
// Rastrear interacción de pregunta
gtag('event', 'faq_question_toggle', {
'question': question,
'action': isOpening ? 'open' : 'close'
});
}
}Compatibilidad del Navegador
- Chrome 60+
- Firefox 55+
- Safari 12+
- Edge 79+
Características Incluidas
✅ Organización por pestañas basada en categorías
✅ Transiciones suaves de pestañas con indicador
✅ Búsqueda individual por pestaña
✅ Soporte de navegación por teclado
✅ Navegación por hash URL
✅ Insignias de preguntas con conteos
✅ Diseño responsivo
✅ Atributos de accesibilidad ARIA
✅ Opciones de estilo personalizado
✅ Sin dependencias externas
✅ Estructura amigable para SEO
✅ Interfaz táctil amigable
✅ Animaciones de carga
✅ Resaltado de términos de búsqueda
Opciones de Personalización
- Colores: Modificar propiedades personalizadas CSS para tematización
- Velocidad de Animación: Ajustar duraciones de transición
- Diseño: Personalizar espaciado y tipografía
- Iconos: Reemplazar iconos de pestañas con personalizados
- Búsqueda: Configurar comportamiento de búsqueda y resaltado
- Pestañas: Agregar o quitar categorías según sea necesario
Este componente FAQ con pestañas proporciona una forma organizada y profesional de mostrar preguntas frecuentes con interacciones suaves y soporte completo de accesibilidad.
HTML
1
líneas
CSS
1
líneas
JavaScript
1
líneas
<!-- HTML content will be added here -->