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.
<UiPricingTable :tiers="..." :sections="..." />Options
One block per prop — what it does, and what it looks like set.
Tiers
requiredtiers 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 | 1 | Unlimited | Unlimited |
| Team members | 1 | 10 | Unlimited |
| Build minutes | 500 | 10,000 | Unlimited |
| Security | |||
| Two-factor authentication | Included | Included | Included |
| SSO (SAML) | Not included | Not included | Included |
| Audit log | Not included | Included | Included |
| Custom data residency | Not included | Not included | Included |
| Support | |||
| Community forum | Included | Included | Included |
| Email support | Not included | Next business day | 1 hour |
| Named engineer | Not included | Not included | Included |
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
requiredsections 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 | 1 | Unlimited |
| Team members | 1 | 10 |
| Build minutes | 500 | 10,000 |
| Security | ||
| Two-factor authentication | Included | Included |
| SSO (SAML) | Not included | Not included |
| Audit log | Not included | Included |
| Custom data residency | Not included | Not included |
| Support | ||
| Community forum | Included | Included |
| Email support | Not included | Next business day |
| Named engineer | Not included | Not 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 | 1 | Unlimited |
| Team members | 1 | 10 |
| Build minutes | 500 | 10,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 | 1 | Unlimited |
| Team members | 1 | 10 |
| Build minutes | 500 | 10,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.
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 popularFor small teams.
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 | 1 | Unlimited | Unlimited |
| Team members | 1 | 10 | Unlimited |
| Build minutes | 500 | 10,000 | Unlimited |
| Security | |||
| Two-factor authentication | Included | Included | Included |
| SSO (SAML) | Not included | Not included | Included |
| Audit log | Not included | Included | Included |
| Custom data residency | Not included | Not included | Included |
| Support | |||
| Community forum | Included | Included | Included |
| Email support | Not included | Next business day | 1 hour |
| Named engineer | Not included | Not included | Included |
| Deployment | |||
| Custom domains | Not included | 10 | Unlimited |
| Preview deployments | Included | Included | Included |
| Concurrent builds | 1 | 5 | 20 |
| Rollbacks | Not included | Included | Included |
| 99.9% uptime SLA | Not included | Not included | Included |
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 | 1 | Unlimited |
| Team members | 1 | 10 |
| Build minutes | 500 | 10,000 |
Free For trying things out. $0/month | ProMost popular For small teams. $29/month billed annually | |
|---|---|---|
| Usage | ||
| Projects | 1 | Unlimited |
| Team members | 1 | 10 |
| Build minutes | 500 | 10,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 | Included | Included |
| SSO (SAML) | Not included | Not included |
| Audit log | Not included | Included |
| Custom data residency | Not included | Not included |
Free For trying things out. $0/month | ProMost popular For small teams. $29/month billed annually | |
|---|---|---|
| Security | ||
| Two-factor authentication | Included | Included |
| SSO (SAML) | Not included | Not included |
| Audit log | Not included | Included |
| Custom data residency | Not included | Not 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.
Usage
- Projects1
- Team members1
- Build minutes500
Pro
Most popularFor small teams.
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 | true | Included |
| Email support | Coming soon | Next 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
tiersrequiredPricingTableTier[]sectionsrequiredPricingTableSection[]string"horizontal" | "vertical"'horizontal'booleanfalse"neutral" | "primary" | "secondary" | "success" | "error" | "warning" | "info" | "pending"'primary'booleanfalseSlots
${tier.id}-cellEmits
select[tier: PricingTableTier, index: number]Types
type Color = "neutral" | "primary" | "secondary" | "success" | "error" | "warning" | "info" | "pending";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;
}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>;
}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[];
}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;
}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";
}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;
}