Skip to content

TkxPlanSelector, TkxBillingCycleToggle, TkxProrationPreview

Three components for SaaS-style pricing pages and plan-change flows. None of them call payment providers directly — they’re UI for the decision; you wire TkxPaymentButton for the actual charge.

Pricing-card grid with optional “Most popular” highlight, feature list, and click-to-select.

import { TkxPlanSelector, type SubscriptionPlan } from 'tekivex-ui';
const PLANS: SubscriptionPlan[] = [
{
id: 'free',
name: 'Free',
tagline: 'For individuals',
prices: { monthly: 0, annual: 0 },
currency: 'USD',
features: ['1 user', '100 MB', 'Community support'],
},
{
id: 'pro',
name: 'Pro',
tagline: 'For small teams',
prices: { monthly: 29, annual: 290 },
currency: 'USD',
features: ['10 users', '10 GB', 'Priority support', 'API access'],
highlighted: true,
},
{
id: 'biz',
name: 'Business',
prices: { monthly: 99, annual: 990 },
currency: 'USD',
features: ['Unlimited users', '1 TB', 'SAML SSO', 'Audit log', 'SLA'],
},
];
const [selected, setSelected] = useState<string>('pro');
<TkxPlanSelector
plans={PLANS}
cycle="annual"
selectedId={selected}
onSelect={(plan) => setSelected(plan.id)}
/>

Renders as <button role="radio"> per card with proper aria-checked — keyboard-accessible and screen-reader-friendly.

Monthly ↔ Annual switch with optional savings indicator.

import { TkxBillingCycleToggle, type BillingCycle } from 'tekivex-ui';
const [cycle, setCycle] = useState<BillingCycle>('annual');
<TkxBillingCycleToggle
value={cycle}
onChange={setCycle}
annualSavingsLabel="Save 17%"
/>

Mid-cycle plan-change calculator. Shows credit from the current plan + prorated charge for the new one + amount due today.

import { TkxProrationPreview, TkxButton } from 'tekivex-ui';
<TkxProrationPreview
currentPlan={PLANS.find(p => p.id === 'free')!}
newPlan={PLANS.find(p => p.id === 'pro')!}
cycle="annual"
daysRemaining={142}
cycleDays={365}
>
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
<TkxButton variant="ghost" onClick={cancel}>Cancel</TkxButton>
<TkxButton variant="primary" onClick={confirm}>Confirm change</TkxButton>
</div>
</TkxProrationPreview>

Math is straightforward proration: (newPrice - currentPrice) × daysRemaining / cycleDays. If the result is negative (downgrade), it shows a credit applied at next billing instead.

The natural three-step pricing page:

function PricingPage() {
const [cycle, setCycle] = useState<BillingCycle>('annual');
const [plan, setPlan] = useState<SubscriptionPlan | null>(null);
return (
<div>
<header style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<h1>Choose a plan</h1>
<TkxBillingCycleToggle
value={cycle}
onChange={setCycle}
annualSavingsLabel="Save 17%"
/>
</header>
<TkxPlanSelector
plans={PLANS}
cycle={cycle}
selectedId={plan?.id}
onSelect={setPlan}
/>
{plan && plan.id !== 'free' && (
<TkxPaymentButton
config={{
provider: 'stripe',
publicKey: env.STRIPE_PK,
sessionId: serverSessionId,
}}
onSuccess={(r) => router.push(`/welcome?id=${r.paymentId}`)}
/>
)}
</div>
);
}
  • TkxPlanSelector uses role="radiogroup" + role="radio" so screen readers announce “1 of 3 selected, Pro plan, $29/month, most popular”
  • TkxBillingCycleToggle uses the same pattern
  • TkxProrationPreview is a static breakdown with semantic structure — no special interaction needed
PropTypeDescription
plansSubscriptionPlan[]Plans to display.
cycleBillingCycleWhich price column (monthly or annual).
selectedIdstringCurrently selected plan id.
onSelect(plan: SubscriptionPlan) => void
PropTypeDescription
value'monthly' | 'annual'
onChange(next) => void
annualSavingsLabelstringBadge on annual button.
PropTypeDescription
newPlanSubscriptionPlanTarget plan.
currentPlanSubscriptionPlanCurrent plan.
cycleBillingCycle
daysRemainingnumberDays left in current cycle.
cycleDaysnumberCycle length (typically 30 or 365).

src/components/TkxSubscription.tsx