1 звезда на GitHub = 1 новый компонент.

Поставить звезду
Тарифные планы

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
  • 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; } }