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.
"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.
npx shadcn@latest add https://www.gassierconsulting.com/r/precios-materialEjemplo 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.
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.
| Propiedad | Tipo | Descripción |
|---|---|---|
title? | string | Opcional. |
plans? | PreciosMaterialPlan[] | Opcional. |
currency? | string | Opcional. |
cta? | string | Opcional. |
onChoose? | (plan: string, billing: "mensual" | "anual") => void | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
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".