Una página de precios convierte o no convierte. El componente que más peso tiene en cualquier página de precios SaaS es la tabla comparativa, el lugar donde los clientes potenciales colocan los planes uno al lado del otro y deciden si vale la pena mejorar. Una tabla mal construida pierde ventas. Una tabla clara, fácil de leer y adaptada a móvil las cierra. Esta guía explica cómo construir tablas comparativas de calidad profesional en React, desde rejillas básicas hasta matrices de funciones completas con toggles de facturación.
La rejilla comparativa básica
Empieza por la estructura más simple posible: una rejilla donde cada columna es un plan y cada fila es una función. Los props tipados con TypeScript mantienen limpia la capa de datos:
interface Plan {
name: string;
price: { monthly: number; annual: number };
features: Record<string, boolean | string>;
highlighted?: boolean;
}
interface ComparisonTableProps {
plans: Plan[];
features: string[];
billingCycle: "monthly" | "annual";
}
El array features define el orden de las filas. El record features de cada plan asocia los nombres de funciones a un boolean (marca de verificación o guion) o a un valor de tipo string ("10 GB", "Ilimitado"). Este enfoque es lo bastante flexible para cubrir la mayoría de estructuras de precios SaaS sin caer en la sobreingeniería de los tipos.
Matriz de funciones con marcas de verificación
La matriz de funciones es el núcleo de cualquier página comparativa. Renderízala como un CSS Grid en lugar de una <table> HTML, ya que las rejillas te dan mucho más control sobre el comportamiento responsive:
export default function ComparisonTable({ plans, features, billingCycle }: ComparisonTableProps) {
const columns = plans.length + 1; // +1 for feature label column
return (
<div
style={{
display: "grid",
gridTemplateColumns: `minmax(200px, 1.5fr) repeat(${plans.length}, 1fr)`,
gap: 0,
width: "100%",
maxWidth: "var(--container-max-width)",
margin: "0 auto",
}}
>
{/* Header row */}
<div style={{ padding: "1.5rem 1rem" }} />
{plans.map((plan) => (
<div
key={plan.name}
style={{
padding: "1.5rem 1rem",
textAlign: "center",
background: plan.highlighted ? "var(--color-accent-subtle)" : "transparent",
borderRadius: plan.highlighted ? "var(--radius-lg) var(--radius-lg) 0 0" : 0,
}}
>
<h3 style={{ fontSize: "1.25rem", fontWeight: 700, color: "var(--color-foreground)" }}>
{plan.name}
</h3>
<p style={{ fontSize: "2rem", fontWeight: 700, marginTop: "0.5rem" }}>
${billingCycle === "monthly" ? plan.price.monthly : plan.price.annual}
<span style={{ fontSize: "0.875rem", fontWeight: 400, color: "var(--color-foreground-muted)" }}>
/mo
</span>
</p>
</div>
))}
{/* Feature rows */}
{features.map((feature, i) => (
<>
<div
key={`label-${feature}`}
style={{
padding: "1rem",
borderTop: "1px solid var(--color-border)",
color: "var(--color-foreground-muted)",
fontSize: "0.9375rem",
display: "flex",
alignItems: "center",
}}
>
{feature}
</div>
{plans.map((plan) => {
const value = plan.features[feature];
return (
<div
key={`${plan.name}-${feature}`}
style={{
padding: "1rem",
borderTop: "1px solid var(--color-border)",
textAlign: "center",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: plan.highlighted ? "var(--color-accent-subtle)" : "transparent",
}}
>
{value === true && <CheckIcon />}
{value === false && <span style={{ color: "var(--color-foreground-muted)" }}>, </span>}
{typeof value === "string" && (
<span style={{ fontWeight: 500 }}>{value}</span>
)}
</div>
);
})}
</>
))}
</div>
);
}
La marca plan.highlighted añade un fondo de acento sutil a la columna del plan recomendado. Ese peso visual atrae la mirada de forma natural, sin que haga falta una insignia de "Más popular" (aunque puedes añadirla).
Toggle de facturación: anual frente a mensual
El toggle de facturación es un componente controlado que se coloca encima de la tabla. Guarda el estado en el componente padre y pasa billingCycle hacia abajo:
"use client";
import { useState } from "react";
export default function PricingSection() {
const [billing, setBilling] = useState<"monthly" | "annual">("annual");
return (
<section style={{ padding: "var(--section-padding-y-lg) 0" }}>
<div style={{ display: "flex", justifyContent: "center", gap: "0.5rem", marginBottom: "3rem" }}>
{(["monthly", "annual"] as const).map((cycle) => (
<button
key={cycle}
onClick={() => setBilling(cycle)}
style={{
padding: "0.625rem 1.5rem",
borderRadius: "var(--radius-full)",
border: "1px solid var(--color-border)",
background: billing === cycle ? "var(--color-accent)" : "transparent",
color: billing === cycle ? "#fff" : "var(--color-foreground-muted)",
fontWeight: 500,
fontSize: "0.875rem",
cursor: "pointer",
transition: "all var(--duration-normal) var(--ease-out)",
}}
>
{cycle === "monthly" ? "Monthly" : "Annual (save 20%)"}
</button>
))}
</div>
<ComparisonTable plans={plans} features={features} billingCycle={billing} />
</section>
);
}
Consejo: pon siempre "annual" como valor por defecto. Los planes anuales tienen un LTV más alto y la mayoría de las empresas SaaS quieren empujar a los usuarios hacia ellos. La etiqueta del toggle debería indicar el ahorro de forma explícita, ya que "Annual (save 20%)" convierte mejor que un simple "Annual".
Destacar el plan recomendado
Más allá del color de fondo sutil, puedes añadir un acento en el borde superior y una insignia:
{plan.highlighted && (
<div
style={{
position: "absolute",
top: "-1px",
left: 0,
right: 0,
height: "3px",
background: "var(--color-accent)",
borderRadius: "var(--radius-lg) var(--radius-lg) 0 0",
}}
/>
)}
Esta barra de acento de 3px se ve sin resultar intrusiva. Combínala con una pequeña insignia tipo pastilla que diga "Recomendado" o "Más popular" y la columna destacada se convertirá en la opción por defecto evidente.
Tablas responsive para móvil
Las tablas comparativas son célebres por lo difíciles que resultan en pantallas pequeñas. Una rejilla de 4 columnas no cabe en un viewport de 375px. Hay dos enfoques que funcionan bien:
Scroll horizontal: envuelve la rejilla en un contenedor con overflow-x: auto y un ancho mínimo en la propia rejilla. Añade un degradado sutil que se desvanezca en el borde derecho para indicar que se puede desplazar:
<div style={{ position: "relative", overflow: "hidden" }}>
<div style={{ overflowX: "auto", WebkitOverflowScrolling: "touch" }}>
<div style={{ minWidth: "700px" }}>
<ComparisonTable {...props} />
</div>
</div>
<div
aria-hidden
style={{
position: "absolute",
top: 0,
right: 0,
bottom: 0,
width: "40px",
background: "linear-gradient(to right, transparent, var(--color-background))",
pointerEvents: "none",
}}
/>
</div>
Tarjetas apiladas: en móvil, cambia de una rejilla a tarjetas de plan apiladas, donde cada tarjeta enumera sus propias funciones. Usa una media query o un hook useMediaQuery para intercambiar los diseños. Este enfoque da más trabajo, pero ofrece una mejor experiencia móvil en páginas con 4 o más planes.
Accesibilidad
Las tablas comparativas tienen que poder navegarse con lectores de pantalla. Si usas CSS Grid en lugar de <table>, añade roles ARIA:
role="table"en el contenedor de la rejillarole="row"en cada fila lógicarole="columnheader"en las celdas del nombre del planrole="rowheader"en las celdas de etiqueta de funciónrole="cell"en las celdas de valor
Los iconos de marca de verificación necesitan aria-label="Included" y los iconos de guion necesitan aria-label="Not included". Sin esto, los usuarios de lectores de pantalla no oyen nada en esas celdas.
Secciones comparativas listas para producción
Construir una tabla comparativa que gestione bien los toggles de facturación, los planes destacados, los diseños responsive y la accesibilidad supone una cantidad de trabajo nada despreciable. El catálogo de precios de Incubator incluye varias variantes de tabla comparativa, tarjetas lado a lado, matrices de funciones completas y diseños con toggle, todas construidas con el sistema de tokens CSS y los props de TypeScript descritos en esta guía. Si necesitas una página comparativa dedicada, revisa el catálogo de secciones comparativas para ver diseños de página completa con agrupación por categorías y filas plegables. Cada variante está lista para copiar y pegar en tu proyecto Next.js.
Relacionado en incubator
- Componentes de sección de precios: tablas de precios listas para copiar y pegar con toggles de facturación.
- Componentes de sección de estadísticas: destacados numéricos de funciones para páginas de comparación de planes.
- Componentes de sección de funciones: rejillas de funciones que combinan de forma natural con las tablas de precios.
- Incubator frente a Tailwind UI: descubre cómo se comparan las secciones para copiar y pegar.
- Catálogo completo de componentes: explora todos los tipos de sección disponibles en el registro.