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
—
Compatibilidad del Navegador
No
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">
<div class="faq-header">
<h1>Preguntas Frecuentes</h1>
<p>Encuentra respuestas a preguntas comunes organizadas por categoría</p>
</div>
<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>
<div class="tab-indicator"></div>
</div>
<div class="tab-content">
<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>
<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>
<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>
<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;
}.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;
}.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;
}.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;
}.tab-content {
position: relative;
min-height: 400px;
}
.tab-panel {
display: none;
padding: 30px;
animation: fadeIn 0.3s ease;
}
.tab-panel.active {
display: block;
}.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;
}.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;
}.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;
}.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;
}.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;
}.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;
}.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;
}.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;
}.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;
}@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;
}@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() {
this.tabButtons.forEach(button => {
button.addEventListener('click', (e) => {
this.switchTab(e.target.closest('.tab-button'));
});
});
document.addEventListener('click', (e) => {
const questionHeader = e.target.closest('.question-header');
if (questionHeader) {
this.toggleQuestion(questionHeader.parentElement);
}
});
this.searchInputs.forEach(input => {
input.addEventListener('input', (e) => {
this.filterQuestions(e.target.value, e.target.dataset.searchTarget);
});
});
window.addEventListener('hashchange', () => {
this.handleURLHash();
});
}
setupKeyboardNavigation() {
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;
this.tabButtons.forEach(button => {
button.classList.remove('active');
button.setAttribute('aria-selected', 'false');
});
targetButton.classList.add('active');
targetButton.setAttribute('aria-selected', 'true');
this.tabPanels.forEach(panel => {
panel.classList.remove('active');
});
const targetPanel = document.getElementById(`${targetTab}-panel`);
if (targetPanel) {
targetPanel.classList.add('active');
}
this.updateIndicator();
history.replaceState(null, null, `#${targetTab}`);
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';
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++;
if (term !== '') {
this.highlightSearchTerm(item, term);
} else {
this.removeHighlights(item);
}
} else {
item.style.display = 'none';
if (item.classList.contains('active')) {
this.toggleQuestion(item);
}
}
});
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);
}
}
}
}
document.addEventListener('DOMContentLoaded', () => {
new TabsFAQ();
document.documentElement.style.scrollBehavior = 'smooth';
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);
});
window.addEventListener('resize', () => {
const faq = new TabsFAQ();
faq.updateIndicator();
});
});
Ejemplos de Uso
Implementación Básica
const faq = new TabsFAQ();
faq.switchTab(document.querySelector('[data-tab="facturacion"]'));
faq.filterQuestions('pago', 'facturacion');
Configuración Personalizada
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
class AnalyticsTabsFAQ extends TabsFAQ {
switchTab(targetButton) {
super.switchTab(targetButton);
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');
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.