PricingTable

The feature-by-feature comparison under a set of plans: tiers across the top, features grouped into sections down the side, and a checkmark, a rule or a value in each cell. `orientation="vertical"` re-reads the same data as one `PricingPlan` per tier for small screens.

vue
<UiPricingTable :tiers="..." :sections="..." />

Options

One block per prop — what it does, and what it looks like set.

Tiers

required

tiers are the columns, left to right. Each one carries the same figures as a PricingPlan — name, price, badge, button — plus the id every feature row is keyed against. highlight tints that tier's whole column from the header down to the last row.

Free

For trying things out.

$0/month
ProMost popular

For small teams.

$29/month

billed annually

Enterprise

For procurement teams.

Custom
Usage
Projects 1UnlimitedUnlimited
Team members 110Unlimited
Build minutes
50010,000Unlimited
Security
Two-factor authentication IncludedIncludedIncluded
SSO (SAML) Not includedNot includedIncluded
Audit log Not includedIncludedIncluded
Custom data residency Not includedNot includedIncluded
Support
Community forum IncludedIncludedIncluded
Email support Not includedNext business day1 hour
Named engineer Not includedNot includedIncluded

A tier's id is load-bearing twice: it's the key every feature's tiers record is written against, and it names that column's #<id>-cell slot. Renaming one means renaming all three.

Sections

required

sections are the feature rows, grouped. Each feature's tiers record says what every column gets: true draws a checkmark, false or a missing key draws a rule, and a string or number is printed as-is — which is how one row says '3' for one tier and 'Unlimited' for the next. A feature's description adds an Info icon beside its name.

Free

For trying things out.

$0/month
ProMost popular

For small teams.

$29/month

billed annually

Usage
Projects 1Unlimited
Team members 110
Build minutes
50010,000
Security
Two-factor authentication IncludedIncluded
SSO (SAML) Not includedNot included
Audit log Not includedIncluded
Custom data residency Not includedNot included
Support
Community forum IncludedIncluded
Email support Not includedNext business day
Named engineer Not includedNot included

A feature only needs an entry for the tiers that have it — a missing key renders exactly like false. Convenient on a wide table, but it also means a typo in a tier id reads as not included rather than failing.

Label

label is the heading over the feature column, in the otherwise-empty top-left cell. Name it after what the rows actually are — 'Features', 'Limits', 'What's included' — rather than leaving a reader to infer it from the first section.

What's included
Free

For trying things out.

$0/month
ProMost popular

For small teams.

$29/month

billed annually

Usage
Projects 1Unlimited
Team members 110
Build minutes
50010,000

Caption

caption is the line above the table — the currency, what the prices exclude, when they were last changed. It's the place for the footnote that would otherwise end up in a superscript nobody reads.

All prices in USD and exclude VAT. Annual plans are billed as one payment.

Free

For trying things out.

$0/month
ProMost popular

For small teams.

$29/month

billed annually

Usage
Projects 1Unlimited
Team members 110
Build minutes
50010,000

Orientation

horizontal is the comparison grid. vertical re-reads exactly the same tiers and sections as one PricingPlan per tier, each section a CheckList — because a grid of five columns can't be made readable on a phone, only smaller. Switch on a breakpoint rather than committing to one.

Free

For trying things out.

$0/month

Usage

  • Projects1
  • Team members1
  • Build minutes500

Security

  • Two-factor authentication
  • SSO (SAML)
  • Audit log
  • Custom data residency

Support

  • Community forum
  • Email support
  • Named engineer

Pro

Most popular

For small teams.

$29/month

billed annually

Usage

  • ProjectsUnlimited
  • Team members10
  • Build minutes10,000

Security

  • Two-factor authentication
  • SSO (SAML)
  • Audit log
  • Custom data residency

Support

  • Community forum
  • Email supportNext business day
  • Named engineer

vertical renders PricingPlans with one PricingPlan per tier, so a tier's fields map onto plan props and each section becomes a CheckList. The #<id>-cell slots and sticky are horizontal-only and have no effect there.

Sticky

sticky pins the tier header row to the top of the viewport while the features scroll past it. On any comparison long enough to need scrolling, it's the difference between reading a checkmark and remembering which column it was in. Horizontal only.

Free

For trying things out.

$0/month
ProMost popular

For small teams.

$29/month

billed annually

Enterprise

For procurement teams.

