Pricing Toggle Cards
Три тарифа с доступным переключателем месяц/год, бейджем экономии и выделенным планом.
pricingtogglemonthly-yearlyplans
Живое превью
Simple pricing that scales with you
Start free. Upgrade when your team grows. Cancel anytime.
MonthlyYearly Save 20%
Starter
For side projects and prototypes.
$0/ month, billed yearly
Get started- All components
- Community support
- 1 project
- Most popular
Pro
For professionals shipping client work.
$24/ month, billed yearly
Start 14-day trial- Everything in Starter
- Premium sections
- Unlimited projects
- Priority support
Team
For agencies and product teams.
$79/ month, billed yearly
Contact sales- Everything in Pro
- 10 seats included
- Figma kit
- Private Discord
Использование
Скопируйте и вставьте этот код в ваш React-компонент:
tsx
import { PricingToggleCards } from 'react-component-kit-ui/sections';
<PricingToggleCards plans={[
{ name: 'Free', monthly: 0, yearly: 0, description: 'Hobby', features: ['1 project'], cta: 'Start', href: '#' },
{ name: 'Pro', monthly: 24, yearly: 19, description: 'Pros', features: ['Unlimited'], cta: 'Try Pro', href: '#', featured: true },
]} />Ручная установка
Если вы не используете пакет npm, создайте файл и вставьте следующий код: src/components/sections/PricingToggleCards.tsx + PricingToggleCards.css:
PricingToggleCards.tsx
'use client';
import { useEffect, useRef } from 'react';
import './PricingToggleCards.css';
interface Plan { name: string; monthly: number; yearly: number; description: string; features: string[]; cta: string; href: string; featured?: boolean }
/**
* PricingToggleCards — Three pricing tiers with an accessible monthly/yearly
* switch (role="switch"), savings badge and a highlighted recommended plan.
*
* @param {string} title - Section heading.
* @param {string} subtitle - Section description.
* @param {Array<{name: string, monthly: number, yearly: number, description: string, features: string[], cta: string, href: string, featured?: boolean}>} plans - Tiers.
* @param {string} currency - Currency symbol.
* @param {string} savingsLabel - Badge next to the yearly option.
* @param {string} variant - Card style (glass|solid|outline).
* @param {string} size - Vertical rhythm (sm|md|lg).
* @param {string} className - Extra CSS class.
*/
export interface PricingToggleCardsProps {
title?: string;
subtitle?: string;
plans?: Plan[];
currency?: string;
savingsLabel?: string;
variant?: 'glass' | 'solid' | 'outline';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
export function PricingToggleCards({
title = 'Simple pricing that scales with you',
subtitle = 'Start free. Upgrade when your team grows. Cancel anytime.',
plans = [
{ name: 'Starter', monthly: 0, yearly: 0, description: 'For side projects and prototypes.', features: ['All components', 'Community support', '1 project'], cta: 'Get started', href: '#' },
{ name: 'Pro', monthly: 24, yearly: 19, description: 'For professionals shipping client work.', features: ['Everything in Starter', 'Premium sections', 'Unlimited projects', 'Priority support'], cta: 'Start 14-day trial', href: '#', featured: true },
{ name: 'Team', monthly: 79, yearly: 63, description: 'For agencies and product teams.', features: ['Everything in Pro', '10 seats included', 'Figma kit', 'Private Discord'], cta: 'Contact sales', href: '#' },
],
currency = '$',
savingsLabel = 'Save 20%',
variant = 'glass',
size = 'md',
className = '',
}: PricingToggleCardsProps) {
const id = `ptc-${Math.random().toString(36).slice(2, 8)}`;
// Ported from the Astro <script>: runs once after mount (DOM logic kept as in the Astro kit).
const mounted = useRef(false);
useEffect(() => {
if (mounted.current) return;
mounted.current = true;
document.querySelectorAll<HTMLElement>('[data-ptc]').forEach((root) => {
const sw = root.querySelector<HTMLButtonElement>('.ptc__switch');
const live = root.querySelector('.ptc__live');
sw?.addEventListener('click', () => {
const yearly = sw.getAttribute('aria-checked') !== 'true';
sw.setAttribute('aria-checked', String(yearly));
root.dataset.period = yearly ? 'yearly' : 'monthly';
root.querySelectorAll<HTMLElement>('.ptc__amount').forEach((el) => {
el.textContent = (yearly ? el.dataset.yearly : el.dataset.monthly) || '';
});
if (live) live.textContent = yearly ? 'Showing yearly prices' : 'Showing monthly prices';
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<section className={`ptc ptc--${variant} ptc--${size} ${className}`} aria-labelledby={id} data-ptc data-period="monthly" data-rk-101321="">
<div className="ptc__inner" data-rk-101321="">
<header className="ptc__head" data-rk-101321="">
<h2 id={id} className="ptc__title" data-rk-101321="">{title}</h2>
<p className="ptc__subtitle" data-rk-101321="">{subtitle}</p>
<div className="ptc__toggle" data-rk-101321="">
<span className="ptc__period" id={`${id}-m`} data-rk-101321="">Monthly</span>
<button type="button" className="ptc__switch" role="switch" aria-checked="false" aria-labelledby={`${id}-y`} data-rk-101321="">
<span className="ptc__knob" aria-hidden="true" data-rk-101321=""></span>
</button>
<span className="ptc__period" id={`${id}-y`} data-rk-101321="">Yearly <span className="ptc__save" data-rk-101321="">{savingsLabel}</span></span>
</div>
</header>
<ul className="ptc__grid" data-rk-101321="">
{plans.map((p, __i: number) => (
<li key={__i} className={`ptc__card ${p.featured ? 'ptc__card--featured' : ''}`} data-rk-101321="">
{p.featured && <span className="ptc__ribbon" data-rk-101321="">Most popular</span>}
<h3 className="ptc__name" data-rk-101321="">{p.name}</h3>
<p className="ptc__desc" data-rk-101321="">{p.description}</p>
<p className="ptc__price" data-rk-101321="">
<span className="ptc__amount" data-monthly={`${currency}${p.monthly}`} data-yearly={`${currency}${p.yearly}`} data-rk-101321="">{currency}{p.monthly}</span>
<span className="ptc__per" data-rk-101321="">/ month<span className="ptc__billed" data-yearly-only data-rk-101321="">, billed yearly</span></span>
</p>
<a className="ptc__cta" href={p.href} data-rk-101321="">{p.cta}</a>
<ul className="ptc__features" data-rk-101321="">
{p.features.map((f, __i: number) => (
<li key={__i} data-rk-101321=""><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" data-rk-101321=""><path d="M20 6 9 17l-5-5" data-rk-101321="" /></svg>{f}</li>
))}
</ul>
</li>
))}
</ul>
<p className="ptc__live" role="status" aria-live="polite" data-rk-101321=""></p>
</div>
</section>
</>
);
}
export default PricingToggleCards;PricingToggleCards.css
/* PricingToggleCards — ported from astro-component-kit (sections/PricingToggleCards.astro) */
.ptc:where([data-rk-101321]) {
--ptc-py: 5.5rem;
padding: var(--ptc-py) var(--sp-6, 1.5rem);
background: radial-gradient(ellipse 60% 40% at 50% 0%, var(--c-primary-glow, color-mix(in srgb, var(--c-primary, #b8860b) 10%, transparent)), transparent 70%), var(--c-bg, #050505);
}
.ptc--sm:where([data-rk-101321]) { --ptc-py: 3.5rem; }
.ptc--lg:where([data-rk-101321]) { --ptc-py: 7.5rem; }
.ptc__inner:where([data-rk-101321]) { max-width: 1120px; margin: 0 auto; }
.ptc__head:where([data-rk-101321]) { text-align: center; margin-bottom: 3rem; }
.ptc__title:where([data-rk-101321]) { margin: 0 0 0.75rem; font-size: clamp(1.9rem, 4vw, 2.75rem); font-weight: 800; letter-spacing: -0.03em; color: var(--c-text-1, #fff); }
.ptc__subtitle:where([data-rk-101321]) { margin: 0 0 2rem; font-size: 1.0625rem; color: var(--c-text-2, #a3a3a3); }
.ptc__toggle:where([data-rk-101321]) { display: inline-flex; align-items: center; gap: 0.85rem; font-size: 0.9375rem; color: var(--c-text-2, #a3a3a3); }
.ptc__switch:where([data-rk-101321]) {
position: relative;
width: 52px;
height: 30px;
border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.15);
border-radius: 999px;
background: rgb(var(--ink-rgb, 255 255 255) / 0.08);
cursor: pointer;
transition: background 0.25s ease;
}
.ptc__switch:where([data-rk-101321])::after { content: ''; position: absolute; inset: -8px; }
.ptc__switch[aria-checked='true']:where([data-rk-101321]) { background: var(--c-primary, #b8860b); border-color: transparent; }
.ptc__switch:focus-visible:where([data-rk-101321]) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 3px; }
.ptc__knob:where([data-rk-101321]) { position: absolute; top: 3px; left: 3px; width: 22px; height: 22px; border-radius: 50%; background: #fff; transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); }
.ptc__switch[aria-checked='true']:where([data-rk-101321]) .ptc__knob:where([data-rk-101321]) { transform: translateX(22px); }
[data-period='monthly']:where([data-rk-101321]) .ptc__period:first-child:where([data-rk-101321]),
[data-period='yearly']:where([data-rk-101321]) .ptc__period:last-child:where([data-rk-101321]) { color: var(--c-text-1, #fff); }
.ptc__save:where([data-rk-101321]) { margin-left: 0.35rem; padding: 0.15rem 0.5rem; border-radius: 999px; background: rgba(52, 195, 143, 0.15); color: #7ee2b8; font-size: 0.75rem; font-weight: 650; }
.ptc__grid:where([data-rk-101321]) { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.25rem; align-items: stretch; }
.ptc__card:where([data-rk-101321]) {
position: relative;
display: flex;
flex-direction: column;
padding: 2rem;
border-radius: var(--r-xl, 24px);
}
.ptc--glass:where([data-rk-101321]) .ptc__card:where([data-rk-101321]) { background: linear-gradient(160deg, rgb(var(--ink-rgb, 255 255 255) / 0.05), rgb(var(--ink-rgb, 255 255 255) / 0.015)); border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.07)); backdrop-filter: blur(12px); }
.ptc--solid:where([data-rk-101321]) .ptc__card:where([data-rk-101321]) { background: var(--c-bg-2, #111); border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.06); }
.ptc--outline:where([data-rk-101321]) .ptc__card:where([data-rk-101321]) { border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.12); }
.ptc__card--featured:where([data-rk-101321]) { border-color: color-mix(in srgb, var(--c-primary-light, #d4a017) 55%, transparent) !important; box-shadow: 0 30px 80px -30px color-mix(in srgb, var(--c-primary-light, #d4a017) 35%, transparent); }
.ptc__ribbon:where([data-rk-101321]) { position: absolute; top: -12px; left: 2rem; padding: 0.25rem 0.75rem; border-radius: 999px; background: var(--c-primary-light, #d4a017); color: var(--c-on-primary, #0a0a0a); font-size: 0.75rem; font-weight: 700; }
.ptc__name:where([data-rk-101321]) { margin: 0 0 0.35rem; font-size: 1.125rem; font-weight: 650; color: var(--c-text-1, #fff); }
.ptc__desc:where([data-rk-101321]) { margin: 0 0 1.5rem; font-size: 0.9rem; color: var(--c-text-2, #a3a3a3); }
.ptc__price:where([data-rk-101321]) { display: flex; align-items: baseline; gap: 0.4rem; margin: 0 0 1.5rem; }
.ptc__amount:where([data-rk-101321]) { font-size: 3rem; font-weight: 800; letter-spacing: -0.04em; color: var(--c-text-1, #fff); font-variant-numeric: tabular-nums; line-height: 1; }
.ptc__per:where([data-rk-101321]) { font-size: 0.875rem; color: var(--c-text-3, #8a8a8a); }
.ptc__billed:where([data-rk-101321]) { display: none; }
[data-period='yearly']:where([data-rk-101321]) .ptc__billed:where([data-rk-101321]) { display: inline; }
.ptc__cta:where([data-rk-101321]) {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 48px;
border-radius: var(--r-md, 12px);
border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.14);
color: var(--c-text-1, #fff);
font-weight: 650;
text-decoration: none;
transition: background 0.2s ease, border-color 0.2s ease;
}
.ptc__cta:hover:where([data-rk-101321]) { border-color: rgb(var(--ink-rgb, 255 255 255) / 0.3); color: var(--c-text-1, #fff); }
.ptc__card--featured:where([data-rk-101321]) .ptc__cta:where([data-rk-101321]) { background: var(--c-primary-light, #d4a017); color: var(--c-on-primary, #0a0a0a); border-color: transparent; }
.ptc__card--featured:where([data-rk-101321]) .ptc__cta:hover:where([data-rk-101321]) { background: var(--c-accent, #fef3c7); color: var(--c-on-primary, #0a0a0a); }
.ptc__cta:focus-visible:where([data-rk-101321]) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 3px; }
.ptc__features:where([data-rk-101321]) { list-style: none; margin: 1.75rem 0 0; padding: 1.5rem 0 0; border-top: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.07); display: grid; gap: 0.7rem; }
.ptc__features:where([data-rk-101321]) li:where([data-rk-101321]) { display: flex; align-items: center; gap: 0.6rem; font-size: 0.9rem; color: var(--c-text-1, #fff); }
.ptc__features:where([data-rk-101321]) svg:where([data-rk-101321]) { color: var(--c-primary-light, #d4a017); flex-shrink: 0; }
.ptc__live:where([data-rk-101321]) { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
@media (prefers-reduced-motion: reduce) { .ptc__knob:where([data-rk-101321]), .ptc__switch:where([data-rk-101321]) { transition: none; } }