Pricing Table Responsive: Design Moderno Solo CSS
Perché una pricing table CSS responsive nel 2026
La pricing table è uno degli elementi più critici di qualsiasi landing page o SaaS. È il momento in cui l’utente decide se comprare o abbandonare. Eppure, in troppi progetti la si tratta come un dettaglio: componenti Bootstrap copiati, tabelle HTML rigide, oppure librerie da 200 KB per mostrare tre colonne con dei prezzi.
Nel 2026 il CSS ha tutto ciò che serve per costruire una pricing table responsive elegante, accessibile e performante senza toccare un singolo framework. CSS Grid, Custom Properties, :has(), clamp() e container queries rendono possibile ciò che fino a qualche anno fa richiedeva JavaScript o librerie esterne.
In questo articolo costruiamo da zero una pricing table completa: tre piani tariffari, layout a griglia che collassa in colonna singola su mobile, highlight visivo del piano consigliato, toggle mensile/annuale via CSS puro e badge. Nessuna dipendenza, nessun framework, solo HTML semantico e CSS moderno. Se stai costruendo la landing page del tuo Micro SaaS, questa guida è esattamente ciò di cui hai bisogno.
Struttura HTML semantica della pricing table
Prima del CSS, serve una base HTML robusta. L’elemento centrale è un section che contiene un wrapper .pricing-grid con tre article: uno per piano. Ogni article include header con nome piano e prezzo, lista feature e un CTA. Il piano evidenziato riceve la classe .featured.
<section class="pricing-section">
<div class="pricing-toggle">
<input type="checkbox" id="billing-toggle" class="toggle-input">
<label for="billing-toggle" class="toggle-label">
<span class="label-monthly">Mensile</span>
<span class="toggle-switch"></span>
<span class="label-yearly">Annuale <em>-20%</em></span>
</label>
</div>
<div class="pricing-grid">
<!-- Piano Base -->
<article class="pricing-card">
<header class="card-header">
<h2>Base</h2>
<p class="price">
<span class="price-monthly"><strong>9</strong>€/mese</span>
<span class="price-yearly"><strong>86</strong>€/anno</span>
</p>
<p class="price-note">Per freelance e indie dev</p>
</header>
<ul class="feature-list">
<li>✓ 3 progetti attivi</li>
<li>✓ 5 GB storage</li>
<li>✓ API access base</li>
<li class="disabled">✗ Team collaboration</li>
<li class="disabled">✗ Analytics avanzate</li>
</ul>
<footer class="card-footer">
<a href="#" class="cta-btn">Inizia gratis</a>
</footer>
</article>
<!-- Piano Pro (evidenziato) -->
<article class="pricing-card featured" aria-label="Piano consigliato">
<span class="badge">Consigliato</span>
<header class="card-header">
<h2>Pro</h2>
<p class="price">
<span class="price-monthly"><strong>29</strong>€/mese</span>
<span class="price-yearly"><strong>278</strong>€/anno</span>
</p>
<p class="price-note">Per team fino a 5 persone</p>
</header>
<ul class="feature-list">
<li>✓ Progetti illimitati</li>
<li>✓ 50 GB storage</li>
<li>✓ API access completo</li>
<li>✓ Team collaboration</li>
<li class="disabled">✗ Analytics avanzate</li>
</ul>
<footer class="card-footer">
<a href="#" class="cta-btn">Inizia ora</a>
</footer>
</article>
<!-- Piano Business -->
<article class="pricing-card">
<header class="card-header">
<h2>Business</h2>
<p class="price">
<span class="price-monthly"><strong>79</strong>€/mese</span>
<span class="price-yearly"><strong>758</strong>€/anno</span>
</p>
<p class="price-note">Per aziende e team grandi</p>
</header>
<ul class="feature-list">
<li>✓ Progetti illimitati</li>
<li>✓ 500 GB storage</li>
<li>✓ API access completo</li>
<li>✓ Team collaboration</li>
<li>✓ Analytics avanzate</li>
</ul>
<footer class="card-footer">
<a href="#" class="cta-btn">Contattaci</a>
</footer>
</article>
</div>
</section>L’input checkbox con id billing-toggle è la chiave del toggle mensile/annuale in CSS puro. Il suo stato :checked verrà sfruttato come interruttore per mostrare o nascondere i prezzi. Nessun JavaScript richiesto.
CSS Grid per il layout a tre colonne
Il layout della pricing grid è il punto dove CSS Grid brilla davvero. Usiamo grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)) per ottenere un layout completamente fluido: tre colonne su desktop, due su tablet, una su mobile, senza una singola media query dedicata al layout. Come spiegato nella guida pratica Flexbox vs Grid, Grid è la scelta corretta quando hai bisogno di allineamento bidimensionale.
/* === Custom Properties === */
:root {
--clr-bg: #0f172a;
--clr-card: #1e293b;
--clr-card-featured: #0e7490;
--clr-accent: #06b6d4;
--clr-accent-lo: #67e8f9;
--clr-text: #e2e8f0;
--clr-text-muted: #94a3b8;
--clr-disabled: #475569;
--clr-cta: #06b6d4;
--clr-cta-featured: #ffffff;
--radius-card: 1.25rem;
--shadow-card: 0 4px 24px rgba(0, 0, 0, 0.35);
--shadow-featured: 0 0 40px rgba(6, 182, 212, 0.25);
}
/* === Layout === */
.pricing-section {
background: var(--clr-bg);
padding: clamp(2rem, 5vw, 4rem) clamp(1rem, 4vw, 2rem);
}
.pricing-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
max-width: 1100px;
margin: 2rem auto 0;
align-items: start; /* default: card alte uguali */
}
/* === Card base === */
.pricing-card {
background: var(--clr-card);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: var(--radius-card);
padding: 2rem 1.75rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
box-shadow: var(--shadow-card);
position: relative;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.pricing-card:hover {
transform: translateY(-4px);
}
/* === Featured card === */
.pricing-card.featured {
background: linear-gradient(
160deg,
#0e4c5c 0%,
#0e3850 50%,
#0f172a 100%
);
border-color: var(--clr-accent);
box-shadow: var(--shadow-featured);
/* Leggermente più grande su desktop */
scale: 1.03;
}
@media (max-width: 768px) {
.pricing-card.featured {
scale: 1; /* reset su mobile */
order: -1; /* portala in cima su mobile */
}
}
/* === Badge "Consigliato" === */
.badge {
position: absolute;
top: -14px;
left: 50%;
translate: -50% 0;
background: var(--clr-accent);
color: #001a22;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 0.3rem 1rem;
border-radius: 100vw;
}
/* === Typography dei prezzi === */
.card-header h2 {
font-size: 1.25rem;
font-weight: 700;
color: var(--clr-text);
margin: 0 0 0.5rem;
}
.price strong {
font-size: clamp(2.5rem, 5vw, 3.5rem);
font-weight: 800;
color: var(--clr-accent-lo);
line-height: 1;
}
.price-note {
font-size: 0.85rem;
color: var(--clr-text-muted);
margin-top: 0.25rem;
}
/* === Feature list === */
.feature-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.6rem;
flex: 1; /* occupa lo spazio disponibile */
}
.feature-list li {
font-size: 0.92rem;
color: var(--clr-text);
}
.feature-list li.disabled {
color: var(--clr-disabled);
text-decoration: line-through;
}
/* === CTA Button === */
.cta-btn {
display: block;
text-align: center;
padding: 0.85rem 1.5rem;
border-radius: 0.75rem;
font-weight: 700;
font-size: 0.95rem;
text-decoration: none;
border: 2px solid var(--clr-cta);
color: var(--clr-cta);
transition: background 0.2s, color 0.2s;
}
.cta-btn:hover {
background: var(--clr-cta);
color: #001a22;
}
.featured .cta-btn {
background: var(--clr-cta);
color: #001a22;
}
.featured .cta-btn:hover {
background: var(--clr-accent-lo);
border-color: var(--clr-accent-lo);
}Nota l’uso di scale: 1.03 sulla card featured invece di un transform: scale() esplicito: la proprietà scale standalone (disponibile da CSS Transforms Level 2) permette di combinare la scala con altri transform senza sovrascriverli. Per il reset su mobile, un’unica media query gestisce sia la scala che il riordinamento via order: -1.
Toggle mensile/annuale in CSS puro
Il toggle tra prezzi mensili e annuali è la funzionalità più richiesta in una pricing table moderna. La soluzione classica usa JavaScript per aggiungere/rimuovere classi. La soluzione CSS-only sfrutta il selettore fratello ~ e lo stato :checked della checkbox nascosta.
/* === Toggle Billing === */
.toggle-input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.toggle-label {
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
justify-content: center;
font-size: 0.95rem;
color: var(--clr-text-muted);
user-select: none;
}
.toggle-switch {
width: 52px;
height: 28px;
background: var(--clr-card);
border: 2px solid var(--clr-accent);
border-radius: 100vw;
position: relative;
transition: background 0.2s;
flex-shrink: 0;
}
.toggle-switch::after {
content: "";
position: absolute;
width: 18px;
height: 18px;
background: var(--clr-accent);
border-radius: 50%;
top: 3px;
left: 3px;
transition: translate 0.2s;
}
/* Stato: toggle attivato (annuale) */
.toggle-input:checked ~ .pricing-section .toggle-switch,
.toggle-input:checked + .toggle-label .toggle-switch {
background: var(--clr-accent);
}
.toggle-input:checked + .toggle-label .toggle-switch::after {
translate: 24px 0;
}
/* Prezzi: mostra mensile di default, annuale quando checked */
.price-yearly {
display: none;
}
.toggle-input:checked ~ .pricing-grid .price-monthly {
display: none;
}
.toggle-input:checked ~ .pricing-grid .price-yearly {
display: block;
}
/* Label attiva */
.label-monthly,
.label-yearly {
transition: color 0.2s, font-weight 0.2s;
}
.toggle-input:not(:checked) + .toggle-label .label-monthly {
color: var(--clr-accent-lo);
font-weight: 600;
}
.toggle-input:checked + .toggle-label .label-yearly {
color: var(--clr-accent-lo);
font-weight: 600;
}
.label-yearly em {
font-style: normal;
background: rgba(6, 182, 212, 0.15);
color: var(--clr-accent);
font-size: 0.75rem;
padding: 0.1rem 0.4rem;
border-radius: 4px;
margin-left: 0.25rem;
}La tecnica si basa sul fatto che la checkbox #billing-toggle precede nel DOM tutti gli elementi che devono reagire al suo stato. Il selettore :checked ~ .pricing-grid seleziona il grid fratello successivo quando la checkbox è attiva. Questo è uno dei pattern CSS più eleganti per interattività leggera — nessun JavaScript, nessun listener, zero KB aggiuntivi.
Se stai integrando questa pricing table con Stripe per abbonamenti e one-time payment, il valore del toggle determinerà quale price ID di Stripe passare al checkout. In quel caso avrai bisogno di un minimo di JavaScript solo per leggere lo stato della checkbox e costruire l’URL di redirect.
Accessibilità e responsive avanzato con container queries
Una pricing table moderna deve funzionare in qualsiasi contesto: pagina intera, sidebar, widget embedded. Le container queries risolvono questo problema permettendo alle card di adattarsi non alla viewport ma al loro contenitore. Come approfondito nell’articolo sulle interfacce fluide con CSS Container Queries, questo approccio è superiore alle media queries classiche per i componenti riutilizzabili.
/* === Container Queries per adattamento contestuale === */
.pricing-section {
container-type: inline-size;
container-name: pricing;
}
/* Quando il container è stretto (es. sidebar) */
@container pricing (max-width: 640px) {
.pricing-grid {
grid-template-columns: 1fr;
}
.pricing-card.featured {
scale: 1;
order: -1;
}
.price strong {
font-size: 2.5rem;
}
}
/* Layout intermedio: 2 colonne */
@container pricing (min-width: 641px) and (max-width: 900px) {
.pricing-grid {
grid-template-columns: repeat(2, 1fr);
}
/* La terza card occupa tutta la larghezza */
.pricing-card:last-child {
grid-column: 1 / -1;
max-width: 400px;
margin: 0 auto;
}
}
/* === Accessibilità === */
/* Focus visible migliorato */
.cta-btn:focus-visible {
outline: 3px solid var(--clr-accent);
outline-offset: 3px;
}
.toggle-input:focus-visible + .toggle-label .toggle-switch {
outline: 3px solid var(--clr-accent);
outline-offset: 3px;
}
/* Rispetto prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
.pricing-card,
.toggle-switch,
.toggle-switch::after,
.cta-btn {
transition: none;
}
.pricing-card:hover {
transform: none;
}
}
/* === Print === */
@media print {
.pricing-section {
background: white;
color: black;
}
.pricing-toggle {
display: none;
}
.price-yearly {
display: none !important;
}
}L’accessibilità non è optional. Il :focus-visible garantisce un indicatore di focus chiaro per la navigazione da tastiera senza interferire con il click del mouse. L’aria-label="Piano consigliato" sulla card featured fornisce contesto agli screen reader senza testo visibile ridondante. Sempre includere prefers-reduced-motion per rispettare le preferenze degli utenti con sensibilità al movimento.
Varianti stilistiche: glassmorphism e dark/light mode
La base che abbiamo costruito è facilmente personalizzabile. Due varianti molto richieste nel 2026: glassmorphism per interfacce dark premium e supporto automatico light/dark mode via prefers-color-scheme e Custom Properties. Il CSS diventa il motore del tema, non il JavaScript — esattamente come discusso nella guida sulle variabili CSS per temi adattivi.
/* === Variante Glassmorphism === */
.pricing-card.glass {
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(16px) saturate(160%);
-webkit-backdrop-filter: blur(16px) saturate(160%);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.pricing-card.glass.featured {
background: rgba(6, 182, 212, 0.08);
border-color: rgba(6, 182, 212, 0.4);
}
/* === Light / Dark mode automatico === */
@media (prefers-color-scheme: light) {
:root {
--clr-bg: #f1f5f9;
--clr-card: #ffffff;
--clr-text: #0f172a;
--clr-text-muted: #64748b;
--clr-disabled: #94a3b8;
--clr-accent: #0891b2;
--clr-accent-lo: #0e7490;
--shadow-card: 0 4px 24px rgba(0, 0, 0, 0.08);
--shadow-featured: 0 0 40px rgba(8, 145, 178, 0.15);
}
.pricing-card.featured {
background: linear-gradient(
160deg,
#e0f2fe 0%,
#bae6fd 50%,
#f0f9ff 100%
);
}
.price strong {
color: var(--clr-accent);
}
}
/* === Skeleton loading (opzionale) === */
.pricing-card.loading {
pointer-events: none;
}
.pricing-card.loading .card-header,
.pricing-card.loading .feature-list,
.pricing-card.loading .cta-btn {
background: linear-gradient(
90deg,
rgba(255,255,255,0.05) 25%,
rgba(255,255,255,0.1) 50%,
rgba(255,255,255,0.05) 75%
);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
border-radius: 6px;
color: transparent;
}
@keyframes skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
.pricing-card.loading .card-header,
.pricing-card.loading .feature-list,
.pricing-card.loading .cta-btn {
animation: none;
}
}Il supporto automatico light/dark mode tramite prefers-color-scheme è oggi uno standard atteso dagli utenti. Sovrascrivendo solo le Custom Properties dentro la media query, non duplichi nessuna regola strutturale: il layout, i border-radius, i gap restano identici. Solo i colori cambiano. Questo è il potere delle CSS Custom Properties come sistema di design token.
Integrazione con landing page e Stripe: il flusso completo
La pricing table CSS è solo la parte visiva. Per un Micro SaaS lanciato da developer solitario, il flusso completo comprende: visualizzazione prezzi → selezione piano → redirect a Stripe Checkout. Il minimo JavaScript necessario è solo per leggere il piano selezionato e costruire il link.
// Minimo JS per collegare pricing table a Stripe
const STRIPE_PRICES = {
base: {
monthly: "price_base_monthly_xxx",
yearly: "price_base_yearly_xxx",
},
pro: {
monthly: "price_pro_monthly_xxx",
yearly: "price_pro_yearly_xxx",
},
business: {
monthly: "price_business_monthly_xxx",
yearly: "price_business_yearly_xxx",
},
};
const toggle = document.getElementById("billing-toggle");
const ctaButtons = document.querySelectorAll(".cta-btn[data-plan]");
ctaButtons.forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
const plan = btn.dataset.plan;
const billing = toggle.checked ? "yearly" : "monthly";
const priceId = STRIPE_PRICES[plan]?.[billing];
if (!priceId) return;
// Redirect a Stripe Checkout (lato server o Stripe Billing Portal)
window.location.href = `/checkout?price_id=${priceId}`;
});
});
// Aggiungi data-plan agli anchor nel tuo HTML:
// <a href="#" class="cta-btn" data-plan="pro">Inizia ora</a>Per la gestione degli abbonamenti lato server, la guida completa su Stripe per Developer copre tutto: webhook, customer portal, trial period e gestione dei piani. La pricing table CSS che hai costruito è già pronta a ricevere questi dati dinamici.
FAQ e Domande Frequenti
Posso usare questa pricing table con React o Vue?
Assolutamente sì. Il CSS rimane identico. In React, importi il CSS come modulo e converti l’HTML in JSX (attributi come class diventano className, for diventa htmlFor). Il toggle in CSS puro continua a funzionare, ma in React potresti preferire gestire lo stato con useState per sincronizzare il piano selezionato con il resto dell’app. In Vue, la sintassi è ancora più diretta con v-model sulla checkbox.
Il toggle CSS-only funziona su tutti i browser?
Il selettore :checked ~ elemento ha supporto universale su tutti i browser moderni da oltre 10 anni. L’unica attenzione riguarda la struttura DOM: la checkbox deve essere un fratello precedente (not inside) degli elementi che deve controllare. Le container queries sono supportate da tutti i browser moderni dal 2023 (Chrome 105+, Safari 16+, Firefox 110+). Per browser molto vecchi (IE11), una semplice media query tradizionale come fallback è sufficiente.
Come gestisco una pricing table con più di 3 piani?
Con grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)) il grid si adatta automaticamente a qualsiasi numero di piani. Per 4 piani su desktop otterrai 4 colonne; su tablet, 2. Il piano featured manterrà il suo highlight visivo. L’unica cosa da considerare con molti piani è la larghezza minima della card: scendere sotto 240px compromette la leggibilità dei prezzi. Per più di 5 piani, valuta una struttura a tabella comparativa orizzontale.
Come aggiungo un periodo di prova gratuita nella card?
Aggiungi un elemento <p class="trial-note"> dopo il CTA nel footer della card. Stilizzalo con un colore di accent leggero e font-size ridotto. Lato Stripe, il trial period si gestisce con il parametro trial_period_days nella creazione della subscription — il testo nella card dovrebbe riflettere esattamente ciò che è configurato in Stripe. Evita di promettere “gratis per sempre” se la logica backend non lo supporta: la coerenza tra UI e business logic è fondamentale per la fiducia dell’utente.
Conclusione
Una pricing table CSS responsive moderna non richiede framework, librerie o JavaScript complesso. CSS Grid gestisce il layout adattivo, Custom Properties rendono il tema mantenibile, il selettore :checked alimenta il toggle prezzi e le container queries garantiscono il funzionamento in qualsiasi contesto.
Il codice che hai letto in questo articolo è production-ready: accessibile, responsive, con supporto light/dark mode e animazioni rispettose delle preferenze utente. Puoi copiarlo, adattarlo al tuo brand modificando le Custom Properties e collegarlo a Stripe in poche righe di JavaScript.
La pricing table è la vetrina del tuo SaaS. Investire in un design curato direttamente nel CSS — senza overhead di dipendenze — è una delle scelte tecniche che più impattano il tasso di conversione. Costruiscila bene una volta, usala ovunque.
Suggerimenti e Risorse
🔨 Strumento: Usa il CSS Nesting (supporto nativo 2024+) per raggruppare le varianti della card nel tuo CSS —
.pricing-card { & .featured { ... } }rende il codice molto più leggibile e manutenibile.
💡 Pro tip: Metti la pricing table in una pagina standalone dedicata e testala con Hotjar o Microsoft Clarity per vedere dove gli utenti si bloccano. Spesso il problema non è il design ma la mancanza di social proof (testimonial, loghi clienti) nelle vicinanze.
🎯 Strategia: Il piano centrale evidenziato non è solo design: è psicologia dell’anchoring. Posiziona sempre il tuo piano a maggior margine al centro con il badge “Consigliato”. Studi di conversione mostrano aumenti del 15-30% nelle entrate medie per utente con questo layout rispetto a card equiparate.

