Ir al contenido
Evaluar mi proyecto
Componentes · PreciosDisponible

Tabla comparativa de precios

Matriz de funciones por plan, con columna destacada y primera columna fija.

Compara los planes

FunciónGratis$0EmpezarPro$19 / mesElegir ProNegocio$59 / mesElegir Negocio
Uso
Proyectos3IlimitadosIlimitados
Personas en el equipo1525
Almacenamiento1 GB50 GB500 GB
Funciones
Exportar a PDF y CSVNo incluidoIncluidoIncluido
AutomatizacionesNo incluidoIncluidoIncluido
Historial de cambios7 días90 díasIlimitado
Inicio de sesión únicoNo incluidoNo incluidoIncluido
Soporte
Centro de ayudaIncluidoIncluidoIncluido
Soporte por correoNo incluidoIncluidoIncluido
Acompañamiento dedicadoNo incluidoNo incluidoIncluido

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

pricing-table.tsx126 líneas
Descargar
/**
 * 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.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/precios-comparativa

Ejemplo 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.

ejemplo.tsx5 líneas
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.

PropiedadTipoDescripción
title?stringOpcional.
plans?TablePlan[]Opcional.
groups?TableGroup[]Opcional.
highlight?numberÍndice del plan destacado.
className?stringOpcional.
style?CSSPropertiesOpcional.

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.
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React · Next.js · Sin Tailwind

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.