← Catalogue

Pricing

Grille de tarifs 3 colonnes avec bascule Monthly/Yearly (masque glissant) et roulement animé des prix.

#pricing#tarifs#toggle#vanilla

Aperçu

Code source

HTML
<div class="wtl-pricing">
<div class="wtl-pricing__bar">
<div class="wtl-pricing__toggle" role="tablist">
<span class="wtl-pricing__thumb" aria-hidden="true"></span>
<button class="wtl-pricing__tab is-active" data-period="monthly">
Monthly
</button>
<button class="wtl-pricing__tab" data-period="yearly">Yearly</button>
</div>
</div>
<div class="wtl-pricing__grid">
<!-- Starter -->
<article class="wtl-pricing__card">
<div class="wtl-pricing__head">
<div class="wtl-pricing__top">
<div class="wtl-pricing__price-row wtl-pricing__price-row--main">
<span class="wtl-pricing__price" data-monthly="$19" data-yearly="$15">
<span class="wtl-pricing__price-track">
<span class="wtl-pricing__price-val">$19</span>
</span>
</span>
<span class="wtl-pricing__per">/month</span>
</div>
<span class="wtl-pricing__badge">Starter Plan</span>
</div>
</div>
<hr class="wtl-pricing__divider" />
<p class="wtl-pricing__desc">
Made for teams launching from a single location.
</p>
<div class="wtl-pricing__includes">
<p class="wtl-pricing__includes-label">Includes</p>
<ul class="wtl-pricing__list">
<li>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
1 location, up to 2 users
</li>
<li>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
Basic inventory tracking
</li>
<li>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
Low stock alerts
</li>
<li>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
Email support
</li>
</ul>
</div>
<a class="wtl-pricing__cta" href="#">
<span class="wtl-pricing__cta-mask">
<span class="wtl-pricing__cta-text">Choose starter plan</span>
<span class="wtl-pricing__cta-text" aria-hidden="true">Choose starter plan</span>
</span>
</a>
</article>
<!-- Growth (popular) -->
<article class="wtl-pricing__card wtl-pricing__card--popular">
<div class="wtl-pricing__head">
<div class="wtl-pricing__top">
<div class="wtl-pricing__price-row wtl-pricing__price-row--main">
<span class="wtl-pricing__price" data-monthly="$129" data-yearly="$103">
<span class="wtl-pricing__price-track">
<span class="wtl-pricing__price-val">$129</span>
</span>
</span>
<span class="wtl-pricing__per">/month</span>
</div>
<div class="wtl-pricing__price-row" style="gap: 8px">
<span class="wtl-pricing__badge">Growth Plan</span>
</div>
</div>
</div>
<hr class="wtl-pricing__divider" />
<p class="wtl-pricing__desc">
Built for multi-location teams with real-time visibility everywhere.
</p>
<div class="wtl-pricing__includes">
<p class="wtl-pricing__includes-label">Includes</p>
<ul class="wtl-pricing__list">
<li>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
Up to 10 locations
</li>
<li>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
Transfers and approvals
</li>
<li>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
Advanced dashboards
</li>
<li>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
Role-based permissions
</li>
</ul>
</div>
<a class="wtl-pricing__cta" href="#">
<span class="wtl-pricing__cta-mask">
<span class="wtl-pricing__cta-text">Choose growth plan</span>
<span class="wtl-pricing__cta-text" aria-hidden="true">Choose growth plan</span>
</span>
</a>
</article>
<!-- Custom (dark) -->
<article class="wtl-pricing__card wtl-pricing__card--dark">
<div class="wtl-pricing__head">
<div class="wtl-pricing__top">
<span class="wtl-pricing__plan-title">Custom Plan</span>
</div>
</div>
<hr class="wtl-pricing__divider" />
<p class="wtl-pricing__desc">
Designed for large operations needing consistency and control.
</p>
<div class="wtl-pricing__includes">
<p class="wtl-pricing__includes-label">Includes</p>
<ul class="wtl-pricing__list">
<li>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
Unlimited locations
</li>
<li>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
Custom integrations
</li>
<li>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
Dedicated onboarding
</li>
<li>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
SLA and priority support
</li>
</ul>
</div>
<a class="wtl-pricing__cta wtl-pricing__cta--grad" href="#">
<span class="wtl-pricing__cta-mask">
<span class="wtl-pricing__cta-text">
Contact our sales
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H4a1 1 0 0 1-1-1v-6a9 9 0 0 1 18 0v6a1 1 0 0 1-1 1h-2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3" /><path d="M21 16v2a4 4 0 0 1-4 4h-5" /></svg>
</span>
<span class="wtl-pricing__cta-text" aria-hidden="true">
Contact our sales
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H4a1 1 0 0 1-1-1v-6a9 9 0 0 1 18 0v6a1 1 0 0 1-1 1h-2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3" /><path d="M21 16v2a4 4 0 0 1-4 4h-5" /></svg>
</span>
</span>
</a>
</article>
</div>
</div>
CSS
/* Pricing - 3 cartes + toggle Monthly/Yearly. Vanilla, police Geist. */
.wtl-pricing {
--p-ink: #1d191a; /* carte noire + texte foncé */
--p-muted: #6f6f78; /* gris neutre */
--p-cream: #ffffff; /* cartes claires */
--p-border: rgba(24, 24, 27, 0.1); /* bordure neutre */
--p-accent: #0061e0; /* accent bleu */
width: 100%;
max-width: 1200px;
margin: 0 auto;
font-family: "Geist", system-ui, sans-serif;
}
/* Toggle Monthly / Yearly */
.wtl-pricing__bar {
display: flex;
justify-content: center;
}
.wtl-pricing__toggle {
position: relative;
display: inline-flex;
gap: 5px;
margin: 0 auto 28px;
padding: 5px;
background: #ffffff;
border: 0.8px solid var(--p-border);
border-radius: 6px;
width: fit-content;
}
/* Pastille noire qui glisse derriere l'onglet actif (masque) */
.wtl-pricing__thumb {
position: absolute;
top: 0;
left: 0;
z-index: 0;
background: var(--p-ink);
border-radius: 5px;
transition:
transform 0.4s cubic-bezier(0.7, 0, 0.3, 1),
width 0.4s cubic-bezier(0.7, 0, 0.3, 1);
}
.wtl-pricing__tab {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border: none;
border-radius: 5px;
background: transparent;
color: var(--p-muted);
font-family: inherit;
font-size: 16px;
cursor: pointer;
transition: color 0.3s ease;
}
.wtl-pricing__tab.is-active {
color: #ffffff;
}
/* Grille des cartes */
.wtl-pricing__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
align-items: stretch;
}
/* Carte */
.wtl-pricing__card {
display: flex;
flex-direction: column;
gap: 28px;
padding: 34px;
border-radius: 12px;
border: 0.8px solid var(--p-border);
background: var(--p-cream);
color: var(--p-ink);
}
.wtl-pricing__card--popular {
background: var(--p-accent);
color: #ffffff;
border: none;
}
.wtl-pricing__card--dark {
background: var(--p-ink);
color: #ffffff;
}
/* En-tete : prix + badge */
.wtl-pricing__head {
display: flex;
flex-direction: column;
gap: 16px;
}
.wtl-pricing__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 26px;
}
.wtl-pricing__price-row {
display: flex;
align-items: flex-end;
gap: 3px;
}
/* Ligne prix + /month : hauteur figée pour que /month ne bouge pas */
.wtl-pricing__price-row--main {
height: 44px;
align-items: flex-end;
}
.wtl-pricing__price-row--main .wtl-pricing__per {
line-height: 1;
padding-bottom: 5px;
}
.wtl-pricing__price {
display: inline-block;
overflow: hidden;
height: 44px;
vertical-align: bottom;
}
.wtl-pricing__price-track {
display: flex;
flex-direction: column;
will-change: transform;
}
.wtl-pricing__price-val {
height: 44px;
line-height: 44px;
font-size: 40px;
letter-spacing: -0.8px;
font-variant-numeric: tabular-nums;
}
.wtl-pricing__per {
color: var(--p-muted);
font-size: 16px;
padding-bottom: 3px;
}
.wtl-pricing__card--popular .wtl-pricing__per,
.wtl-pricing__card--dark .wtl-pricing__per {
color: rgba(255, 255, 255, 0.75);
}
.wtl-pricing__plan-title {
font-size: 40px;
line-height: 1;
letter-spacing: -0.8px;
}
/* Badge plan */
.wtl-pricing__badge {
padding: 4px 10px;
border-radius: 5px;
background: #f4f4f5;
border: 0.8px solid var(--p-border);
color: var(--p-muted);
font-size: 12px;
line-height: 1.4;
white-space: nowrap;
}
/* Divider pointille */
.wtl-pricing__divider {
height: 0;
margin: 0;
border: none;
border-top: 0.8px dashed rgba(29, 25, 26, 0.2);
}
.wtl-pricing__card--popular .wtl-pricing__divider,
.wtl-pricing__card--dark .wtl-pricing__divider {
border-top-color: rgba(255, 255, 255, 0.4);
}
.wtl-pricing__desc {
margin: 0;
color: var(--p-muted);
font-size: 16px;
line-height: 1.4;
}
.wtl-pricing__card--popular .wtl-pricing__desc {
color: #ffffff;
}
.wtl-pricing__card--dark .wtl-pricing__desc {
color: #cbc5c3;
}
/* Liste includes */
.wtl-pricing__includes {
display: flex;
flex-direction: column;
gap: 12px;
}
.wtl-pricing__includes-label {
margin: 0;
color: var(--p-muted);
font-size: 12px;
}
.wtl-pricing__card--popular .wtl-pricing__includes-label {
color: #ffffff;
}
.wtl-pricing__card--dark .wtl-pricing__includes-label {
color: #cbc5c3;
}
.wtl-pricing__list {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 10px;
}
.wtl-pricing__list li {
display: flex;
align-items: flex-start;
gap: 8px;
color: var(--p-muted);
font-size: 16px;
line-height: 1.2;
}
.wtl-pricing__card--popular .wtl-pricing__list li {
color: #ffffff;
}
.wtl-pricing__card--dark .wtl-pricing__list li {
color: #cbc5c3;
}
.wtl-pricing__list svg {
flex: none;
margin-top: 2px;
}
/* CTA - effet masque reveal au survol (texte monte, clone arrive du bas) */
.wtl-pricing__cta {
margin-top: auto;
display: inline-flex;
align-items: center;
justify-content: center;
height: 48px;
border-radius: 8px;
background: var(--p-ink);
color: #ffffff;
text-decoration: none;
font-size: 16px;
overflow: hidden;
}
.wtl-pricing__cta--grad {
background: var(--p-accent);
}
.wtl-pricing__cta-mask {
position: relative;
display: block;
overflow: hidden;
height: 24px;
}
.wtl-pricing__cta-text {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 24px;
line-height: 24px;
transition: transform 0.45s cubic-bezier(0.7, 0, 0.3, 1);
}
.wtl-pricing__cta-text + .wtl-pricing__cta-text {
position: absolute;
inset: 0;
transform: translateY(100%);
}
.wtl-pricing__cta:hover .wtl-pricing__cta-text:first-child {
transform: translateY(-100%);
}
.wtl-pricing__cta:hover .wtl-pricing__cta-text + .wtl-pricing__cta-text {
transform: translateY(0);
}
/* Responsive */
@media (max-width: 920px) {
.wtl-pricing__grid {
grid-template-columns: 1fr;
}
}
JS
(function () {
// Roulement masque : l'ancien prix monte, le nouveau arrive du bas.
function roll(price, period) {
var next = price.dataset[period];
if (!next) return;
var track = price.querySelector(".wtl-pricing__price-track");
var vals = track.querySelectorAll(".wtl-pricing__price-val");
var current = vals[vals.length - 1];
if (current.textContent === next) return;
var span = document.createElement("span");
span.className = "wtl-pricing__price-val";
span.textContent = next;
track.appendChild(span);
var h = current.offsetHeight;
track.style.transition = "none";
track.style.transform = "translateY(0)";
requestAnimationFrame(function () {
track.style.transition = "transform 0.5s cubic-bezier(0.7, 0, 0.3, 1)";
track.style.transform = "translateY(-" + h + "px)";
});
track.addEventListener("transitionend", function handler() {
track.removeEventListener("transitionend", handler);
track.style.transition = "none";
track.style.transform = "translateY(0)";
while (track.children.length > 1) {
track.removeChild(track.firstChild);
}
});
}
document.querySelectorAll(".wtl-pricing").forEach(function (root) {
var toggle = root.querySelector(".wtl-pricing__toggle");
var thumb = toggle && toggle.querySelector(".wtl-pricing__thumb");
var tabs = root.querySelectorAll(".wtl-pricing__tab");
var prices = root.querySelectorAll(".wtl-pricing__price");
function placeThumb(tab, animate) {
if (!thumb) return;
if (!animate) thumb.style.transition = "none";
thumb.style.width = tab.offsetWidth + "px";
thumb.style.height = tab.offsetHeight + "px";
thumb.style.transform =
"translate(" + tab.offsetLeft + "px," + tab.offsetTop + "px)";
if (!animate) {
void thumb.offsetWidth; // reflow
thumb.style.transition = "";
}
}
var active = root.querySelector(".wtl-pricing__tab.is-active") || tabs[0];
if (active) placeThumb(active, false);
window.addEventListener("load", function () {
if (active) placeThumb(active, false);
});
window.addEventListener("resize", function () {
var cur = root.querySelector(".wtl-pricing__tab.is-active") || tabs[0];
if (cur) placeThumb(cur, false);
});
tabs.forEach(function (tab) {
tab.addEventListener("click", function () {
tabs.forEach(function (t) {
t.classList.toggle("is-active", t === tab);
});
placeThumb(tab, true);
var period = tab.dataset.period;
prices.forEach(function (p) {
roll(p, period);
});
});
});
});
})();