Pricing Feature Tier Slider
blockpricingContinuous MAU and usage scale pricing slider with dynamic tier calculation and annual discount toggle.
Installation
$pnpm dlx uipkge-ng@latest add pricing-feature-tier-slider$npx uipkge-ng@latest add pricing-feature-tier-slider$yarn dlx uipkge-ng@latest add pricing-feature-tier-slider$bunx uipkge-ng@latest add pricing-feature-tier-sliderManifest:
https://uipkge.dev/r/angular/pricing-feature-tier-slider.jsonInstalls to:src/app/components/blocks/pricing-feature-tier-slider/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.
PricingTierinterface PricingTier {
label: string
mauCount: string
basePriceMonthly: number
tierName: string
features: string[]
}npm dependencies
Used by
Files installed (2)
src/app/components/blocks/pricing-feature-tier-slider/pricing-feature-tier-slider.component.ts10.9 kB
// Continuous MAU usage-scale pricing slider: stepped tier calculation with an annual // billing toggle and per-tier deliverables. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { ArrowRight, CheckCircle2, LucideAngularModule, Sliders } 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 } from '@/ui/card/card.component' export interface PricingTier { label: string mauCount: string basePriceMonthly: number tierName: string features: string[] } const TIERS: PricingTier[] = [ { label: '10K MAU', mauCount: '10,000', basePriceMonthly: 19, tierName: 'Starter Hobbyist', features: [ '10,000 Monthly Users', '2 Dedicated Edge Locations', 'Community Discord SLA', 'Standard 1-Day Log Retention', ], }, { label: '50K MAU', mauCount: '50,000', basePriceMonthly: 49, tierName: 'Pro Creator', features: [ '50,000 Monthly Users', '12 Global Anycast Edges', 'Next-Business-Day Support SLA', '7-Day Immutable Log Retention', ], }, { label: '250K MAU', mauCount: '250,000', basePriceMonthly: 149, tierName: 'Growth Scale', features: [ '250,000 Monthly Users', 'All 36 Global Edge Regions', '4-Hour Priority Engineering SLA', '30-Day SOC2 Audit Logs', ], }, { label: '1M MAU', mauCount: '1,000,000', basePriceMonthly: 399, tierName: 'Enterprise Core', features: [ '1,000,000 Monthly Users', 'Dedicated VPC & Multi-Region Mesh', '15-Minute Critical Incident SLA', '365-Day Cold Storage Logs', ], }, { label: '5M+ MAU', mauCount: '5,000,000+', basePriceMonthly: 899, tierName: 'Hyperscale Cluster', features: [ '5,000,000+ Monthly Users', 'Custom Bare-Metal Cloud Partitions', 'Dedicated Solutions Architect', 'Custom Security Review & BAA', ], }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-pricing-feature-tier-slider, [ui-pricing-feature-tier-slider]', standalone: true, // React renders the root <section> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent], template: ` <section data-slot="pricing-feature-tier-slider" [class]="rootClass"> <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> <!-- Section Header --> <div class="mx-auto max-w-3xl space-y-4 text-center"> <a href="#scale-pricing" class="group border-border/80 bg-secondary/60 hover:bg-secondary text-foreground inline-flex items-center gap-2 rounded-full border px-3.5 py-1 text-xs font-medium shadow-2xs transition-colors" > <lucide-icon [img]="Sliders" class="text-primary size-3.5" /> <span>Continuous Scale Synthesizer</span> <lucide-icon [img]="ArrowRight" class="text-muted-foreground size-3 transition-transform group-hover:translate-x-0.5" /> </a> <h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl"> {{ title() }} </h2> <p class="text-muted-foreground text-base sm:text-lg"> {{ description() }} </p> <!-- Cadence Switcher --> <div class="flex items-center justify-center gap-3 pt-2"> <span [class]="cadenceLabelClass(!isAnnual())">Monthly</span> <button type="button" aria-label="Toggle annual billing" [class]="cadenceTrackClass()" (click)="isAnnual.set(!isAnnual())" > <span aria-hidden="true" [class]="cadenceThumbClass()"></span> </button> <span [class]="cadenceAnnualLabelClass()"> <span>Annual Billing</span> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success text-xs"> Save 20% </span> </span> </div> </div> <!-- Pricing Slider Main Card --> <div class="mx-auto mt-12 max-w-4xl"> <div ui-card class="border-border bg-card overflow-hidden shadow-sm"> <div ui-card-content class="space-y-8 p-8"> <!-- Scale Indicator Bar --> <div class="border-border flex flex-wrap items-center justify-between gap-4 border-b pb-6"> <div class="space-y-1"> <span class="text-muted-foreground text-xs font-bold tracking-wider uppercase" >Calculated Tier Plan</span > <h3 class="text-foreground text-2xl font-bold">{{ currentTier().tierName }}</h3> <div class="text-muted-foreground text-xs"> Engineered for {{ currentTier().mauCount }} active user sessions </div> </div> <!-- Price Box --> <div class="text-right"> <div class="flex items-baseline justify-end gap-1"> <span class="text-foreground font-mono text-4xl font-bold">${{ effectivePrice() }}</span> <span class="text-muted-foreground font-mono text-xs">/ month</span> </div> @if (isAnnual()) { <div class="text-success font-mono text-xs font-medium"> Save ${{ annualSavings() }}/yr on annual </div> } </div> </div> <!-- Interactive Stepped Range Slider --> <div class="space-y-3"> <div class="text-foreground flex items-center justify-between text-xs font-bold"> <span>Monthly Traffic Volume</span> <span class="text-primary font-mono font-bold">{{ currentTier().mauCount }} Users</span> </div> <input type="range" min="0" max="4" step="1" aria-label="Monthly traffic volume tier" [value]="sliderIndex()" (input)="onSliderInput($event)" class="accent-primary h-2 w-full cursor-pointer" /> <!-- Slider Step Labels --> <div class="text-muted-foreground flex justify-between font-mono text-xs"> @for (t of tiers; track t.label) { <span [class]="stepLabelClass(t.label)" (click)="setTierByLabel(t.label)"> {{ t.label }} </span> } </div> </div> <!-- Features Checklist Grid for Current Tier --> <div class="border-border space-y-3 border-t pt-4"> <span class="text-muted-foreground text-xs font-bold tracking-wider uppercase" >Guaranteed Tier Deliverables</span > <div class="grid grid-cols-1 gap-2.5 sm:grid-cols-2"> @for (feat of currentTier().features; track $index) { <div class="text-foreground flex items-center gap-2 text-xs"> <lucide-icon [img]="CheckCircle2" class="text-success size-4 shrink-0" /> <span>{{ feat }}</span> </div> } </div> </div> <!-- Action Button --> <div class="pt-2"> <button ui-button size="lg" class="w-full gap-2 shadow-xs"> <span>Deploy with {{ currentTier().tierName }}</span> <lucide-icon [img]="ArrowRight" class="size-4" /> </button> </div> </div> </div> </div> </div> </section> `, }) export class UiPricingFeatureTierSliderComponent { protected readonly ArrowRight = ArrowRight protected readonly CheckCircle2 = CheckCircle2 protected readonly Sliders = Sliders protected readonly tiers = TIERS @Input('title') set titleInput(v: string) { if (v !== undefined) this.titleSignal.set(v) } @Input('description') set descriptionInput(v: string) { if (v !== undefined) this.descriptionSignal.set(v) } @Input('class') className?: string private readonly titleSignal = signal('Predictable usage pricing with dynamic linear scale.') private readonly descriptionSignal = signal( 'Slide to your estimated monthly active users or edge invocations to calculate your exact monthly investment.', ) readonly title = computed(() => this.titleSignal()) readonly description = computed(() => this.descriptionSignal()) readonly sliderIndex = signal(2) readonly isAnnual = signal(true) readonly currentTier = computed(() => TIERS[this.sliderIndex()] ?? TIERS[2]!) readonly effectivePrice = computed(() => { const base = this.currentTier().basePriceMonthly return this.isAnnual() ? Math.round(base * 0.8) : base }) readonly annualSavings = computed(() => (this.currentTier().basePriceMonthly - this.effectivePrice()) * 12) get rootClass(): string { return cn('bg-background relative overflow-hidden py-16 sm:py-24', this.className) } cadenceLabelClass(active: boolean): string { return cn('text-xs font-medium', active ? 'text-foreground font-semibold' : 'text-muted-foreground') } cadenceAnnualLabelClass(): string { return cn( 'flex items-center gap-1.5 text-xs font-medium', this.isAnnual() ? 'text-foreground font-semibold' : 'text-muted-foreground', ) } cadenceTrackClass(): string { return cn( 'focus-visible:ring-ring relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus-visible:ring-2 focus-visible:outline-none', this.isAnnual() ? 'bg-primary' : 'bg-muted', ) } cadenceThumbClass(): string { return cn( 'bg-background pointer-events-none inline-block size-5 transform rounded-full shadow-lg ring-0 transition duration-200 ease-in-out', this.isAnnual() ? 'translate-x-5' : 'translate-x-0', ) } stepLabelClass(label: string): string { return cn( 'hover:text-foreground cursor-pointer transition-colors', this.currentTier().label === label ? 'text-primary font-bold' : '', ) } setTierByLabel(label: string): void { const idx = TIERS.findIndex((t) => t.label === label) if (idx >= 0) this.sliderIndex.set(idx) } onSliderInput(event: Event): void { const value = Number((event.target as HTMLInputElement).value) if (Number.isInteger(value) && value >= 0 && value < TIERS.length) this.sliderIndex.set(value) } }src/app/components/blocks/pricing-feature-tier-slider/index.ts0.1 kB
export { UiPricingFeatureTierSliderComponent, type PricingTier } from './pricing-feature-tier-slider.component'
Raw manifest:https://uipkge.dev/r/angular/pricing-feature-tier-slider.json