Pricing
The Pricing component provides you with pricing tables and plans.
Live Examples
View Code
View Code
View Code
View Code
Import
import { Pricing } from "@jarllyng/nostromo";
// or
import { Pricing } from "@jarllyng/nostromo/pricing";Usage
import { Pricing } from "@jarllyng/nostromo";
export default function Example() {
return (
<Pricing
title="Choose Your Plan"
plans={[
{
id: "starter",
name: "Starter",
price: { monthly: 9 },
period: "month",
features: [
{ id: "projects", name: "Up to 5 projects", included: true },
{ id: "sso", name: "SSO", included: false },
],
cta: { text: "Get Started", href: "/signup" },
},
]}
/>
);
}Monthly and yearly
The switch appears as soon as you pass onToggleBilling, and showYearly says
which way it is set. The component does not hold that state itself, so the
period shown is always the one you asked for.
A plan is billed yearly only if it has a yearly price. One without keeps its
monthly amount and its monthly period, rather than presenting a monthly figure
under a /year label.
import React from "react";
import { Pricing } from "@jarllyng/nostromo";
export default function Example() {
const [yearly, setYearly] = React.useState(false);
return (
<Pricing
showYearly={yearly}
onToggleBilling={setYearly}
plans={[
{
id: "pro",
name: "Pro",
price: { monthly: 29, yearly: 290 },
features: [{ id: "sso", name: "SSO", included: true }],
cta: { text: "Start free trial", href: "/signup" },
},
]}
/>
);
}The saving beside the switch is worked out from the prices themselves: 290
against 29 a month is 17% off, so that is what it says. When the plans save
different amounts it reports the best one as “up to”, and when nothing is saved
it says nothing at all. showYearlyDiscount={false} removes it, and
yearlyDiscountLabel rewords it:
<Pricing
showYearly={yearly}
onToggleBilling={setYearly}
billingToggleLabel="Skift til årlig betaling"
yearlyDiscountLabel={(percent, exact) =>
exact ? `(Spar ${percent}%)` : `(Spar op til ${percent}%)`
}
plans={plans}
/>Calls to action
cta.href renders a link and cta.onClick renders a button, so the control
matches what it does. A plan that gives both navigates and calls the handler,
which is the shape analytics usually wants.
{ text: "Get Started", href: "/signup" } // <a href="/signup">
{ text: "Contact Sales", onClick: openSalesModal } // <button type="button">
{ text: "Start trial", href: "/signup", onClick: track } // bothProps
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
plans | PricingPlan[] | - | Yes | The plans to show (see the interface below) |
title | string | undefined | No | Section title |
subtitle | string | undefined | No | Section subtitle |
columns | 1 | 2 | 3 | 4 | 3 | No | Columns in the grid |
variant | "default" | "muted" | "accent" | "default" | No | Section background |
showYearly | boolean | false | No | Whether yearly prices are the ones shown |
onToggleBilling | (yearly: boolean) => void | undefined | No | Pass it to render the switch; it reports the flip |
billingToggleLabel | string | "Bill yearly" | No | Accessible name for that switch |
showYearlyDiscount | boolean | true | No | Show what the yearly prices save |
yearlyDiscountLabel | (percent: number, exact: boolean) => string | (percent, exact) => ... | No | Wording of the saving |
className | string | undefined | No | Additional CSS classes |
PricingPlan
interface PricingPlan {
id: string;
name: string;
description?: string;
/** `yearly` is optional; a plan without one is never billed yearly. */
price: { monthly: number; yearly?: number };
currency?: string;
/**
* Overrides the period label, which is otherwise "month" or "year".
* It is used for both, so leave it off if the plan can be billed yearly.
*/
period?: string;
features: PricingFeature[];
/** `href` renders a link, `onClick` a button, both render a link that calls it. */
cta: { text: string; href?: string; onClick?: () => void };
badge?: {
text: string;
variant?: "default" | "accent" | "success" | "warning";
};
popular?: boolean;
}
interface PricingFeature {
id: string;
name: string;
/** A feature the plan does not have is struck through rather than dropped. */
included: boolean;
}