Ir al contenido
Evaluar mi proyecto
Componentes · PreciosDisponible

Precios Material Design 3

Botón segmentado para el periodo, tarjetas outlined, tonal y filled, y chips de beneficios según Material 3.

Planes simples, sin letra pequeña

Básico

Para empezar sin compromiso.

$0/mes

  • 3 espacios
  • Almacenamiento 5 GB

Plus

Para quienes trabajan a diario.

$6/mes

  • Espacios ilimitados
  • 200 GB
  • Historial 90 días

Equipo

Para colaborar con control.

$15/mes

  • Todo lo de Plus
  • Roles
  • Auditoría

Código completo

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

precios-material.tsx104 líneas
Descargar
"use client";

import { useState, type CSSProperties } from "react";

/** Precios Material Design 3: botón segmentado para el periodo, tarjetas elevated/filled/outlined y chips de beneficios. Datos de muestra. React + TypeScript, sin dependencias. */
export type PreciosMaterialPlan = { name: string; monthly: number; yearly: number; desc: string; features: string[]; featured?: boolean };

export type PreciosMaterialProps = {
  title?: string;
  plans?: PreciosMaterialPlan[];
  currency?: string;
  cta?: string;
  onChoose?: (plan: string, billing: "mensual" | "anual") => void;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcpma-root { box-sizing: border-box; container-type: inline-size; width: 100%; padding: clamp(24px, 5cqi, 64px) clamp(16px, 4cqi, 56px); background: #fef7ff; color: #1d1b20; font: 400 14px/20px Roboto, "Google Sans", ui-sans-serif, system-ui, sans-serif; letter-spacing: .25px; }
.gcpma-root *, .gcpma-root *::before, .gcpma-root *::after { box-sizing: border-box; }
.gcpma-root button { font: inherit; letter-spacing: inherit; }
.gcpma-root :focus-visible { outline: 3px solid #1d1b20; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .gcpma-root *, .gcpma-root *::before, .gcpma-root *::after { transition-duration: .01ms !important; } }
.gcpma-head { display: grid; justify-items: center; gap: 20px; margin-bottom: 32px; text-align: center; }
.gcpma-title { margin: 0; font: 400 clamp(32px, 5cqi, 52px)/1.1 Roboto, "Google Sans", ui-sans-serif, sans-serif; letter-spacing: -.01em; text-wrap: balance; }
.gcpma-seg { display: inline-flex; height: 40px; border-radius: 20px; box-shadow: inset 0 0 0 1px #79747e; overflow: hidden; }
.gcpma-seg button { display: inline-flex; align-items: center; gap: 8px; padding: 0 20px; border: 0; background: none; color: #1d1b20; font-weight: 500; letter-spacing: .1px; cursor: pointer; transition: background .2s; }
.gcpma-seg button + button { border-left: 1px solid #79747e; }
.gcpma-seg button:hover { background: rgba(29, 27, 32, .08); }
.gcpma-seg button[aria-checked="true"] { background: #e8def8; color: #1d192b; }
.gcpma-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; align-items: stretch; }
.gcpma-card { position: relative; display: grid; align-content: start; gap: 16px; padding: 24px; border-radius: 28px; background: #fef7ff; box-shadow: inset 0 0 0 1px #cac4d0; transition: box-shadow .2s; }
.gcpma-card[data-v="1"] { background: #f3edf7; box-shadow: none; }
.gcpma-card[data-f="true"] { background: #e8def8; box-shadow: none; }
.gcpma-name { margin: 0; font: 500 22px/28px Roboto, "Google Sans", ui-sans-serif, sans-serif; letter-spacing: 0; }
.gcpma-desc { margin: 0; color: #49454f; }
.gcpma-price { display: flex; align-items: baseline; gap: 4px; }
.gcpma-price b { font: 400 45px/52px Roboto, "Google Sans", ui-sans-serif, sans-serif; letter-spacing: 0; font-variant-numeric: tabular-nums; }
.gcpma-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 0; list-style: none; }
.gcpma-chips li { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 12px 0 8px; border-radius: 8px; box-shadow: inset 0 0 0 1px #79747e; color: #49454f; font-weight: 500; letter-spacing: .1px; }
.gcpma-card[data-f="true"] .gcpma-chips li { box-shadow: inset 0 0 0 1px #7d7192; color: #1d192b; }
.gcpma-btn { position: relative; overflow: hidden; height: 40px; padding: 0 24px; border: 0; border-radius: 20px; background: #6750a4; color: #fff; font-weight: 500; letter-spacing: .1px; cursor: pointer; justify-self: start; transition: box-shadow .2s, border-radius .2s; }
.gcpma-btn::after { content: ""; position: absolute; inset: 0; background: currentColor; opacity: 0; transition: opacity .15s; }
.gcpma-btn:hover::after { opacity: .08; }
.gcpma-btn:hover { box-shadow: 0 1px 3px 1px rgba(0, 0, 0, .15), 0 1px 2px rgba(0, 0, 0, .3); }
.gcpma-btn:active { border-radius: 12px; }
.gcpma-card:not([data-f="true"]) .gcpma-btn { background: transparent; color: #6750a4; box-shadow: inset 0 0 0 1px #79747e; }
.gcpma-card:not([data-f="true"]) .gcpma-btn:hover { box-shadow: inset 0 0 0 1px #79747e; }
@container (max-width: 860px) { .gcpma-grid { grid-template-columns: minmax(0, 1fr); } }`;

const PLANS: PreciosMaterialPlan[] = [
  { name: "Básico", monthly: 0, yearly: 0, desc: "Para empezar sin compromiso.", features: ["3 espacios", "Almacenamiento 5 GB"] },
  { name: "Plus", monthly: 8, yearly: 6, desc: "Para quienes trabajan a diario.", features: ["Espacios ilimitados", "200 GB", "Historial 90 días"], featured: true },
  { name: "Equipo", monthly: 18, yearly: 15, desc: "Para colaborar con control.", features: ["Todo lo de Plus", "Roles", "Auditoría"] },
];

export function PreciosMaterial({ title = "Planes simples, sin letra pequeña", plans = PLANS, currency = "$", cta = "Elegir", onChoose, className = "", style }: PreciosMaterialProps) {
  const [yearly, setYearly] = useState(true);
  return (
    <section className={`gcpma-root ${className}`} style={style}>
      <style>{CSS}</style>
      <div className="gcpma-head">
        <h2 className="gcpma-title">{title}</h2>
        <div className="gcpma-seg" role="radiogroup" aria-label="Periodo de facturación">
          <button type="button" role="radio" aria-checked={!yearly} onClick={() => setYearly(false)}>
            {!yearly ? "✓ " : ""}Mensual
          </button>
          <button type="button" role="radio" aria-checked={yearly} onClick={() => setYearly(true)}>
            {yearly ? "✓ " : ""}Anual
          </button>
        </div>
      </div>
      <div className="gcpma-grid">
        {plans.map((plan, index) => (
          <article key={plan.name} className="gcpma-card" data-f={plan.featured ? "true" : undefined} data-v={index === 2 ? "1" : undefined}>
            <h3 className="gcpma-name">{plan.name}</h3>
            <p className="gcpma-desc">{plan.desc}</p>
            <p className="gcpma-price" aria-live="polite">
              <b>
                {currency}
                {yearly ? plan.yearly : plan.monthly}
              </b>
              <span>/mes</span>
            </p>
            <ul className="gcpma-chips">
              {plan.features.map((feature) => (
                <li key={feature}>
                  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                    <path d="m5 12.5 4.5 4.5L19 7.5" />
                  </svg>
                  {feature}
                </li>
              ))}
            </ul>
            <button className="gcpma-btn" type="button" onClick={() => onChoose?.(plan.name, yearly ? "anual" : "mensual")}>
              {cta}
            </button>
          </article>
        ))}
      </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-material

Ejemplo de uso de Precios Material Design 3

Después de copiar precios-material.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { PreciosMaterial } from "./precios-material";

export default function Ejemplo() {
  return <PreciosMaterial title="Ejemplo" />;
}

Propiedades de PreciosMaterial

PreciosMaterial acepta 7 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
title?stringOpcional.
plans?PreciosMaterialPlan[]Opcional.
currency?stringOpcional.
cta?stringOpcional.
onChoose?(plan: string, billing: "mensual" | "anual") => voidOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Accesibilidad y rendimiento

El archivo tiene 104 líneas (7 KB) y no necesita @keyframes. Esto es lo que hace Precios Material Design 3 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: radiogroup, radio.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)