Tabla comparativa de precios
Matriz de funciones por plan, con columna destacada y primera columna fija.
Compara los planes
| Función | Gratis$0Empezar | Pro$19 / mesElegir Pro | Negocio$59 / mesElegir Negocio |
|---|---|---|---|
| Uso | |||
| Proyectos | 3 | Ilimitados | Ilimitados |
| Personas en el equipo | 1 | 5 | 25 |
| Almacenamiento | 1 GB | 50 GB | 500 GB |
| Funciones | |||
| Exportar a PDF y CSV | No incluido | Incluido | Incluido |
| Automatizaciones | No incluido | Incluido | Incluido |
| Historial de cambios | 7 días | 90 días | Ilimitado |
| Inicio de sesión único | No incluido | No incluido | Incluido |
| Soporte | |||
| Centro de ayuda | Incluido | Incluido | Incluido |
| Soporte por correo | No incluido | Incluido | Incluido |
| Acompañamiento dedicado | No incluido | No incluido | Incluido |
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
/**
* Tabla comparativa de planes: encabezado con precio y botón, filas agrupadas por tema, columna destacada y primera columna fija
* al desplazar en pantallas estrechas. Los valores pueden ser sí/no (true/false) o texto. Solo CSS.
*
* <PricingTable
* plans={[{ name: "Gratis", price: "$0", cta: { label: "Empezar", href: "/registro" } }, { name: "Pro", price: "$19", cta: { label: "Elegir Pro", href: "/pro" } }]}
* groups={[{ title: "Uso", rows: [{ label: "Proyectos", values: ["3", "Ilimitados"] }, { label: "Exportar", values: [false, true] }] }]}
* highlight={1}
* />
*/
import type { CSSProperties } from "react";
export type TablePlan = { name: string; price: string; period?: string; cta: { label: string; href: string } };
export type TableRow = { label: string; values: (boolean | string)[] };
export type TableGroup = { title: string; rows: TableRow[] };
export type PricingTableProps = {
title?: string;
plans?: TablePlan[];
groups?: TableGroup[];
/** Índice del plan destacado. */
highlight?: number;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcpc-root { --gcpc-accent: #ff5a1f; box-sizing: border-box; padding: clamp(32px, 5vw, 72px) clamp(14px, 4vw, 56px); background: #fff; color: #17181d; font-family: inherit; }
.gcpc-title { margin: 0 0 clamp(20px, 3vw, 36px); max-width: 18ch; font-size: clamp(30px, 4.2vw, 52px); font-weight: 700; line-height: 1.04; letter-spacing: -.04em; text-wrap: balance; }
.gcpc-scroll { overflow-x: auto; }
.gcpc-table { width: 100%; min-width: 640px; border-collapse: separate; border-spacing: 0; font-size: 15px; }
.gcpc-table th, .gcpc-table td { padding: 14px 16px; border-bottom: 1px solid #e7e7ec; text-align: center; }
.gcpc-table th:first-child, .gcpc-table td:first-child { position: sticky; left: 0; z-index: 1; width: 32%; background: #fff; text-align: left; font-weight: 500; }
.gcpc-table thead th { padding-block: 22px 20px; vertical-align: bottom; border-bottom: 2px solid #17181d; font-weight: 600; }
.gcpc-name { display: block; font-size: 16px; font-weight: 700; }
.gcpc-price { display: block; margin: 6px 0 14px; font-size: 32px; font-weight: 700; letter-spacing: -.04em; line-height: 1; }
.gcpc-price small { color: #6d6e78; font-size: 13px; font-weight: 500; letter-spacing: 0; }
.gcpc-cta { display: inline-flex; align-items: center; justify-content: center; min-height: 40px; padding: 0 18px; border: 1.5px solid #17181d; border-radius: 10px; color: #17181d; font-size: 14px; font-weight: 600; text-decoration: none; transition: background-color .2s, color .2s; }
.gcpc-cta:hover { background: #17181d; color: #fff; }
.gcpc-cta:focus-visible { outline: 2px solid var(--gcpc-accent); outline-offset: 3px; }
.gcpc-group th { padding: 28px 16px 10px; border-bottom: 1px solid #17181d; color: #6d6e78; font-size: 12px; font-weight: 700; letter-spacing: .08em; text-align: left; text-transform: uppercase; }
.gcpc-group th:first-child { position: sticky; left: 0; background: #fff; }
.gcpc-table .gcpc-hl { background: #fff4ee; }
.gcpc-table thead .gcpc-hl { position: relative; border-top: 4px solid var(--gcpc-accent); }
.gcpc-table thead .gcpc-hl .gcpc-cta { border-color: var(--gcpc-accent); background: var(--gcpc-accent); color: #fff; }
.gcpc-yes { width: 22px; height: 22px; color: var(--gcpc-accent); }
.gcpc-no { color: #b7b8c2; }
.gcpc-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
@media (prefers-reduced-motion: reduce) { .gcpc-cta { transition: none; } }
`;
const DEFAULT_PLANS: TablePlan[] = [
{ name: "Gratis", price: "$0", cta: { label: "Empezar", href: "#registro" } },
{ name: "Pro", price: "$19", period: "/ mes", cta: { label: "Elegir Pro", href: "#pro" } },
{ name: "Negocio", price: "$59", period: "/ mes", cta: { label: "Elegir Negocio", href: "#negocio" } },
];
const DEFAULT_GROUPS: TableGroup[] = [
{ title: "Uso", rows: [
{ label: "Proyectos", values: ["3", "Ilimitados", "Ilimitados"] },
{ label: "Personas en el equipo", values: ["1", "5", "25"] },
{ label: "Almacenamiento", values: ["1 GB", "50 GB", "500 GB"] },
] },
{ title: "Funciones", rows: [
{ label: "Exportar a PDF y CSV", values: [false, true, true] },
{ label: "Automatizaciones", values: [false, true, true] },
{ label: "Historial de cambios", values: ["7 días", "90 días", "Ilimitado"] },
{ label: "Inicio de sesión único", values: [false, false, true] },
] },
{ title: "Soporte", rows: [
{ label: "Centro de ayuda", values: [true, true, true] },
{ label: "Soporte por correo", values: [false, true, true] },
{ label: "Acompañamiento dedicado", values: [false, false, true] },
] },
];
export function PricingTable({ title = "Compara los planes", plans = DEFAULT_PLANS, groups = DEFAULT_GROUPS, highlight = 1, className, style }: PricingTableProps) {
return (
<>
<style>{CSS}</style>
<section className={["gcpc-root", className].filter(Boolean).join(" ")} style={style}>
<h2 className="gcpc-title">{title}</h2>
<div className="gcpc-scroll" tabIndex={0} role="region" aria-label="Tabla comparativa de planes">
<table className="gcpc-table">
<thead>
<tr>
<th scope="col"><span className="gcpc-sr">Función</span></th>
{plans.map((plan, index) => (
<th scope="col" key={plan.name} className={index === highlight ? "gcpc-hl" : undefined}>
<span className="gcpc-name">{plan.name}</span>
<span className="gcpc-price">{plan.price}{plan.period ? <small> {plan.period}</small> : null}</span>
<a className="gcpc-cta" href={plan.cta.href}>{plan.cta.label}</a>
</th>
))}
</tr>
</thead>
{groups.map((group) => (
<tbody key={group.title}>
<tr className="gcpc-group"><th scope="colgroup" colSpan={plans.length + 1}>{group.title}</th></tr>
{group.rows.map((row) => (
<tr key={row.label}>
<th scope="row">{row.label}</th>
{plans.map((plan, index) => {
const value = row.values[index];
return (
<td key={plan.name} className={index === highlight ? "gcpc-hl" : undefined}>
{value === true ? (
<><svg className="gcpc-yes" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5" /></svg><span className="gcpc-sr">Incluido</span></>
) : value === false || value === undefined ? (
<><span className="gcpc-no" aria-hidden="true">—</span><span className="gcpc-sr">No incluido</span></>
) : value}
</td>
);
})}
</tr>
))}
</tbody>
))}
</table>
</div>
</section>
</>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/precios-comparativaEjemplo de uso de Tabla comparativa de precios
Después de copiar pricing-table.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { PricingTable } from "./pricing-table";
export default function Ejemplo() {
return <PricingTable title="Ejemplo" />;
}Propiedades de PricingTable
PricingTable acepta 6 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
title? | string | Opcional. |
plans? | TablePlan[] | Opcional. |
groups? | TableGroup[] | Opcional. |
highlight? | number | Índice del plan destacado. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 126 líneas (7 KB) y no necesita @keyframes. Esto es lo que hace Tabla comparativa de precios 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.
Acerca del producto
Una tabla para comparar planes: encabezado con precio y botón, filas agrupadas por tema, valores de sí/no o texto, una columna destacada y la primera columna fija al desplazar en pantallas estrechas. Usa una tabla semántica con encabezados de fila y de columna.
Incluye
Una sección completa en un solo archivo React + TypeScript, con los estilos dentro. No usa Tailwind ni bibliotecas externas.
- Tabla HTML semántica con scope en filas y columnas
- Valores sí/no con texto para lectores de pantalla
- Columna destacada configurable (highlight)
- Región con desplazamiento accesible por teclado
Cómo usarlo
Pasa plans, groups (con rows y values) y el índice highlight.