Custom
Usage
Projects 1UnlimitedUnlimited
Team members 110Unlimited
Build minutes
50010,000Unlimited
Security
Two-factor authentication IncludedIncludedIncluded
SSO (SAML) Not includedNot includedIncluded
Audit log Not includedIncludedIncluded
Custom data residency Not includedNot includedIncluded
Support
Community forum IncludedIncludedIncluded
Email support Not includedNext business day1 hour
Named engineer Not includedNot includedIncluded
Deployment
Custom domains Not included10Unlimited
Preview deployments IncludedIncludedIncluded
Concurrent builds
1520
Rollbacks Not includedIncludedIncluded
99.9% uptime SLA Not includedNot includedIncluded

Sticky positions against the nearest scrolling ancestor, so a table inside an overflow-y-auto container pins to that container's top rather than to the viewport's.

Color

color themes the checkmarks and the tint on the highlighted tier's column.

Free

For trying things out.

$0/month
ProMost popular

For small teams.

$29/month

billed annually

Usage
Projects 1Unlimited
Team members 110
Build minutes
50010,000
Free

For trying things out.

$0/month
ProMost popular

For small teams.

$29/month

billed annually

Usage
Projects 1Unlimited
Team members 110
Build minutes
50010,000

Cross unchecked

crossUnchecked marks what a tier doesn't get with a red cross instead of a rule. It's worth it on a short table where the gaps are the argument you're making; on a forty-row comparison it turns the cheaper columns into a wall of red, which reads as a warning rather than a summary.

Free

For trying things out.

$0/month
ProMost popular

For small teams.

$29/month

billed annually

Security
Two-factor authentication IncludedIncluded
SSO (SAML) Not includedNot included
Audit log Not includedIncluded
Custom data residency Not includedNot included
Free

For trying things out.

$0/month
ProMost popular

For small teams.

$29/month

billed annually

Security
Two-factor authentication IncludedIncluded
SSO (SAML) Not includedNot included
Audit log Not includedIncluded
Custom data residency Not includedNot included

Examples

The component in real use: compositions that answer "how do I build X with this", rather than one prop at a time.

Switching orientation on a breakpoint

The two orientations render the same data, so pick between them from the viewport rather than from the content. useMediaQuery against the md breakpoint is enough — and it's the reason the vertical form was built out of PricingPlan rather than as a second table.

Free

For trying things out.

$0/month

Usage

  • Projects1
  • Team members1
  • Build minutes500

Pro

Most popular

For small teams.

$29/month

billed annually

Usage

  • ProjectsUnlimited
  • Team members10
  • Build minutes10,000

Custom cells

Each tier gets a #<id>-cell slot for the values a checkmark can't carry — a limit that's coming soon, a value worth linking. It's handed the tier, the feature and the raw value, so you can fall back to the default rendering for everything you don't want to change. Horizontal only; the vertical form renders a CheckList instead.

Free

For trying things out.

$0/month
ProMost popular

For small teams.

$29/month

billed annually

Support
Community forum trueIncluded
Email support Coming soonNext business day
Named engineer Not included

A missing key and false mean the same thing

A feature only needs an entry for the tiers that have it — anything absent from its tiers record renders exactly like false. That keeps a wide table's data readable, but it also means a typo in a tier id silently reads as 'not included' rather than failing.


API Reference

Generated from the component's source — props, slots and emits as the component actually declares them.

Props

Prop
Type
Default
Description
tiersrequired
PricingTableTier[]
—
The tiers being compared, left to right — one column each. Same figures as `PricingPlan`, plus the `id` every feature row is keyed against.
sectionsrequired
PricingTableSection[]
—
The feature rows, grouped. Sections are what keeps a forty-row comparison readable; use a single section if the list is genuinely short.
string
'Features'
Heading over the feature column, in the top-left cell.
string
—
Text above the table — a footnote about what the prices exclude, or which currency they're in.
"horizontal" | "vertical"
'horizontal'
`horizontal` is the comparison grid, tiers across the top. `vertical` re-reads the same data as one `PricingPlan` per tier with a `CheckList` per section — the shape that survives a phone. Switch on a breakpoint rather than picking one forever.
boolean
false
Pins the tier header row to the top of the viewport while the feature rows scroll past it, which is the difference between a long comparison being readable and being a memory test. Only applies to `horizontal`.
"neutral" | "primary" | "secondary" | "success" | "error" | "warning" | "info" | "pending"
'primary'
Color of the checkmarks and of the highlighted tier's column.
boolean
false
Marks a feature a tier doesn't get with a red cross instead of a rule. Worth it on a short table where the gaps are the argument; on a long one it turns every column into a wall of red.

Slots

Slot
${tier.id}-cell
Dynamic — one slot per item, named from the expression shown. Receives `{ tier, feature, value }`.

