Detent web components
By Shane Logsdon

Data & tables

Comparison Table

Implemented

Interactive tier comparison matrix with difference filtering

detent-comparison-table

Port status
Catalog status
implemented
Origin
upstream
Phase
3
Wave
phase-3-data
Group
Data & tables

6.4 KB gzip for the class module

Variants

Three plans with the keyword search, the Differences only filter, collapsible categories, per-feature info tooltips and readable Included / Not included cells. Below 40rem the columns collapse into a segmented plan switcher instead of a scrolling table. Prices and entitlements are static demo data: the CTA is a plain button and no checkout, payment or network call happens.

Ready.

Recorded states

  • collapsed recorded state; no setter

Usage

Import

import '/components/comparison-table/define.js';

// or register it yourself
import { DetentComparisonTable } from '/components/comparison-table/comparison-table.js';

Attributes

  • default-plan-id
  • enable-diff-filter
  • enable-search

Properties

  • categories
  • plans

Slots

No slots are recorded for this component.

Events

No events are recorded for this component.

Example

Runnable example: site/examples/comparison-table.ts. The snippet below is generated from the catalogued attributes.

<detent-comparison-table
  default-plan-id
  enable-diff-filter
  enable-search
></detent-comparison-table>

Property-only values

const element = document.querySelector('detent-comparison-table');
if (element !== null) {
  // plans: ComparisonPlan[]
  element.plans = [];
  // categories: ComparisonCategory[]
  element.categories = [];
}

Source

The pinned upstream source for this row:

  • src/components/ui/SmartComparison.tsx sha256 9783ffc73a23…

Native files

The native module and its artifacts:

  • src/components/comparison-table/comparison-table.ts
  • src/components/comparison-table/styles.css
  • src/components/comparison-table/define.ts
  • docs/components/comparison-table.md
  • site/examples/comparison-table.ts
  • tests/browser/comparison-table.spec.ts
  • evidence/components/comparison-table.md

API

Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.

Attributes

Attributes
AttributePropertyTypeDefaultNotes
default-plan-iddefaultPlanIdstringfeatured plan or plans[0].idKebab-case attribute; camelCase property.
enable-diff-filterenableDiffFilterbooleantruePresence semantics: present means true, absent means false.
enable-searchenableSearchbooleantruePresence semantics: present means true, absent means false.

Properties

Properties
PropertyTypeDefaultNotes
categoriesComparisonCategory[][]Property-only: assign from script; not accepted as an attribute.
plansComparisonPlan[][]Property-only: assign from script; not accepted as an attribute.

Events

No custom events are recorded for this component.

Slots

No slots are recorded for this component.

CSS customisation

  • `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
  • Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
  • `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
  • React state/hooks replaced by element-owned state with pre-upgrade property preservation.
  • A React.ReactNode cell value cannot cross into a custom element, so cell values are string | number | boolean | null; rich cell content would need a per-cell slot, which is not this row's recorded API (R-1.2, PC-17). Custom rendering is a CSS parts + data-* concern instead.
  • plan.onCtaClick is not ported as a property: it is a function inside plain data (PC-3.1, PC-6.3). The CTA is a native <button part="cta"> whose composed click the consumer can delegate; no custom event is added, keeping the row's events: [].
  • Boolean cells show words instead of a ✓/✗ glyph pair, and every plan row is prefixed with the plan name on small screens: recorded accessibility corrections (PC-12.6).
  • The info tooltip opens on click/keyboard and closes on Escape; upstream is a hover-only popover (which is unusable on touch). The description is also exposed via aria-describedby, so it is reachable without opening the popover.
  • Framer Motion's springGentle height animation and the layoutId mobile pill become a CSS keyframe on the panel and CSS transitions on the switcher; the animated properties are colours and a short fade, not values the shared spring helper can drive. Under reduced motion the animation is removed (PC-10.5).
  • The matrix is not dark-only: upstream's hard-coded #0E0E0E/#FAFAFA palette becomes --detent-color-* tokens (PC-9.5).
  • className is replaced by the documented parts and custom properties; there is no Tailwind in consumers.

What it does

A tier comparison matrix: plan columns with price and CTA, a keyword search, a "Differences only" toggle, collapsible category accordions of feature rows, per-feature info tooltips, and a segmented plan switcher for small screens instead of a horizontally scrolling table.

Upstream behaviour
  • Live "Differences Only" filter to instantly surface plan divergence
  • Integrated feature keyword search with real-time row matching
  • Collapsible category groups with spring height transitions
  • Segmented mobile plan selector avoiding wide horizontal table scrolling
  • Contextual info tooltips and custom value cell rendering
Differences from upstream
  • `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
  • Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
  • `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
  • React state/hooks replaced by element-owned state with pre-upgrade property preservation.
  • A React.ReactNode cell value cannot cross into a custom element, so cell values are string | number | boolean | null; rich cell content would need a per-cell slot, which is not this row's recorded API (R-1.2, PC-17). Custom rendering is a CSS parts + data-* concern instead.
  • plan.onCtaClick is not ported as a property: it is a function inside plain data (PC-3.1, PC-6.3). The CTA is a native <button part="cta"> whose composed click the consumer can delegate; no custom event is added, keeping the row's events: [].
  • Boolean cells show words instead of a ✓/✗ glyph pair, and every plan row is prefixed with the plan name on small screens: recorded accessibility corrections (PC-12.6).
  • The info tooltip opens on click/keyboard and closes on Escape; upstream is a hover-only popover (which is unusable on touch). The description is also exposed via aria-describedby, so it is reachable without opening the popover.
  • Framer Motion's springGentle height animation and the layoutId mobile pill become a CSS keyframe on the panel and CSS transitions on the switcher; the animated properties are colours and a short fade, not values the shared spring helper can drive. Under reduced motion the animation is removed (PC-10.5).
  • The matrix is not dark-only: upstream's hard-coded #0E0E0E/#FAFAFA palette becomes --detent-color-* tokens (PC-9.5).
  • className is replaced by the documented parts and custom properties; there is no Tailwind in consumers.
Previous: Compact Action Dock Next: Animated Accordion

Docs

Components

Project