← All playbooks · Raw API
plan-picker
# Plan picker — point.me
A **Plan picker** external is a pricing chooser. The reader picks one plan. Each plan can offer more than one way to pay. The selected plan shows that billing option’s benefits and signup button.
Create a new external for each picker. Put it on any marketing page or article. The component owns the layout. Every visible string is HTML in `data`.
Do not rebuild this with **Pricing plans**, **Sticky plan selector**, or custom HTML.
## Create or edit
1. Open the page or article: `cms-edit open <slug>`.
2. Create one: `cms-edit add "Plan picker" --content-type externalComponent --label "<internal name>"`. To change a picker already on that page, use its ref from the snapshot instead of adding another.
3. Set the entry fields, then replace `data` with the JSON below.
- Under 8k: `cms-edit set <ref> data --json '<object>'`
- Over 8k: staged upload `kind=json`, then `cms-edit set <ref> data --staged <id>`
4. `diff` → `save`.
5. `cms-edit preview urls <slug>` and open the staging URL. Production shows published content only. If the JSON is invalid, the page says “This plan picker could not be loaded.”
6. Publish only when asked. Use `cms-edit://customer/task-publish-handoff`.
The same external can be linked onto another page with `cms-edit add "Plan picker" --content-type externalComponent --existing-id <id>`.
## Entry fields
| Field | What it does |
|-------|----------------|
| `externalComponentType` | `Plan picker` |
| `cmsLabel` | Name in the Contentful list. Readers never see it. |
| `backgroundColour` | Band behind the picker. Any external background colour, such as White, Pale Blue, Pale Purple, or Pale Teal. The plan cards stay white. |
| `textColour` | Section text colour. Plan names, prices, and buttons use the component’s own colours. |
| `anchor` | Optional id for an in-page link, such as `plans`. |
| `heading` | Leave empty. The visible title is `data.titleHtml`. A heading here would sit above that title. |
| `data` | The configuration object. Replace the whole object. |
## What the reader sees
On a phone, the plans stack. The selected plan’s benefit list and button sit under that card. On a laptop, the cards stay in a column and the selected plan’s detail sits beside them.
The first view uses `defaultPlanId` and that plan’s `defaultBillingId`. Choosing another plan does not reset the billing the reader already picked on a plan. A billing switch changes only that plan’s price, benefits, and button.
## Configuration
```json
{
"titleHtml": "Choose a plan",
"subtitleHtml": "Optional line under the title",
"footerHtml": "Optional note under the whole picker",
"defaultPlanId": "plan-a",
"plans": [
{
"id": "plan-a",
"nameHtml": "Plan name",
"badgeHtml": "OPTIONAL BADGE",
"accent": "purple",
"defaultBillingId": "annual",
"billing": [
{
"id": "annual",
"labelHtml": "Annual billing",
"compareHtml": "$20/mo",
"saveBadgeHtml": "SAVE 40%",
"priceHtml": "$12",
"unitHtml": "/month",
"noteHtml": "Billed annually",
"valueHtml": "Optional value line",
"switchLabelHtml": "switch to monthly",
"switchToId": "monthly",
"detailNameHtml": "Plan name",
"pillHtml": "Optional note beside the detail heading",
"pillIcon": "plane",
"benefits": [
{
"state": "included",
"labelHtml": "Included benefit <span>smaller gray aside</span>",
"detailHtml": "Shown when the reader opens the row."
}
],
"cta": {
"labelHtml": "Get started",
"href": "https://www.point.me/signup",
"tone": "purple",
"variant": "solid"
}
}
]
}
]
}
```
| Key | What it does |
|-----|----------------|
| `titleHtml` | Centered title. |
| `subtitleHtml` | Optional line under the title. |
| `footerHtml` | Optional note under the cards and the detail. |
| `defaultPlanId` | Plan selected on first load. Must match a plan `id`. |
| `plans` | One or more cards, in the order they appear. |
| `plans[].id` | Stable id. Unique among plans. Not shown. |
| `plans[].nameHtml` | Card name. |
| `plans[].badgeHtml` | Optional purple pill above the name. Omit to hide it. |
| `plans[].accent` | `purple`, `green`, or `gray`. Colours the selected card border, the radio, the price, and included check icons. |
| `plans[].defaultBillingId` | Billing option shown before the reader switches. Must match a billing `id` on that plan. |
| `plans[].billing` | One or more ways to pay. Benefits and the button belong here, because two billing options can list different rows and different signup URLs. |
| `billing[].id` | Stable id. Unique within that plan. Not shown. |
| `billing[].labelHtml` | Small line under the plan name, such as “Annual billing”. |
| `billing[].compareHtml` | Struck-through comparison price. The component draws the line. Do not wrap it in `<s>`. |
| `billing[].saveBadgeHtml` | Teal chip beside the comparison price. |
| `billing[].priceHtml` | Price as HTML, such as `$12` or `Free`. This is not a Stripe id. |
| `billing[].unitHtml` | Text after the price, such as `/month`. |
| `billing[].noteHtml` | Line under the price, such as how often it is billed. |
| `billing[].valueHtml` | Purple chip under the price. |
| `billing[].switchLabelHtml` | Text of the billing switch. Shown only together with `switchToId`. |
| `billing[].switchToId` | Another billing `id` on the **same** plan. The switch changes billing only. It does not select the plan. |
| `billing[].detailNameHtml` | Heading of the detail panel. Falls back to the plan name. |
| `billing[].pillHtml` | Note on the detail panel. Omit to hide the pill. |
| `billing[].pillIcon` | `plane` adds ✈️ before the pill. `none` or omitting it shows the words only. |
| `billing[].benefits` | Rows in the detail panel. |
| `billing[].cta` | The signup button for this billing option. |
Benefit rows:
| Key | What it does |
|-----|----------------|
| `state` | `included` or `locked`. Locked rows are gray. |
| `icon` | `check`, `cross`, or `none`. Omit it for a check on included rows and a cross on locked rows. |
| `labelHtml` | The row label. A `span` inside it is the smaller gray aside. |
| `detailHtml` | Extra copy. The row can open only when `state` is `included` and this is set. |
| `emphasize` | `true` makes an included label bold. |
Button:
| Key | What it does |
|-----|----------------|
| `cta.labelHtml` | Button label. |
| `cta.href` | `http://`, `https://`, or a site path that starts with `/`. A hash-only href is rejected, and so is `javascript:`. |
| `cta.tone` | `purple` or `green`. |
| `cta.variant` | `solid` fills the button. `outline` leaves it hollow. |
## HTML in any string
Allowed tags: `strong`, `em`, `b`, `br`, `sup`, `sub`, `span`, `a`, and a simple inline `svg` with `path` or `circle`.
Scripts, event handlers, `style`, and a `<` that is not one of those tags are removed. An inline `svg` in a benefit `labelHtml` sits on the same line as the words. Set that row’s `icon` to `none` when the SVG is the icon.
## Rules that make the picker fail to load
- Plan ids must be unique. Billing ids must be unique within that plan.
- `defaultPlanId`, `defaultBillingId`, and `switchToId` must point at ids that exist.
- Every billing option needs `priceHtml`, `benefits`, and `cta`.
- A CTA href must be http(s) or a path starting with `/`.
## Out of scope
- Changing the card layout, the accent colours, or the airplane emoji in code
- **Pricing plans** or **Sticky plan selector**