Emits

Event
Payload
Description
select
[tier: PricingTableTier, index: number]
Fires when a tier's call-to-action button is clicked, with that tier and its index.

Types

ts
type Color = "neutral" | "primary" | "secondary" | "success" | "error" | "warning" | "info" | "pending";
ts
interface PricingTableTier {
    /**
     * Identity for this tier — the key each feature's `tiers` record is written against, and the name
     * of its `-cell` slot.
     */
    id: string;

    /**
     * The tier's name — 'Free', 'Pro', 'Enterprise'.
     */
    title?: string;

    /**
     * One line on who the tier is for, under the name.
     */
    description?: string;

    /**
     * A pill beside the name, for 'Most popular'. A bare string uses the table's own `color`.
     */
    badge?: string | PricingPlanBadge;

    /**
     * The headline figure, formatted however you want it ('$29', 'Free', 'Custom').
     */
    price?: string;

    /**
     * The price this tier is currently going for. Setting it strikes `price` out and takes its place.
     */
    discount?: string;

    /**
     * What the price buys, printed right after it — '/month', '/seat'.
     */
    billingCycle?: string;

    /**
     * When the money actually moves, under the price — 'billed annually'.
     */
    billingPeriod?: string;

    /**
     * Marks this as the recommended tier: its whole column is tinted in the table's `color`.
     * @defaultValue false
     */
    highlight?: boolean;

    /**
     * The call to action in this tier's header cell.
     */
    button?: PricingPlanButton;
}
ts
interface PricingTableFeature {
    /**
     * The feature's name, in the row header.
     */
    title: string;

    /**
     * An explanation, revealed from an `Info` icon beside the name — for a feature whose name isn't
     * self-explanatory ('Audit log retention', 'Concurrent builds').
     */
    description?: string;

    /**
     * What each tier gets, keyed by tier `id`. `true` renders a checkmark, `false` (or a missing key)
     * renders a rule, and a string or number renders as text — which is how one row says '3 seats'
     * for one tier and 'Unlimited' for the next.
     */
    tiers: Record<string, boolean | string | number>;
}
ts
interface PricingTableSection {
    /**
     * The group's heading, above its features — 'Collaboration', 'Security'.
     */
    title: string;

    /**
     * The features in this group, in the order they should be read.
     */
    features: PricingTableFeature[];
}
ts
interface CheckListItem {
    /**
     * Text label for this item.
     */
    label: string;

    /**
     * Secondary text shown below the label.
     */
    description?: string;

    /**
     * Identity used for `v-model` when the list is interactive. Falls back to the item's index.
     */
    value?: string | number;

    /**
     * Whether this item is checked. Used directly when the list isn't interactive; only used as the
     * initial state when interactive and uncontrolled.
     * @defaultValue true
     */
    checked?: boolean;

    /**
     * Disables interaction with this item. Has no effect on a non-interactive list.
     * @defaultValue false
     */
    disabled?: boolean;
}
ts
interface PricingPlanBadge {
    /**
     * The pill's text — 'Most popular', 'Save 20%'.
     */
    label: string;

    /**
     * Defaults to the plan's own `color`.
     */
    color?: Color;

    /**
     * @defaultValue 'soft'
     */
    variant?: "solid" | "soft";
}
ts
interface PricingPlanButton {
    /**
     * The button's text — 'Get started', 'Contact sales'.
     */
    label: string;

    /**
     * Route or URL to navigate to. Leave it off and handle `select` instead when signing up needs
     * to run something first (open a checkout, create a subscription).
     */
    to?: string;

    /**
     * The target attribute for the link (e.g. '_blank').
     */
    target?: string;

    /**
     * Name of the icon displayed inside the button.
     */
    icon?: string;

    /**
     * Defaults to the plan's own `color` on a highlighted plan and `neutral` on the rest — so the
     * recommended plan owns the only coloured call to action in the row.
     */
    color?: Color;

    /**
     * Defaults to `solid` on a highlighted plan and `subtle` on the rest — which is what makes the
     * recommended plan's call to action the loudest one in a row. A `disabled` button stays
     * `subtle` either way: `Button` dims a disabled control to 50%, and half a solid fill reads as
     * broken rather than as "you are already on this plan".
     */
    variant?: "solid" | "subtle" | "soft" | "ghost";

    /**
     * @defaultValue 'md'
     */
    size?: "sm" | "md" | "lg";

    /**
     * Disables the button — for the plan the viewer is already on.
     * @defaultValue false
     */
    disabled?: boolean;
}