Pricing Cards source

Inspect and copy the reviewed source included with this Free block.

Free source · 1 files

pricing_cards.rs

// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: (MIT OR Apache-2.0) AND MIT

// Adapted from MIT-licensed sources:
// - Tailark, `packages/dusk-kit/blocks/pricing/five/index.tsx`
//   https://github.com/tailark/blocks @ 8eadeb3389ccccc4bfc4a10bca959c1b39e47b71 (MIT License)
// - TWBlocks, `blocks/pricing/pricing1.tsx`
//   https://github.com/tommyjepsen/twblocks @ ff88cb951dab0dc01198b6570ec5f5cf21250d45 (MIT License)
//! Pricing cards block — clear tiers, feature lists, and a featured plan.

use proa_core::WebRenderSync;
use proa_macros::html_sync;
use proa_ui::{
    Badge, BadgeVariant, Button, ButtonSize, ButtonVariant, Card, CardContent, CardFooter,
    CardHeader,
};

struct Tier {
    name: &'static str,
    blurb: &'static str,
    price: &'static str,
    /// `None` on the custom-priced tier, which shows no "/ month" suffix.
    period: Option<&'static str>,
    features_heading: &'static str,
    features: &'static [&'static str],
    cta: &'static str,
    /// The recommended plan: gets the glow frame, the badge, and a solid button.
    featured: bool,
}

const TIERS: &[Tier] = &[
    Tier {
        name: "Launch",
        blurb: "For solo builders shipping the first version.",
        price: "$29",
        period: Some("/ month"),
        features_heading: "Everything to launch",
        features: &[
            "3 production projects",
            "Core component library",
            "Community support",
            "Automatic upgrades",
        ],
        cta: "Start with Launch",
        featured: false,
    },
    Tier {
        name: "Scale",
        blurb: "For teams turning reliable releases into growth.",
        price: "$89",
        period: Some("/ month"),
        features_heading: "Everything in Launch, plus",
        features: &[
            "Unlimited projects",
            "Advanced blocks and themes",
            "Up to 10 collaborators",
            "Priority implementation help",
        ],
        cta: "Choose Scale  →",
        featured: true,
    },
    Tier {
        name: "Company",
        blurb: "For organizations standardizing the product surface.",
        price: "Custom",
        period: None,
        features_heading: "Everything in Scale, plus",
        features: &[
            "Unlimited collaborators",
            "Private component registry",
            "Design-system onboarding",
            "Security and SLA review",
        ],
        cta: "Talk to our team",
        featured: false,
    },
];

/// Copy-paste-ready block composition.
pub struct PricingCardsBlock;

impl<B: proa_core::WriteBuf, L: proa_core::DataLoader> proa_core::WebRenderSync<L, B> for PricingCardsBlock {
    fn render(
        self,
        cx: &mut proa_core::WebContext<L, B>,
    ) -> Result<(), proa_core::WriteError> {
        html_sync! {
            <div class="flex flex-col gap-6">
                <section
                    data-block="pricing-cards"
                    class="rounded-3xl border border-border bg-muted/30 px-5 py-14 md:px-8 md:py-20 lg:px-12"
                >
                    <div class="mx-auto max-w-5xl">
                        <div class="mx-auto max-w-2xl text-center">
                            {Badge { variant: BadgeVariant::Outline, class: Some("rounded-full bg-background"), children: Some("Simple pricing"), }}
                            <h2 class="mt-5 text-balance text-3xl font-semibold tracking-tight text-foreground md:text-5xl">
                                "Start small. Keep your momentum."
                            </h2>
                            <p class="mx-auto mt-5 max-w-xl text-pretty text-lg leading-8 text-muted-foreground">
                                "Every plan includes the production essentials. Upgrade when your traffic, team, or ambitions demand it."
                            </p>
                            <div class="mx-auto mt-7 inline-flex items-center rounded-full border border-border bg-background px-4 py-2 text-sm text-muted-foreground">
                                "Monthly billing · cancel anytime"
                            </div>
                        </div>
                        <div class="mt-12 grid items-stretch gap-5 lg:grid-cols-3">
                            {for tier in TIERS {
                                // The featured plan sits in its own wrapper so the
                                // gradient frame can bleed a pixel past the card and
                                // the column can grow taller than its neighbours.
                                let card = Card {
                                    class: Some(if tier.featured {
                                        "relative flex h-full flex-col border-primary shadow-xl lg:py-3"
                                    } else {
                                        "flex h-full flex-col shadow-none"
                                    }),
                                    children: Some(html_sync! {
                                        {CardHeader { class: Some("gap-4"), children: Some(html_sync! {
                                            <div class="flex items-start justify-between gap-4">
                                                <div>
                                                    <h3 class="text-xl font-semibold text-foreground">
                                                        {tier.name}
                                                    </h3>
                                                    <p class="mt-2 min-h-10 text-sm leading-6 text-muted-foreground">
                                                        {tier.blurb}
                                                    </p>
                                                </div>
                                                {if tier.featured {
                                                    Badge { variant: BadgeVariant::Default, class: Some("rounded-full"), children: Some("Most popular") }.render(cx)?;
                                                }}
                                            </div>
                                            <p class="flex items-end gap-2 text-foreground">
                                                <span class="text-4xl font-semibold tracking-tight">
                                                    {tier.price}
                                                </span>
                                                {if let Some(period) = tier.period {
                                                    html_sync! {
                                                        <span class="pb-1 text-sm text-muted-foreground">
                                                            {period}
                                                        </span>
                                                    }.render(cx)?;
                                                }}
                                            </p>
                                        }), }}
                                        {CardContent { class: Some("flex-1"), children: Some(html_sync! {
                                            <p class="text-xs font-semibold uppercase tracking-widest text-muted-foreground">
                                                {tier.features_heading}
                                            </p>
                                            <ul class="mt-5 space-y-3 text-sm text-foreground">
                                                {for feature in tier.features {
                                                    html_sync! {
                                                        <li class="flex gap-3">
                                                            <span class="font-bold text-primary">"✓"</span>
                                                            {*feature}
                                                        </li>
                                                    }
                                                }}
                                            </ul>
                                        }), }}
                                        {CardFooter { children: Some(Button {
                                            size: Some(ButtonSize::Lg),
                                            variant: if tier.featured { None } else { Some(ButtonVariant::Outline) },
                                            class: Some("w-full"),
                                            children: Some(tier.cta),
                                            ..Default::default()
                                        }), ..Default::default() }}
                                    }),
                                };

                                if tier.featured {
                                    html_sync! {
                                        <div class="relative lg:-my-3">
                                            <div
                                                aria-hidden="true"
                                                class="absolute -inset-px rounded-[calc(0.75rem+1px)] bg-primary"
                                            ></div>
                                            {card}
                                        </div>
                                    }.render(cx)?;
                                } else {
                                    card.render(cx)?;
                                }
                            }}
                        </div>
                        <p class="mt-8 text-center text-sm text-muted-foreground">
                            "All plans include a 14-day trial. No credit card required. Cancel any time."
                        </p>
                    </div>
                </section>
            </div>
        }
        .render(cx)
    }
}

Copy this source with the Free Proa UI components it imports; those component files remain available from their own source pages.

Licensed under MIT OR Apache-2.0. Third-party notices remain attached to the installed payload.

Search

Type at least 2 characters

Customize

Make it yours

Palette

Radius

Overview
Components
Settings

Build your own

Themed Proa UI surface

Preview
Components52
RecipesTyped
customer-portal