A feature-by-plan comparison: the highlighted plan's column lit, rows revealed with a 40 ms stagger as the table scrolls into view, and a header row that sticks when you cap the table's height.
Fee Table
What a practice charges, as tables a reader can trust: one a group, each row a service, its price and a note, and where insurance applies a column that says in words whether it is covered, partly or not, with a mark that repeats it in shape. Under the tables, the insurers it works with, the ways to pay and the notes a price needs. Real tables with captions and row headers, so a screen reader reads a price with its service; three columns fit a phone without scrolling sideways. Zero JavaScript.
Live demo
What it costs
Sample prices of a made-up practice.
| Service | With insurance | Price |
|---|---|---|
| Check-up X-rays when they are due | Covered | $85 |
| Hygiene clean | Partly covered | $95 |
| Service | With insurance | Price |
|---|---|---|
| White filling | Partly covered | From $140 |
| Tooth whitening Two visits and trays to keep | Not covered | $390 |
Insurers we work with
- Northwell Health
- Brightcare
- Albion Dental Plan
Ways to pay
- Card
- Bank transfer
- Monthly plan
- A price from is the least it costs: you have the full price in writing before any treatment.
Live, from the library itself, at full size: scroll, hover, navigate away and back. The demo is never gated.
Variants · each row writes the attribute the prop writes; the section's own CSS answers
Measured
JavaScript of its own
0 B
Uses Reveal and the shared runtime (1.9 kB raw, once per site). With those included: 3.0 kB raw.
CSS 10.5 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.
Browser support
every browser. Elsewhere: nothing to fall back from.
Install Pro
The demo above is never gated; the source is. With a key in components.json (see account), the registry serves it like any other item:
npx shadcn@latest add @moonarc-pro/fee-table---
import FeeTable from '@/components/moonarc/pro/FeeTable.astro';
---
<FeeTable
headline="What it costs"
groups={[{ name: 'Check-ups', rows: [{ service: 'Check-up', price: '$85', covered: 'yes' }, { service: 'Hygiene clean', price: '$95', note: '45 minutes', covered: 'part' }] }]}
insurers={{ label: 'Insurers we work with', names: ['Insurer A', 'Insurer B'] }}
payment={{ label: 'Ways to pay', methods: ['Card', 'Bank transfer'] }}
notes={['A price from is the least it costs: the dentist gives you the full price before any treatment.']}
/>Without a key the registry answers 401 with an upgrade message. Everything this section composes is free: reveal.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
headline | string | none | The heading. Slot "headline" wins and may hold an <em>. |
eyebrow | string | none | A short line above the heading. |
copy | string | none | A line under the heading. |
groups | { name; rows: { service; price; note?; covered?: 'yes' | 'part' | 'no' }[] }[] | none | One table a group, its name the caption. The insurance column shows when any row sets covered. |
words | { service?; price?; covered?; yes?; part?; no? } | none | The column heads and the three answers of the insurance column. |
insurers | { label; names: string[] } | none | The insurers it works with, as chips. |
payment | { label; methods: string[] } | none | The ways to pay, as chips. |
notes | string[] | none | The notes under the tables. |
level | 1 | 2 | 3 | 2 | Heading level of the heading; the side's labels are one below. |
tone | 'surface' | 'muted' | 'ink' | 'brand' | unset (paints nothing) | The section's ground. |
density | 'compact' | 'cozy' | 'roomy' | 'cozy' | Block padding ×0.8 / ×1 / ×1.25 through --ma-density. |
Theming and overrides
The section reads the theme contract (shadcn's variable names with the base tokens as fallback), so a pasted theme or one --ma-brand recolours it. Its class names are the override surface and stay stable; a named slot replaces the matching prop with your own markup.
Classes
.ma-fees.ma-fees__caption.ma-fees__chips.ma-fees__copy.ma-fees__cover.ma-fees__eyebrow.ma-fees__head.ma-fees__headline.ma-fees__inner.ma-fees__label.ma-fees__note.ma-fees__notes.ma-fees__num.ma-fees__price.ma-fees__service.ma-fees__side.ma-fees__table.ma-fees__tables
Slots
slot="headline"
Reduced motion
The tables arrive at once.
With ClientRouter
CSS and Reveal.
Why it is built this way
- Each service is a row header (th scope="row"), so a screen reader announces the service with its price and its cover.
- Cover is a word first; the mark before it, full, half or empty, repeats it for a reader scanning down the column, never colour alone.
- The price column never wraps and the service column does, which is how three columns fit 320 px.
Replaces: Price list tables. See the migration table.
Related
A service's own page, top to bottom: its name and what it is in a sentence, then the parts a reader looks for before booking (what it is, who it is for, what happens step by step, what to bring, what comes after), each under its own heading; beside them a card that stays in view while the page scrolls (position: sticky, nothing animated) with the facts (how long, from what price, whether insurance covers it), the two actions and a line of small print.
What a practice does, by group: each group's name and a line about it on one side, its services on the other as rows, each with a line of what it is, how long it takes and what it costs from, and a link to its own page.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown