Pricing Calculator
blockmarketingInteractive SaaS and API pricing calculator with dynamic seat, request throughput, and cloud storage sliders, annual billing toggle with 20% discount, enterprise add-on switches, automated tier recommendation, and sticky live estimated cost breakdown.
Installation
$pnpm dlx uipkge-ng@latest add pricing-calculator$npx uipkge-ng@latest add pricing-calculator$yarn dlx uipkge-ng@latest add pricing-calculator$bunx uipkge-ng@latest add pricing-calculatorManifest:
https://uipkge.dev/r/angular/pricing-calculator.jsonInstalls to:src/app/components/blocks/pricing-calculator/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
PricingCalculatorRequestTierinterface PricingCalculatorRequestTier {
label: string
requests: number
cost: number
}PricingCalculatorStorageTierinterface PricingCalculatorStorageTier {
label: string
gb: number
cost: number
}Used by
Files installed (2)
src/app/components/blocks/pricing-calculator/pricing-calculator.component.ts23.5 kB
// Interactive SaaS and API pricing calculator: seat, request throughput, and cloud storage // sliders, annual billing toggle with 20% discount, enterprise add-on switches, automated tier // recommendation, and a sticky live estimated cost breakdown. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { ArrowRight, Check, HardDrive, Headphones, KeyRound, LucideAngularModule, ShieldCheck, Users, Zap, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiSliderComponent } from '@/ui/slider/slider.component' import { UiSwitchComponent } from '@/ui/switch/switch.component' export interface PricingCalculatorRequestTier { label: string requests: number cost: number } export interface PricingCalculatorStorageTier { label: string gb: number cost: number } const REQUEST_TIERS: PricingCalculatorRequestTier[] = [ { label: '10K requests', requests: 10_000, cost: 0 }, { label: '50K requests', requests: 50_000, cost: 25 }, { label: '100K requests', requests: 100_000, cost: 60 }, { label: '250K requests', requests: 250_000, cost: 120 }, { label: '500K requests', requests: 500_000, cost: 220 }, { label: '1M requests', requests: 1_000_000, cost: 400 }, { label: '2.5M requests', requests: 2_500_000, cost: 750 }, { label: '5M requests', requests: 5_000_000, cost: 1200 }, ] const STORAGE_TIERS: PricingCalculatorStorageTier[] = [ { label: '100 GB', gb: 100, cost: 10 }, { label: '250 GB', gb: 250, cost: 25 }, { label: '500 GB', gb: 500, cost: 45 }, { label: '1 TB', gb: 1_000, cost: 80 }, { label: '2 TB', gb: 2_000, cost: 150 }, { label: '5 TB', gb: 5_000, cost: 320 }, { label: '10 TB', gb: 10_000, cost: 580 }, ] const SEAT_PRICE = 15 const SUPPORT_MANAGER_PRICE = 200 const CUSTOM_SLA_PRICE = 500 const SSO_PRICE = 100 @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-pricing-calculator, [ui-pricing-calculator]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, UiSeparatorComponent, UiSliderComponent, UiSwitchComponent, ], template: ` <div data-slot="pricing-calculator" [class]="rootClass"> <div class="flex flex-col items-center gap-4 text-center"> <span ui-badge variant="outline" class="gap-1.5 px-3 py-1 text-xs font-medium tracking-wider uppercase"> <lucide-icon [img]="ShieldCheck" class="text-primary size-3.5" /> Pricing Calculator </span> <div class="space-y-2"> <h2 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl lg:text-4xl"> Interactive Pricing Calculator </h2> <p class="text-muted-foreground mx-auto max-w-2xl text-sm sm:text-base"> Calculate your exact monthly or annual investment based on usage. </p> </div> <div class="border-border bg-card mt-2 inline-flex items-center gap-3 rounded-full border px-4 py-2 shadow-xs"> <span [class]="billingLabelClass(!isAnnual())">Monthly</span> <button ui-switch aria-label="Toggle annual billing" [checked]="isAnnual()" (checkedChange)="isAnnual.set($event)" ></button> <span [class]="billingLabelClass(isAnnual())">Pay Annually</span> <span ui-badge variant="secondary" class="bg-primary/10 text-primary border-primary/20 text-xs font-semibold"> Save 20% </span> </div> </div> <div class="grid grid-cols-1 gap-8 lg:grid-cols-12 lg:items-start"> <div class="space-y-6 lg:col-span-7"> <div ui-card> <div ui-card-header> <h3 ui-card-title>Capacity & Scale</h3> <p ui-card-description>Configure team seats, API request throughput, and dedicated storage.</p> </div> <div ui-card-content class="space-y-8"> <div class="space-y-4"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2.5"> <div class="bg-primary/10 text-primary border-primary/20 flex size-8 shrink-0 items-center justify-center rounded-md border" > <lucide-icon [img]="Users" class="size-4" /> </div> <div> <span class="text-foreground text-sm font-medium">Team Seats</span> <span ui-badge [variant]="seatTier().variant" class="ml-2 text-xs font-normal"> {{ seatTier().label }} </span> </div> </div> <div class="text-right"> <span class="text-foreground text-base font-semibold tabular-nums">{{ seats() }}</span> <span class="text-muted-foreground text-xs"> seats (${{ seats() * seatPrice }}/mo)</span> </div> </div> <div class="pb-6"> <ui-slider [value]="[seats()]" [min]="1" [max]="100" [step]="1" [marks]="seatMarks" (valueChange)="onSeatsChange($event)" /> </div> <p class="text-muted-foreground text-xs"> Full workspace access, fine-grained permission controls, and audit log tracking for each member. </p> </div> <div ui-separator></div> <div class="space-y-4"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2.5"> <div class="bg-primary/10 text-primary border-primary/20 flex size-8 shrink-0 items-center justify-center rounded-md border" > <lucide-icon [img]="Zap" class="size-4" /> </div> <div> <span class="text-foreground text-sm font-medium">Monthly API Requests</span> </div> </div> <div class="text-right"> <span class="text-foreground text-base font-semibold tabular-nums"> {{ currentRequestTier().label }} </span> <span class="text-muted-foreground text-xs"> ({{ currentRequestTier().cost === 0 ? 'Included' : '+' + '$' + currentRequestTier().cost + '/mo' }}) </span> </div> </div> <div class="pb-6"> <ui-slider [value]="[requestIndex()]" [min]="0" [max]="7" [step]="1" [marks]="requestMarks" (valueChange)="onRequestChange($event)" /> </div> <p class="text-muted-foreground text-xs"> Globally distributed edge endpoints, automatic rate limiting, and sub-50ms p99 latency SLA. </p> </div> <div ui-separator></div> <div class="space-y-4"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2.5"> <div class="bg-primary/10 text-primary border-primary/20 flex size-8 shrink-0 items-center justify-center rounded-md border" > <lucide-icon [img]="HardDrive" class="size-4" /> </div> <div> <span class="text-foreground text-sm font-medium">Dedicated Cloud Storage</span> </div> </div> <div class="text-right"> <span class="text-foreground text-base font-semibold tabular-nums"> {{ currentStorageTier().label }} </span> <span class="text-muted-foreground text-xs"> (+${{ currentStorageTier().cost }}/mo)</span> </div> </div> <div class="pb-6"> <ui-slider [value]="[storageIndex()]" [min]="0" [max]="6" [step]="1" [marks]="storageMarks" (valueChange)="onStorageChange($event)" /> </div> <p class="text-muted-foreground text-xs"> Encrypted at rest (AES-256) with multi-region automated replication and daily disaster backups. </p> </div> </div> </div> <div ui-card> <div ui-card-header> <h3 ui-card-title>Enterprise Add-ons</h3> <p ui-card-description> Enhance your infrastructure with enterprise compliance, reliability, and support. </p> </div> <div ui-card-content class="space-y-3"> <div class="border-border bg-card hover:border-primary/30 flex flex-col justify-between gap-4 rounded-lg border p-4 transition-colors sm:flex-row sm:items-center" > <div class="flex items-start gap-3"> <div class="bg-primary/10 text-primary border-primary/20 mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border" > <lucide-icon [img]="Headphones" class="size-4" /> </div> <div class="space-y-0.5"> <div class="flex items-center gap-2"> <span class="text-foreground text-sm font-medium">Dedicated Support Manager</span> <span ui-badge variant="outline" class="text-xs font-normal">+$200/mo</span> </div> <p class="text-muted-foreground text-xs"> Direct Slack channel, named technical account manager & 1-hour response SLA. </p> </div> </div> <div class="flex sm:justify-end"> <button ui-switch aria-label="Toggle dedicated support manager add-on" [checked]="addonSupport()" (checkedChange)="addonSupport.set($event)" ></button> </div> </div> <div class="border-border bg-card hover:border-primary/30 flex flex-col justify-between gap-4 rounded-lg border p-4 transition-colors sm:flex-row sm:items-center" > <div class="flex items-start gap-3"> <div class="bg-primary/10 text-primary border-primary/20 mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border" > <lucide-icon [img]="ShieldCheck" class="size-4" /> </div> <div class="space-y-0.5"> <div class="flex items-center gap-2"> <span class="text-foreground text-sm font-medium">Custom SLA Guarantee</span> <span ui-badge variant="outline" class="text-xs font-normal">+$500/mo</span> </div> <p class="text-muted-foreground text-xs"> 99.99% uptime guarantee with financial commitments and priority disaster recovery. </p> </div> </div> <div class="flex sm:justify-end"> <button ui-switch aria-label="Toggle custom SLA guarantee add-on" [checked]="addonSla()" (checkedChange)="addonSla.set($event)" ></button> </div> </div> <div class="border-border bg-card hover:border-primary/30 flex flex-col justify-between gap-4 rounded-lg border p-4 transition-colors sm:flex-row sm:items-center" > <div class="flex items-start gap-3"> <div class="bg-primary/10 text-primary border-primary/20 mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border" > <lucide-icon [img]="KeyRound" class="size-4" /> </div> <div class="space-y-0.5"> <div class="flex items-center gap-2"> <span class="text-foreground text-sm font-medium">Single Sign-On (SSO / SAML)</span> <span ui-badge variant="outline" class="text-xs font-normal">+$100/mo</span> </div> <p class="text-muted-foreground text-xs"> Okta, Azure AD, Google Workspace, and SAML 2.0 enterprise identity integration. </p> </div> </div> <div class="flex sm:justify-end"> <button ui-switch aria-label="Toggle SSO add-on" [checked]="addonSso()" (checkedChange)="addonSso.set($event)" ></button> </div> </div> </div> </div> </div> <div class="lg:sticky lg:top-8 lg:col-span-5"> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-4"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Estimated Investment </span> <span ui-badge [variant]="recommendedPlan().variant" class="gap-1 shadow-xs"> @if (recommendedPlan().name === 'Enterprise') { <lucide-icon [img]="ShieldCheck" class="size-3" /> } {{ recommendedPlan().badge }} </span> </div> <div class="mt-4"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-4xl font-bold tracking-tight tabular-nums sm:text-5xl"> ${{ totalMonthly() }} </span> <span class="text-muted-foreground text-sm font-normal"> / month</span> </div> @if (isAnnual()) { <p class="text-muted-foreground mt-2 text-xs"> Billed annually (${{ totalAnnual().toLocaleString('en-US') }}/yr) · <span class="text-success font-semibold" >Save ${{ annualSavings().toLocaleString('en-US') }}/yr</span > </p> } @else { <p class="text-muted-foreground mt-2 text-xs"> Billed monthly · Switch to annual to save 20% (${{ annualSavings().toLocaleString('en-US') }}/yr) </p> } </div> </div> <div ui-card-content class="space-y-4"> <div ui-separator></div> <div class="space-y-2.5"> <div class="flex items-center justify-between text-sm"> <span class="text-muted-foreground">Base platform ({{ recommendedPlan().name }})</span> <span class="font-medium tabular-nums">${{ annualAdjusted(baseCost()) }}/mo</span> </div> <div class="flex items-center justify-between text-sm"> <span class="text-muted-foreground">Team seats ({{ seats() }} × ${{ seatPrice }})</span> <span class="font-medium tabular-nums">${{ annualAdjusted(seatsCost()) }}/mo</span> </div> <div class="flex items-center justify-between text-sm"> <span class="text-muted-foreground">API throughput ({{ currentRequestTier().label }})</span> <span class="font-medium tabular-nums"> {{ requestsCost() === 0 ? 'Included' : '$' + annualAdjusted(requestsCost()) + '/mo' }} </span> </div> <div class="flex items-center justify-between text-sm"> <span class="text-muted-foreground">Cloud storage ({{ currentStorageTier().label }})</span> <span class="font-medium tabular-nums"> ${{ annualAdjusted(storageCost()) }}/mo </span> </div> <div class="flex items-center justify-between text-sm"> <span class="text-muted-foreground">Add-ons ({{ activeAddonsCount() }} active)</span> <span class="font-medium tabular-nums"> {{ addonsCost() === 0 ? '$0/mo' : '$' + annualAdjusted(addonsCost()) + '/mo' }} </span> </div> @if (isAnnual()) { <div class="bg-success/10 text-success flex items-center justify-between rounded-md px-2.5 py-1.5 text-xs font-medium" > <span>Annual discount applied</span> <span class="font-semibold tabular-nums">20% off</span> </div> } </div> <div ui-separator></div> <ul class="text-muted-foreground space-y-2 text-xs"> <li class="flex items-center gap-2"> <lucide-icon [img]="Check" class="text-primary size-3.5 shrink-0" /> <span>14-day fully featured free trial</span> </li> <li class="flex items-center gap-2"> <lucide-icon [img]="Check" class="text-primary size-3.5 shrink-0" /> <span>No credit card required upfront</span> </li> <li class="flex items-center gap-2"> <lucide-icon [img]="Check" class="text-primary size-3.5 shrink-0" /> <span>Zero-downtime migration assistance</span> </li> </ul> </div> <div ui-card-footer class="flex flex-col gap-2.5 pt-2"> <button ui-button class="w-full gap-2 font-semibold shadow-xs" size="lg"> Start 14-Day Free Trial <lucide-icon [img]="ArrowRight" class="size-4" /> </button> <button ui-button variant="outline" class="w-full" size="default">Request Custom Quote</button> </div> </div> </div> </div> </div> `, }) export class UiPricingCalculatorComponent { protected readonly ArrowRight = ArrowRight protected readonly Check = Check protected readonly HardDrive = HardDrive protected readonly Headphones = Headphones protected readonly KeyRound = KeyRound protected readonly ShieldCheck = ShieldCheck protected readonly Users = Users protected readonly Zap = Zap protected readonly seatPrice = SEAT_PRICE protected readonly seatMarks: Record<number, string> = { 1: '1', 25: '25', 50: '50', 75: '75', 100: '100' } protected readonly requestMarks: Record<number, string> = { 0: '10K', 2: '100K', 4: '500K', 7: '5M' } protected readonly storageMarks: Record<number, string> = { 0: '100 GB', 2: '500 GB', 4: '2 TB', 6: '10 TB' } @Input() set initialAnnual(v: boolean) { this.isAnnual.set(!!v) } @Input('class') className?: string readonly isAnnual = signal(true) readonly seats = signal(12) readonly requestIndex = signal(2) readonly storageIndex = signal(2) readonly addonSupport = signal(false) readonly addonSla = signal(false) readonly addonSso = signal(true) readonly currentRequestTier = computed(() => REQUEST_TIERS[this.requestIndex()] ?? REQUEST_TIERS[0]!) readonly currentStorageTier = computed(() => STORAGE_TIERS[this.storageIndex()] ?? STORAGE_TIERS[0]!) readonly seatTier = computed(() => { if (this.seats() <= 5) return { label: 'Starter Team', variant: 'secondary' as const } if (this.seats() <= 25) return { label: 'Growth Team', variant: 'outline' as const } return { label: 'Scale Team', variant: 'default' as const } }) readonly recommendedPlan = computed(() => { if (this.addonSla() || this.requestIndex() >= 6 || this.storageIndex() >= 5 || this.seats() >= 30) { return { name: 'Enterprise', badge: 'Enterprise Plan', baseFee: 199, variant: 'default' as const } } if (this.seats() >= 8 || this.requestIndex() >= 3 || this.storageIndex() >= 3 || this.addonSupport()) { return { name: 'Growth', badge: 'Growth Plan', baseFee: 79, variant: 'default' as const } } return { name: 'Starter', badge: 'Starter Plan', baseFee: 29, variant: 'secondary' as const } }) readonly baseCost = computed(() => this.recommendedPlan().baseFee) readonly seatsCost = computed(() => this.seats() * SEAT_PRICE) readonly requestsCost = computed(() => this.currentRequestTier().cost) readonly storageCost = computed(() => this.currentStorageTier().cost) readonly addonsCost = computed(() => { let cost = 0 if (this.addonSupport()) cost += SUPPORT_MANAGER_PRICE if (this.addonSla()) cost += CUSTOM_SLA_PRICE if (this.addonSso()) cost += SSO_PRICE return cost }) readonly activeAddonsCount = computed(() => { let count = 0 if (this.addonSupport()) count++ if (this.addonSla()) count++ if (this.addonSso()) count++ return count }) readonly subtotalMonthly = computed( () => this.baseCost() + this.seatsCost() + this.requestsCost() + this.storageCost() + this.addonsCost(), ) readonly discountMultiplier = computed(() => (this.isAnnual() ? 0.8 : 1.0)) readonly totalMonthly = computed(() => Math.round(this.subtotalMonthly() * this.discountMultiplier())) readonly totalAnnual = computed(() => this.totalMonthly() * 12) readonly annualSavings = computed(() => Math.round(this.subtotalMonthly() * 0.2 * 12)) get rootClass(): string { return cn('mx-auto w-full max-w-6xl space-y-10 p-4 sm:p-6 lg:p-8', this.className) } billingLabelClass(active: boolean): string { return cn( 'text-xs font-medium transition-colors', active ? 'text-foreground font-semibold' : 'text-muted-foreground', ) } annualAdjusted(cost: number): number { return this.isAnnual() ? Math.round(cost * 0.8) : cost } onSeatsChange(value: number[]): void { const next = value[0] if (next !== undefined) this.seats.set(next) } onRequestChange(value: number[]): void { const next = value[0] if (next !== undefined) this.requestIndex.set(next) } onStorageChange(value: number[]): void { const next = value[0] if (next !== undefined) this.storageIndex.set(next) } }src/app/components/blocks/pricing-calculator/index.ts0.2 kB
export { UiPricingCalculatorComponent, type PricingCalculatorRequestTier, type PricingCalculatorStorageTier, } from './pricing-calculator.component'
Raw manifest:https://uipkge.dev/r/angular/pricing-calculator.json