# Comparison Table

A feature-by-plan comparison matrix with checkmarks.

- Category: Structured Outputs
- Source: AICSS (https://www.aicss.dev/components/comparison-table)
- Author: @kvnkld (https://x.com/kvnkld)
- Styling: Self-contained CSS with CSS custom properties (design tokens). Theme-aware via [data-theme] (light/dark).

## Instructions

Add this component to the project. Keep the styling self-contained. Map the design tokens (CSS custom properties) to the project's theme if they are not already defined.

## Code

### React - `ComparisonTable.tsx`

```tsx
import styles from "./ComparisonTable.module.css";

type Feature = { label: string; values: boolean[] };

const PLANS = ["Personal", "Enterprise"];
const FEATURES: Feature[] = [
  { label: "Unlimited projects", values: [true, true] },
  { label: "All components", values: [true, true] },
  { label: "Team-wide usage", values: [false, true] },
  { label: "Priority support", values: [false, true] },
];

export function ComparisonTable({
  plans = PLANS,
  features = FEATURES,
}: {
  plans?: string[];
  features?: Feature[];
}) {
  return (
    <div className={styles.tbl}>
      <div className={styles.tblHead}>
        <div className={styles.tblCell}>Feature</div>
        {plans.map((p) => (
          <div key={p} className={styles.tblCell}>{p}</div>
        ))}
      </div>
      <div className={styles.tblBody}>
        {features.map((f) => (
          <div key={f.label} className={styles.tblRow}>
            <div className={styles.tblCell}>
              <span className={styles.tblCellText}>{f.label}</span>
            </div>
            {f.values.map((v, i) => (
              <div key={i} className={styles.tblCell}>
                {v ? <span className={styles.yes}>✓</span> : <span className={styles.no}>—</span>}
              </div>
            ))}
          </div>
        ))}
      </div>
    </div>
  );
}
```

### React - `ComparisonTable.module.css`

```css
/* Theme follows the nearest [data-theme] ancestor (preview switch),
   then .dark, then the OS when no data-theme is set. */
:global(:root),
:global([data-theme="light"]) {
  --tbl-bg: #fafafa;
  --tbl-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.05),
    0 2px 4px rgba(0, 0, 0, 0.02);
  --tbl-body-bg: #fff;
  --tbl-line: #e6e8ec;
  --tbl-cell: #1a1a1a;
  --tbl-yes: #15a06a;
}
:global([data-theme="dark"]),
:global(.dark) {
  --tbl-bg: #242424;
  --tbl-shadow: 0 0 0 0.5px rgba(255, 255, 255, 0.12),
    0 1px 2px rgba(0, 0, 0, 0.4), 0 2px 4px rgba(0, 0, 0, 0.3);
  --tbl-body-bg: #1a1a1a;
  --tbl-line: #303030;
  --tbl-cell: #f5f5f5;
  --tbl-yes: #34d399;
}
@media (prefers-color-scheme: dark) {
  :global(:root:not([data-theme])) {
    --tbl-bg: #242424;
    --tbl-shadow: 0 0 0 0.5px rgba(255, 255, 255, 0.12),
      0 1px 2px rgba(0, 0, 0, 0.4), 0 2px 4px rgba(0, 0, 0, 0.3);
    --tbl-body-bg: #1a1a1a;
    --tbl-line: #303030;
    --tbl-cell: #f5f5f5;
    --tbl-yes: #34d399;
  }
}

.tbl { width: 100%; display: flex; flex-direction: column; border-radius: 12px; overflow: hidden; background: var(--tbl-bg, #fafafa); box-shadow: var(--tbl-shadow); font-size: 13px; }
.tblHead { display: flex; color: #a1a1a1; font-weight: 500; }
.tblHead .tblCell { padding-top: 7px; padding-bottom: 7px; }
.tblBody { display: flex; flex-direction: column; background: var(--tbl-body-bg, #fff); border: 0.5px solid var(--tbl-line, #e6e8ec); border-radius: 12px 12px 0 0; margin: 0 -0.5px -0.5px; }
.tblRow { display: flex; }
.tblRow:not(:last-child) { border-bottom: 0.5px solid var(--tbl-line, #e6e8ec); }
.tblCell { flex: 1 1 0; min-width: 0; padding: 9px 12px; display: flex; align-items: center; color: var(--tbl-cell, #1a1a1a); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tblCell:not(:last-child) { border-right: 0.5px solid var(--tbl-line, #e6e8ec); }
/* text-overflow:ellipsis has no effect on a flex container's raw text, so the
   label lives in this shrinkable child instead. */
.tblCellText { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.yes { color: var(--tbl-yes, #15a06a); }
.no { color: #a1a1a1; }

```

### Vue - `ComparisonTable.vue`

```vue
<template>
  <div class="tbl">
    <div class="tbl-head">
      <div class="tbl-cell">Feature</div>
      <div v-for="p in plans" :key="p" class="tbl-cell">{{ p }}</div>
    </div>
    <div class="tbl-body">
      <div v-for="f in features" :key="f.label" class="tbl-row">
        <div class="tbl-cell"><span class="tbl-cell-text">{{ f.label }}</span></div>
        <div v-for="(v, i) in f.values" :key="i" class="tbl-cell">
          <span v-if="v" class="yes">✓</span><span v-else class="no">—</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
defineProps({
  plans: { type: Array, default: () => ["Personal", "Enterprise"] },
  features: {
    type: Array,
    default: () => [
      { label: "Unlimited projects", values: [true, true] },
      { label: "All components", values: [true, true] },
      { label: "Team-wide usage", values: [false, true] },
      { label: "Priority support", values: [false, true] },
    ],
  },
});
</script>

<style scoped>
/* Theme follows the nearest [data-theme] ancestor, then .dark, then the OS. */
:global(:root),
:global([data-theme="light"]) {
  --tbl-bg: #fafafa;
  --tbl-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.05),
    0 2px 4px rgba(0, 0, 0, 0.02);
  --tbl-body-bg: #fff;
  --tbl-line: #e6e8ec;
  --tbl-cell: #1a1a1a;
  --tbl-yes: #15a06a;
}
:global([data-theme="dark"]),
:global(.dark) {
  --tbl-bg: #242424;
  --tbl-shadow: 0 0 0 0.5px rgba(255, 255, 255, 0.12),
    0 1px 2px rgba(0, 0, 0, 0.4), 0 2px 4px rgba(0, 0, 0, 0.3);
  --tbl-body-bg: #1a1a1a;
  --tbl-line: #303030;
  --tbl-cell: #f5f5f5;
  --tbl-yes: #34d399;
}
@media (prefers-color-scheme: dark) {
  :global(:root:not([data-theme])) {
    --tbl-bg: #242424;
    --tbl-shadow: 0 0 0 0.5px rgba(255, 255, 255, 0.12),
      0 1px 2px rgba(0, 0, 0, 0.4), 0 2px 4px rgba(0, 0, 0, 0.3);
    --tbl-body-bg: #1a1a1a;
    --tbl-line: #303030;
    --tbl-cell: #f5f5f5;
    --tbl-yes: #34d399;
  }
}
.tbl { width: 100%; display: flex; flex-direction: column; border-radius: 12px; overflow: hidden; background: var(--tbl-bg, #fafafa); box-shadow: var(--tbl-shadow); font-size: 13px; }
.tbl-head { display: flex; color: #a1a1a1; font-weight: 500; }
.tbl-head .tbl-cell { padding-top: 7px; padding-bottom: 7px; }
.tbl-body { display: flex; flex-direction: column; background: var(--tbl-body-bg, #fff); border: 0.5px solid var(--tbl-line, #e6e8ec); border-radius: 12px 12px 0 0; margin: 0 -0.5px -0.5px; }
.tbl-row { display: flex; }
.tbl-row:not(:last-child) { border-bottom: 0.5px solid var(--tbl-line, #e6e8ec); }
.tbl-cell { flex: 1 1 0; min-width: 0; padding: 9px 12px; display: flex; align-items: center; color: var(--tbl-cell, #1a1a1a); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tbl-cell:not(:last-child) { border-right: 0.5px solid var(--tbl-line, #e6e8ec); }
/* text-overflow:ellipsis has no effect on a flex container's raw text, so the
   label lives in this shrinkable child instead. */
.tbl-cell-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.yes { color: var(--tbl-yes, #15a06a); }
.no { color: #a1a1a1; }
</style>
```

### Svelte - `ComparisonTable.svelte`

```svelte
<script>
  export let plans = ["Personal", "Enterprise"];
  export let features = [
    { label: "Unlimited projects", values: [true, true] },
    { label: "All components", values: [true, true] },
    { label: "Team-wide usage", values: [false, true] },
    { label: "Priority support", values: [false, true] },
  ];
</script>

<div class="tbl">
  <div class="tbl-head">
    <div class="tbl-cell">Feature</div>
    {#each plans as p}<div class="tbl-cell">{p}</div>{/each}
  </div>
  <div class="tbl-body">
    {#each features as f (f.label)}
      <div class="tbl-row">
        <div class="tbl-cell"><span class="tbl-cell-text">{f.label}</span></div>
        {#each f.values as v}
          <div class="tbl-cell">{#if v}<span class="yes">✓</span>{:else}<span class="no">—</span>{/if}</div>
        {/each}
      </div>
    {/each}
  </div>
</div>

<style>
  /* Theme follows the nearest [data-theme] ancestor, then .dark, then the OS. */
  :global(:root),
  :global([data-theme="light"]) {
    --tbl-bg: #fafafa;
    --tbl-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.05),
      0 2px 4px rgba(0, 0, 0, 0.02);
    --tbl-body-bg: #fff;
    --tbl-line: #e6e8ec;
    --tbl-cell: #1a1a1a;
    --tbl-yes: #15a06a;
  }
  :global([data-theme="dark"]),
  :global(.dark) {
    --tbl-bg: #242424;
    --tbl-shadow: 0 0 0 0.5px rgba(255, 255, 255, 0.12),
      0 1px 2px rgba(0, 0, 0, 0.4), 0 2px 4px rgba(0, 0, 0, 0.3);
    --tbl-body-bg: #1a1a1a;
    --tbl-line: #303030;
    --tbl-cell: #f5f5f5;
    --tbl-yes: #34d399;
  }
  @media (prefers-color-scheme: dark) {
    :global(:root:not([data-theme])) {
      --tbl-bg: #242424;
      --tbl-shadow: 0 0 0 0.5px rgba(255, 255, 255, 0.12),
        0 1px 2px rgba(0, 0, 0, 0.4), 0 2px 4px rgba(0, 0, 0, 0.3);
      --tbl-body-bg: #1a1a1a;
      --tbl-line: #303030;
      --tbl-cell: #f5f5f5;
      --tbl-yes: #34d399;
    }
  }
  .tbl { width: 100%; display: flex; flex-direction: column; border-radius: 12px; overflow: hidden; background: var(--tbl-bg, #fafafa); box-shadow: var(--tbl-shadow); font-size: 13px; }
  .tbl-head { display: flex; color: #a1a1a1; font-weight: 500; }
  .tbl-head .tbl-cell { padding-top: 7px; padding-bottom: 7px; }
  .tbl-body { display: flex; flex-direction: column; background: var(--tbl-body-bg, #fff); border: 0.5px solid var(--tbl-line, #e6e8ec); border-radius: 12px 12px 0 0; margin: 0 -0.5px -0.5px; }
  .tbl-row { display: flex; }
  .tbl-row:not(:last-child) { border-bottom: 0.5px solid var(--tbl-line, #e6e8ec); }
  .tbl-cell { flex: 1 1 0; min-width: 0; padding: 9px 12px; display: flex; align-items: center; color: var(--tbl-cell, #1a1a1a); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .tbl-cell:not(:last-child) { border-right: 0.5px solid var(--tbl-line, #e6e8ec); }
  /* text-overflow:ellipsis has no effect on a flex container's raw text, so the
     label lives in this shrinkable child instead. */
  .tbl-cell-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .yes { color: var(--tbl-yes, #15a06a); }
  .no { color: #a1a1a1; }
</style>
```
