One comparison table.
Three jobs.
Pricing tiers, a full feature matrix, or a head-to-head against the alternatives — all from a single module. It renders as a real table on desktop and becomes one accordion per column on phones, so nobody has to scroll a grid sideways to find out what they get.
Prices in USD, excluding tax.
| Feature | Starter $0 forever One workspace, one seat Create account | Recommended Team $24 $19 /user/mo Billed monthly Billed yearly — 2 months free Start 14-day trial | Enterprise Custom Annual contract Talk to sales |
|---|---|---|---|
| Starter $0 | Team $24 $19 | Enterprise Custom | |
| Core | |||
| Connected accounts Accounts that sync on a schedule. Read-only connections. | 2 | 25 | Unlimited |
| History retained How far back queries and charts can reach. | 90 days | 3 years | 7 years |
| Scheduled reports | — | ||
| Collaboration | |||
| Seats included Named user accounts. Extra seats bill at the same rate. | 1 | 5 | Custom |
| Shared workspaces | 1 | 3 | Unlimited |
| Comments & mentions | — | ||
| Security & support | |||
| SSO (SAML / OIDC) | — | — | |
| Audit log An append-only record of every read, write, and export. | — | 1 year | Custom |
| Support | Community | Email, 2 business days | Named CSM |
| No features match your search. | |||
| Get started | Create account | Start 14-day trial | Talk to sales |
Scroll inside the pane — the header and feature column stay pinned.
Starter $0 forever
One workspace, one seat
Core
Collaboration
Security & support
No features match your search.
Recommended Team $24 $19 /user/mo
Billed monthly Billed yearly — 2 months free
Core
Collaboration
Security & support
No features match your search.
Enterprise Custom
Annual contract
Core
Collaboration
Security & support
No features match your search.
Built for the table you actually have to ship
Three tables in one
Pricing tiers with a monthly/annual switch and a highlighted plan. A long feature matrix with the header and feature column pinned in place. A category comparison written in full sentences. Same module, same fields, three different jobs — so you learn it once.
Phones get a real answer
Below a width you choose, every column becomes its own accordion: one plan, read top to bottom, ending in its own button. No pinch-zoom, no sideways scroll, no six columns crushed into an unreadable strip.
Nothing is hardcoded
Every icon can be replaced — the checkmark, the dash, the info circle, the chevron. Every visible string is a field, right down to the ones only screen readers hear. A brand that uses a different tick doesn't need a developer.
Readable by everyone
A real table with proper header scopes and a described caption, so a screen reader announces "Team, Scheduled reports, Included" instead of reading a wall of disconnected values. Checks and dashes carry text alternatives. The mobile accordions are native elements that work with a keyboard, and the motion respects reduced-motion preferences.
Fast because it does less
Everything renders on the server, so the finished table paints immediately and nothing shifts around as the page loads. No icon font, no CDN script, no web font — the module arrives with everything it needs and borrows nothing from the theme around it.
Find the row, skip the scroll
Optional search filters rows by their label, their tooltip, or any value inside them — and category headings tidy themselves away as they empty out. The match count is announced to screen readers, not just shown. Off by default, because a six-row pricing table doesn't need it.
A Little Bit About How it Works
Need This Module Customized for Your Brand or Function?
(or any other module for that matter?)
Colour, layout, spacing, icons, and every word on screen are already settings - you don't need us for those. Where teams usually do need us is past that line: more than six columns, a third billing period, rows built from HubDB or CRM data instead of typed by hand, or pricing pulled live from the system that actually owns it. A Design Link builds HubSpot modules for a living, and this one is ours — so extending it means changing code we already know.
Purchase a development package and get:
- Expert HubSpot CMS customization
- Brand-perfect styling and design tweaks
- Custom feature development
- Performance optimization
- Ongoing support and maintenance
Perfect for module customization, theme development, and HubSpot CMS consulting.
Documentation
Comparison Table
One table component that covers three jobs: pricing tiers, a long feature matrix, and a category comparison. You author a single set of columns and rows; the module renders a semantic table on desktop and a set of per-column accordions on phones.
Every icon, label, and style value is a field. Nothing visible is hardcoded, so a checkmark can become a different glyph and "Get started" can become "Request a demo" without touching code.
- Categories: Functionality, Design
- Content types: Landing pages, site pages, blog posts, blog listing
- Columns: 1–6
- Rows: unlimited, with optional category headings
Quick Links
- Why this module exists
- How it works
- Building the table: columns and rows
- The billing toggle
- Highlighting a column
- Sticky behavior
- Mobile accordions
- Searching rows
- Changing icons and text
- Fields reference
- Styles reference
- Dark mode
- Accessibility
- Performance notes
- Translations
- Troubleshooting
- Known limitations
Why this module exists
Comparison tables usually fail in one of two ways. Either they are built as a rigid pricing table that cannot be reused for anything else, or they are built as free-form rich text, which looks acceptable on a desktop and collapses into an unreadable mess on a phone.
This module treats the comparison as data — a list of columns and a list of rows — and renders it twice:
- Desktop: a real
<table>with proper header cells, so screen readers announce "Team, Scheduled reports, Included" instead of reading a wall of disconnected text. - Phones: one accordion per column, so a visitor reads a single plan top to bottom instead of scrolling a table sideways.
Both views come from the same fields, so they cannot drift apart.
How it works
module.html -> Renders BOTH views from the same columns/rows fields,
and writes every style setting as a CSS variable on
the module wrapper.
module.css -> Static, namespaced (blk_ctable). Decides which view is
visible at the chosen breakpoint.
module.js -> Billing toggle, tooltips, accordion open/close slide.
It never builds markup.
The wrapper carries the module's configuration as data attributes:
| Attribute | Purpose |
|---|---|
data-rec |
Which column is highlighted (1–6). Absent when none is. |
data-billing |
monthly or annual — which price is showing right now. |
data-sticky |
none, page, or pane. |
data-stack |
Width at which the accordions take over: 768, 880, or 1024. |
data-accordion |
single or multi. |
data-scheme |
light or auto (follow the visitor's device). |
data-frosted |
true or false — blur behind sticky headers. |
Several tables can sit on one page without interfering: the billing radio group, the wrapper id, and every tooltip id are all scoped to the module instance.
Building the table: columns and rows
The table is a grid. You define it in two lists.
Columns are your plans, products, or approaches — one entry each, up to six. A column holds its name, an optional badge, a price for each billing period, a line of subtext, and a button.
Rows are the things being compared. Each row is one of two types:
- Feature row — a label plus one cell value per column.
- Category heading — a full-width band that groups the rows beneath it (e.g. "Security & support"). It needs no cell values.
The one rule to remember: inside a feature row, the cell values are matched to columns by position. The first cell fills the first column, the second fills the second, and so on. If you add a column later, add a cell to every feature row too — otherwise the new column shows blanks.
Each cell is one of three types:
| Cell type | Renders | Use for |
|---|---|---|
| Text | Whatever you type | Limits, quantities, tiers — "25", "Unlimited", "3 years" |
| Included | A check mark | The feature is present |
| Not included | A dash | The feature is absent |
Text cells also take an Emphasis setting. Normal is the default. Muted greys the value down for supporting detail like "Email, 2 business days". Prose is for full sentences — it drops the font size, relaxes the line height, and on mobile stacks the value under its label instead of squeezing it to the right. Use Prose for a category comparison; leave it off for pricing.
The billing toggle
Turn on Show Billing Toggle and each column can carry two prices — a primary (normally monthly) and a secondary (normally annual). The visitor switches between them and every column updates at once, along with the subtext line and the condensed sticky row.
Notes worth knowing:
- If a column's two prices are identical, the module renders a single price with no switching markup at all. Set both to the same value for plans that don't vary — "Free" and "Custom" tiers, typically.
- The option labels are yours: "Monthly" and "Annual" are only defaults. "Pay monthly" / "Pay yearly" works just as well.
- The Savings Note sits beside the second option ("Save 20%"). Clear it to hide it.
- With JavaScript unavailable the toggle is inert and the default period stays showing. No price is ever hidden with no way to reach it.
Highlighting a column
Switch on Highlight This Column on the plan you want to push. It gets a tinted background, an accent rule across the top, and on mobile it is the accordion that starts open and carries the accent border.
If you switch it on for more than one column, the first one wins — the module will not paint two highlights.
The badge ("Recommended", "Most popular") is a separate field. Highlighting and badging are independent on purpose: you can badge a column without tinting it, or tint without badging.
Sticky behavior
| Setting | What happens | Best for |
|---|---|---|
| None | The table scrolls with the page. | Short tables that fit on one screen. |
| Condensed row pins to the page | As you scroll past the header, a slim frosted bar keeps the plan names and prices visible. | Pricing tables. Pinning the full header would eat a third of the screen. |
| Table becomes a scroll pane | The table turns into its own scroll island. The header pins to the top of the pane and the feature column pins to the left, so you keep your bearings in both directions. | Long feature matrices — the page you link to from the pricing page. |
In scroll-pane mode the table is given a minimum width so columns never crush together; below that width the pane scrolls sideways. The Scroll Pane Hint line appears under the table to tell visitors that is what is happening — clear the field to hide it.
Mobile accordions
Below the width you choose in Switch To Accordions At, the table is replaced by one accordion per column. Each opens to show every row as a label-and-value pair, grouped under the same category headings, ending with that column's button.
- Accordion Behavior — One open at a time keeps the page short; Several can stay open lets visitors compare two plans by scrolling.
- Open One Accordion By Default opens the highlighted column, or the first column if none is highlighted. Turn it off to start fully collapsed.
- Any tooltip becomes a hint line under its row. A tooltip is a hover affordance, and there is no hover on a phone.
The accordions are built into the page HTML, not assembled by JavaScript, so they are present immediately and the page does not shift as it loads. They are real <details> elements, so they open and close with the keyboard and work with JavaScript disabled.
Searching rows
Switch on Show Row Search and a search box appears in the toolbar. Typing filters the feature rows down to the ones that match.
The search looks at three things per row: the row label, its tooltip text, and every cell value in that row. So "audit" finds the Audit log row by its name, and "unlimited" finds every row where any plan says Unlimited.
Behaviour worth knowing:
- Category headings disappear with their contents. Filter down to nothing under "Security & support" and that heading hides too, rather than floating above an empty space.
- Both views filter together. The desktop table and the mobile accordions are kept in step, so a visitor who rotates their phone sees the same filtered set.
- Nothing is destroyed. Rows are hidden, not removed. Clearing the box restores everything.
- Screen readers get a running count ("7 results") from a polite live region, so filtering is not a silent visual-only change. The word after the number is the Search Results Word field.
- No matches shows the No Results Message in place of the rows.
It is off by default, and that is deliberate. On an eight-row pricing table a search box is clutter — a visitor can read the whole thing faster than they can type. Turn it on for the long feature matrix, where it saves real scrolling.
With JavaScript unavailable the box does nothing and every row stays visible. Nothing is hidden behind a control that cannot work.
Changing icons and text
Icons. The module draws its own check mark, dash, info circle, and chevron. Each has a matching field under Icons. Leave a field empty and the built-in icon is used; upload an image and it replaces that icon everywhere it appears, in both the table and the accordions.
The built-in check inherits the surrounding text colour, so it follows your palette without a second setting. An uploaded image does not — it renders exactly as supplied. Size all four with Icon Size under Table & Card.
Text. Every visible string lives under Labels & Text, including several that only screen readers hear:
- Table Caption — describes the whole table to a screen reader before it starts reading cells.
- "Included" / "Not Included" Label — what a screen reader says in place of the check or dash.
- Tooltip Button Label — prefixed to the row label, so the button announces as "More information about Audit log".
- Feature Column Heading — always available to screen readers; Show Feature Column Heading controls whether sighted visitors see it. Pricing tables normally hide it; feature matrices normally show it.
Translating these for a Spanish page is a matter of typing Spanish into the fields on that page's Spanish variant — see Translations.
Fields reference
Columns (repeating, 1–6)
| Field | Default | Notes |
|---|---|---|
| Column Name | Plan | Required. |
| Badge Text | empty | Pill above the name. Empty hides it. |
| Highlight This Column | off | First one switched on wins. |
| Show Price | on | Turn off for non-price comparisons. |
| Price Prefix | $ | Currency symbol. Empty for word prices. |
| Price — Primary / Secondary Period | 0 / 0 | Identical values render as one price. |
| Price Suffix | empty | "/user/mo", "forever". |
| Price Style | Number | Word shrinks it for "Custom". |
| Subtext — Primary / Secondary Period | empty | Small line under the price. |
| Show Button / Button Text / Button Link / Button Style | on / Get started / empty / Outline | One button per column. |
Rows (repeating, unlimited)
| Field | Default | Notes |
|---|---|---|
| Row Type | Feature row | Category heading spans the table. |
| Row Label | Feature | Required. |
| Tooltip | empty | Adds an info button. Becomes a hint line on mobile. |
| Cell Values | 3 empty text cells | One per column, in order. |
Settings
| Field | Default |
|---|---|
| Show Billing Toggle | on |
| Default Billing Period | First option (Monthly) |
| Show Toolbar Note | on |
| Sticky Behavior | Condensed row pins to the page |
| Show Tooltips | on |
| Show Row Search | off |
| Repeat Buttons Below Table | off |
| Switch To Accordions At | 880px |
| Accordion Behavior | One open at a time |
| Open One Accordion By Default | on |
Styles reference
The Styles tab is grouped by what each setting paints:
| Group | Covers |
|---|---|
| Colors | Surfaces, text, borders, accent, and the highlighted-column tint. |
| Dark Palette | The same set again, used only when Color Scheme follows the device. |
| Typography | Cell, column-name, price, subtext, and category-heading sizes; heading weight and letter spacing. |
| Spacing & Padding | Row density, cell padding, header padding, toolbar margin, gap between accordions. |
| Toggle Control | The billing switch: radii, padding, font size. |
| Table & Card | Corner radius, column widths, scroll-pane height and minimum width, border widths, frosted chrome, icon size. |
| Button Styles | Button and badge radii, padding, font size and weight. |
| Hint & Tooltip | Scroll hint and tooltip colours, sizes, width, and radius. |
Two settings are worth calling out. Page Background Color is not the module's own background — it tints the frosted sticky bar, so set it to match whatever the module is sitting on, or the bar will look wrong as content passes under it. Row Vertical Padding is the density control: 17px is comfortable, 10px is compact and is the right choice for a matrix with twenty rows.
Corner radii of 980 produce a full pill. Lower the number for squarer corners.
Dark mode
Color Scheme has two settings. Always use the light palette renders the Colors group in every condition. Follow the visitor's system setting swaps to the Dark Palette group when the visitor's device is in dark mode.
Auto mode is only worth switching on if the surrounding page also adapts. A dark module on a permanently white page looks like a mistake.
Accessibility
- A real
<table>withscopeon every header cell, so screen readers can associate any value with both its row and its column. - A visually hidden
<caption>describing the table, editable as Table Caption. - Check marks and dashes are exposed as images with text alternatives, so a screen reader says "Included" rather than skipping a decorative glyph.
- The billing switch is a proper
<fieldset>of radio buttons with a legend — operable by keyboard, announced as a group. - Tooltips are real
role="tooltip"elements referenced byaria-describedby, reachable by keyboard focus rather than hover alone, and rendered in the browser's top layer so they escape the scroll pane's clipping. - The mobile accordions are
<details>/<summary>, which are keyboard- and screen-reader-native. - The open/close slide respects
prefers-reduced-motion. - Category headings use
scope="colgroup", so they are announced as the grouping they are.
Performance notes
- Everything is rendered on the server. The browser paints the finished table immediately, which helps LCP.
- Because both views ship in the HTML and CSS chooses between them, nothing is assembled after load and the page does not jump — no CLS from this module.
- The JavaScript is small and does no layout work. It attaches three sets of listeners and stops.
- There are no external dependencies — no icon font, no CDN script, no web font. The module uses the visitor's system font stack and draws its own icons.
- The trade-off: on a wide table both views are present in the DOM. For a six-column, forty-row table this is worth checking against a very long page; for ordinary use it is a good deal.
Translations
The module ships with English and Spanish translations of its editor field labels. A colleague whose HubSpot account language is Spanish sees "Nombre de la columna" where an English user sees "Column Name". This affects the editing interface only.
Visitor-facing text is separate. HubSpot translates page content through multi-language page variants: you create a Spanish variant of the page and type Spanish into the module's fields there. Because every visible string in this module is a field — including the screen-reader-only ones — nothing needs a code change to ship in another language.
Troubleshooting
| Symptom | Cause |
|---|---|
| A column shows blanks all the way down | The feature rows have fewer cell values than there are columns. Add a cell to each row. |
| Values are in the wrong columns | Cells match columns by position. Reordering columns does not reorder cells — reorder the cells to match. |
| The frosted bar looks like a grey smear | Page Background Color does not match the actual page background. Set it to match, or turn off Frosted Sticky Chrome. |
| The billing toggle does nothing | Both prices on every column are identical, so there is nothing to switch. Check the secondary prices. |
| Nothing renders on the page | The module needs at least one column and one row. In the editor it shows a setup message instead; on a live page it renders nothing rather than an empty shell. |
| Tooltips do not appear on hover | Expected on touch devices. The text appears as a hint line under the row in the accordion view. |
Known limitations
- Six columns maximum. Beyond that the columns are too narrow to read, and the comparison stops being useful.
- Cells are matched to columns by position, not by name. This is the main thing to be careful about when editing an existing table.
- The accordion breakpoint is one of three fixed widths (768, 880, 1024). A CSS media query cannot read a custom setting, so arbitrary values are not possible.
- The billing toggle supports two periods, not three.
- Scroll-pane mode needs a width. Inside a narrow column layout, the condensed-row or none setting works better.