Precios neumorfismo
Superficies grises extruidas, selector de periodo hundido, lista en relieve inverso y botón que se presiona hacia dentro.
Precios que se sienten sólidos
Inicio
$9 /mes
- 1 usuario
- Panel básico
- Soporte por correo
Pro
$24 /mes
- 5 usuarios
- Automatizaciones
- Reportes
- Soporte prioritario
Total
$59 /mes
- Usuarios ilimitados
- Integraciones
- Auditoría
- Acuerdo de servicio
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 neumorfismo: superficies grises extruidas, selector hundido de periodo y botón que se presiona hacia dentro. Datos de muestra. React + TypeScript, sin dependencias. */
export type PreciosNeumorficoPlan = { name: string; monthly: number; yearly: number; features: string[]; featured?: boolean };
export type PreciosNeumorficoProps = {
title?: string;
plans?: PreciosNeumorficoPlan[];
currency?: string;
cta?: string;
onChoose?: (plan: string, billing: "mensual" | "anual") => void;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcpne-root { box-sizing: border-box; container-type: inline-size; width: 100%; padding: clamp(24px, 5cqi, 64px) clamp(16px, 4cqi, 56px); background: #e0e5ec; color: #4b556b; font: 600 15px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
.gcpne-root *, .gcpne-root *::before, .gcpne-root *::after { box-sizing: border-box; }
.gcpne-root button { font: inherit; -webkit-tap-highlight-color: transparent; }
.gcpne-root :focus-visible { outline: 3px solid #5b8def; outline-offset: 4px; }
@media (prefers-reduced-motion: reduce) { .gcpne-root *, .gcpne-root *::before, .gcpne-root *::after { transition-duration: .01ms !important; } }
.gcpne-head { display: grid; justify-items: center; gap: 22px; margin-bottom: 38px; text-align: center; }
.gcpne-title { margin: 0; color: #3b4658; font: 800 clamp(30px, 4.6cqi, 52px)/1.05 ui-sans-serif, system-ui, sans-serif; letter-spacing: -.035em; text-wrap: balance; }
.gcpne-seg { display: inline-flex; padding: 6px; border-radius: 999px; box-shadow: inset 5px 5px 10px #a3b1c6, inset -5px -5px 10px #fff; }
.gcpne-seg button { min-height: 44px; padding: 0 24px; border: 0; border-radius: 999px; background: transparent; color: #7a86a1; font-weight: 700; cursor: pointer; transition: box-shadow .25s, color .25s, background .25s; }
.gcpne-seg button[aria-checked="true"] { background: #e0e5ec; color: #3b4658; box-shadow: 5px 5px 10px #a3b1c6, -5px -5px 10px #fff; }
.gcpne-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 30px; }
.gcpne-card { display: grid; gap: 14px; align-content: start; padding: 30px 28px; border-radius: 28px; background: #e0e5ec; box-shadow: 10px 10px 20px #a3b1c6, -10px -10px 20px #fff; transition: box-shadow .3s; }
.gcpne-card[data-f="true"] { box-shadow: 14px 14px 28px #a3b1c6, -14px -14px 28px #fff; }
.gcpne-name { margin: 0; font-size: 16px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: #7a86a1; }
.gcpne-price { margin: 0; color: #3b4658; }
.gcpne-price b { font: 800 50px/1 ui-sans-serif, system-ui, sans-serif; letter-spacing: -.04em; font-variant-numeric: tabular-nums; }
.gcpne-price span { color: #7a86a1; }
.gcpne-list { margin: 4px 0 8px; padding: 16px; list-style: none; display: grid; gap: 10px; border-radius: 18px; box-shadow: inset 4px 4px 8px #a3b1c6, inset -4px -4px 8px #fff; }
.gcpne-list li { display: flex; gap: 10px; align-items: center; }
.gcpne-dot { width: 8px; height: 8px; border-radius: 50%; background: #5b8def; box-shadow: 0 0 0 3px #e0e5ec, 0 0 0 4px rgba(91, 141, 239, .3); flex-shrink: 0; }
.gcpne-btn { min-height: 52px; border: 0; border-radius: 16px; background: #e0e5ec; color: #3b4658; font-weight: 800; cursor: pointer; box-shadow: 6px 6px 12px #a3b1c6, -6px -6px 12px #fff; transition: box-shadow .15s, transform .15s; }
.gcpne-btn:hover { transform: translateY(-1px); }
.gcpne-btn:active { transform: none; box-shadow: inset 4px 4px 8px #a3b1c6, inset -4px -4px 8px #fff; }
.gcpne-card[data-f="true"] .gcpne-btn { color: #fff; background: linear-gradient(145deg, #6b97f5, #4b79e0); box-shadow: 6px 6px 12px #a3b1c6, -6px -6px 12px #fff; }
@container (max-width: 860px) { .gcpne-grid { grid-template-columns: minmax(0, 1fr); } }`;
const PLANS: PreciosNeumorficoPlan[] = [
{ name: "Inicio", monthly: 9, yearly: 7, features: ["1 usuario", "Panel básico", "Soporte por correo"] },
{ name: "Pro", monthly: 24, yearly: 19, features: ["5 usuarios", "Automatizaciones", "Reportes", "Soporte prioritario"], featured: true },
{ name: "Total", monthly: 59, yearly: 49, features: ["Usuarios ilimitados", "Integraciones", "Auditoría", "Acuerdo de servicio"] },
];
export function PreciosNeumorfico({ title = "Precios que se sienten sólidos", plans = PLANS, currency = "$", cta = "Elegir plan", onChoose, className = "", style }: PreciosNeumorficoProps) {
const [yearly, setYearly] = useState(false);
return (
<section className={`gcpne-root ${className}`} style={style}>
<style>{CSS}</style>
<div className="gcpne-head">
<h2 className="gcpne-title">{title}</h2>
<div className="gcpne-seg" role="radiogroup" aria-label="Periodo de facturación">
<button type="button" role="radio" aria-checked={!yearly} onClick={() => setYearly(false)}>
Mensual
</button>
<button type="button" role="radio" aria-checked={yearly} onClick={() => setYearly(true)}>
Anual
</button>
</div>
</div>
<div className="gcpne-grid">
{plans.map((plan) => (
<article key={plan.name} className="gcpne-card" data-f={plan.featured ? "true" : undefined}>
<h3 className="gcpne-name">{plan.name}</h3>
<p className="gcpne-price" aria-live="polite">
<b>
{currency}
{yearly ? plan.yearly : plan.monthly}
</b>{" "}
<span>/mes</span>
</p>
<ul className="gcpne-list">
{plan.features.map((feature) => (
<li key={feature}>
<i className="gcpne-dot" aria-hidden="true" />
{feature}
</li>
))}
</ul>
<button className="gcpne-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-neumorficoEjemplo de uso de Precios neumorfismo
Después de copiar precios-neumorfico.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { PreciosNeumorfico } from "./precios-neumorfico";
export default function Ejemplo() {
return <PreciosNeumorfico title="Ejemplo" />;
}Propiedades de PreciosNeumorfico
PreciosNeumorfico 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? | PreciosNeumorficoPlan[] | 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 95 líneas (6 KB) y no necesita @keyframes. Esto es lo que hace Precios neumorfismo 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".