Comparison Table

Usage

JamieOtter AIFireflies.aiKrispSonnetSuperpoweredTactiq
Bot-Free
Yes
Both modes
Both modes
Yes
Yes
Yes
Yes (Chrome ext.)
Free Plan
10 meetings/mo
300 min/mo
800-min lifetime
60 min/day + 2 notes/day
5 recordings/mo
Calendar + 1-mo history
10 transcripts + 5 AI
Languages
100+
4
100+
16 (95+ in beta)
~10
50+
60+
Integrations
17 integrations
Slack, Salesforce, HubSpot…
40+ integrations
800+ via audio layer
HubSpot, Salesforce
Slack, Notion, Drive, CRMs
Drive, CRMs, Slack, Notion
Supported Devices
Mac, Windows, iOS
Web, iOS, Android
Web, iOS, Android
Mac, Windows
Mac (mobile soon)
Mac, Windows
Chrome only
Meeting Platforms
Any platform + in-person
Zoom, Meet, Teams (+ desktop audio)
Zoom, Meet, Teams, Webex (+ desktop audio)
Any communication app
Zoom, Meet, Teams, Discord, Slack
Any platform + in-person
Browser-based only
Data Residency
EU (GDPR)
US (AWS)
US (EU option on Business+)
US only
Not disclosed
SOC-2 Type-2 + GDPR
Likely US (GCP)
Recording Stored?
No, auto-deleted
Yes
Yes
Yes (when activated)
Yes
No, deleted immediately
User-controlled
Ask AI / Cross-Meeting Q&A
Yes, cross-meeting
Yes, workspace-scoped
Smart Search (keyword)
Via 3rd-party MCP
No
No
Yes — Ask Tactiq AI
MCP Integration
Yes
Yes
Yes
Yes
Not disclosed
Not disclosed
Not disclosed
Scroll horizontally for more tools
Click any cell with this icon to read more
import { ComparisonTable } from '@jamie/ui/compose/comparison-table'
 
type Verdict = 'yes' | 'no' | 'partial' | 'na'
 
type CellValue = {
  short: string
  long?: string
  verdict?: Verdict
}
 
type Tool = {
  name: string
  isJamie?: boolean
  values: Record<string, CellValue>
}
 
const features = [
  { key: 'botFree', label: 'Bot-Free' },
  { key: 'freePlan', label: 'Free Plan' },
  { key: 'languages', label: 'Languages' },
  { key: 'integrations', label: 'Integrations' },
  { key: 'devices', label: 'Supported Devices' },
  { key: 'platforms', label: 'Meeting Platforms' },
  { key: 'residency', label: 'Data Residency' },
  { key: 'recording', label: 'Recording Stored?' },
  { key: 'askAI', label: 'Ask AI / Cross-Meeting Q&A' },
  { key: 'mcp', label: 'MCP Integration' }
] as const
 
const tools: Tool[] = [
  {
    name: 'Jamie',
    isJamie: true,
    values: {
      botFree: { short: 'Yes', verdict: 'yes' },
      freePlan: { short: '10 meetings/mo', long: '10 meetings/mo, 30-min cap, all features' },
      languages: { short: '100+', verdict: 'yes' },
      integrations: {
        short: '17 integrations',
        long: 'HubSpot, Salesforce, Attio, Notion, Asana, Make.com + 11 more (CRM + note-taking + task + calendar + AI/MCP + SSO + webhooks)'
      },
      devices: { short: 'Mac, Windows, iOS', long: 'Mac, Windows, iOS' },
      platforms: {
        short: 'Any platform + in-person',
        long: 'Any platform incl. in-person (device-audio capture)'
      },
      residency: { short: 'EU (GDPR)', long: 'EU (German company, GDPR by default)' },
      recording: { short: 'No, auto-deleted', verdict: 'yes' },
      askAI: { short: 'Yes, cross-meeting', long: 'Yes, semantic, cross-meeting', verdict: 'yes' },
      mcp: { short: 'Yes', verdict: 'yes' }
    }
  },
  {
    name: 'Otter AI',
    values: {
      botFree: {
        short: 'Both modes',
        long: 'Both modes (bot default; bot-free desktop on Mac/Win)'
      },
      freePlan: { short: '300 min/mo', long: '300 min/mo, 30-min cap per call' },
      languages: { short: '4', long: '4 (English, Spanish, French, Japanese)' },
      integrations: {
        short: 'Slack, Salesforce, HubSpot…',
        long: 'Slack, Salesforce, HubSpot, Zoom, Teams, Meet + others (full count not publicly disclosed)'
      },
      devices: { short: 'Web, iOS, Android', long: 'Web, iOS, Android' },
      platforms: {
        short: 'Zoom, Meet, Teams (+ desktop audio)',
        long: 'Zoom, Meet, Teams (bot mode); any platform via desktop audio (bot-free mode)'
      },
      residency: {
        short: 'US (AWS)',
        long: 'US (AWS US-West region; EU-U.S. DPF certified for transfers)'
      },
      recording: { short: 'Yes' },
      askAI: {
        short: 'Yes, workspace-scoped',
        long: 'Yes, Otter AI Chat (workspace-scoped)',
        verdict: 'yes'
      },
      mcp: { short: 'Yes', long: 'Yes (bidirectional client+server)', verdict: 'yes' }
    }
  },
  {
    name: 'Fireflies.ai',
    values: {
      botFree: {
        short: 'Both modes',
        long: 'Both modes (bot default; bot-free desktop on Mac/Win)'
      },
      freePlan: { short: '800-min lifetime', long: '800-min storage lifetime cap' },
      languages: { short: '100+', verdict: 'yes' },
      integrations: {
        short: '40+ integrations',
        long: 'HubSpot, Salesforce, Pipedrive, Zoho, Dynamics 365, Slack, Notion + others (40+ total)'
      },
      devices: { short: 'Web, iOS, Android', long: 'Web, iOS, Android' },
      platforms: {
        short: 'Zoom, Meet, Teams, Webex (+ desktop audio)',
        long: 'Zoom, Meet, Teams, Webex (bot mode); any platform via desktop audio (bot-free mode)'
      },
      residency: {
        short: 'US (EU option on Business+)',
        long: 'US default (GCP servers + AWS DB); EU Private Storage option on Business+ tier'
      },
      recording: { short: 'Yes' },
      askAI: {
        short: 'Smart Search (keyword)',
        long: 'Smart Search (keyword only, not semantic)'
      },
      mcp: { short: 'Yes', long: 'Yes (Claude connector directory + OAuth)', verdict: 'yes' }
    }
  },
  {
    name: 'Krisp',
    values: {
      botFree: { short: 'Yes', long: 'Yes (no bot unless video)', verdict: 'yes' },
      freePlan: {
        short: '60 min/day + 2 notes/day',
        long: '60 daily min noise cancel + unlimited transcripts + 2 mtg notes/day'
      },
      languages: { short: '16 (95+ in beta)', long: '16 on Core / 95+ in beta' },
      integrations: {
        short: '800+ via audio layer',
        long: 'HubSpot, Salesforce, Pipedrive, Affinity (paid CRMs) + 800+ communication apps via audio layer'
      },
      devices: { short: 'Mac, Windows', long: 'Mac, Windows' },
      platforms: {
        short: 'Any communication app',
        long: 'Any communication app (audio-layer on 800+ apps)'
      },
      residency: {
        short: 'US only',
        long: 'US only (AWS) — no regional residency outside US'
      },
      recording: { short: 'Yes (when activated)', long: 'Yes (when features activated)' },
      askAI: {
        short: 'Via 3rd-party MCP',
        long: 'Via MCP integration with 3rd-party AI tools (no native Ask AI)'
      },
      mcp: { short: 'Yes', long: 'Yes (Krisp MCP server, OAuth, hosted)', verdict: 'yes' }
    }
  },
  {
    name: 'Sonnet',
    values: {
      botFree: { short: 'Yes', long: 'Yes (invisible recorder)', verdict: 'yes' },
      freePlan: { short: '5 recordings/mo', long: '5 recordings/mo, 30-min cap' },
      languages: { short: '~10', long: '~10 (English, Spanish, French, German + 6)' },
      integrations: {
        short: 'HubSpot, Salesforce',
        long: 'HubSpot, Salesforce + CRM-focused (full count not publicly disclosed)'
      },
      devices: { short: 'Mac (mobile soon)', long: 'Mac (mobile companion app launching)' },
      platforms: {
        short: 'Zoom, Meet, Teams, Discord, Slack',
        long: 'Zoom, Meet, Teams, Discord, Slack'
      },
      residency: { short: 'Not disclosed', long: 'Not disclosed (public security page not found)' },
      recording: { short: 'Yes' },
      askAI: { short: 'No', verdict: 'no' },
      mcp: { short: 'Not disclosed' }
    }
  },
  {
    name: 'Superpowered',
    values: {
      botFree: { short: 'Yes', verdict: 'yes' },
      freePlan: {
        short: 'Calendar + 1-mo history',
        long: 'Calendar auto-join + 1-month notes history (AI Notes paid only)'
      },
      languages: { short: '50+' },
      integrations: {
        short: 'Slack, Notion, Drive, CRMs',
        long: 'Slack, Notion, Google Drive, Salesforce, HubSpot, Zapier, email + others'
      },
      devices: { short: 'Mac, Windows' },
      platforms: {
        short: 'Any platform + in-person',
        long: 'Any platform incl. in-person (device audio)'
      },
      residency: { short: 'SOC-2 Type-2 + GDPR', long: 'SOC-2 Type-2 + GDPR compliant' },
      recording: {
        short: 'No, deleted immediately',
        long: 'No (audio deleted immediately)',
        verdict: 'yes'
      },
      askAI: { short: 'No', verdict: 'no' },
      mcp: { short: 'Not disclosed' }
    }
  },
  {
    name: 'Tactiq',
    values: {
      botFree: { short: 'Yes (Chrome ext.)', long: 'Yes (Chrome extension)', verdict: 'yes' },
      freePlan: {
        short: '10 transcripts + 5 AI',
        long: '10 transcripts/mo + 5 AI summary credits'
      },
      languages: { short: '60+' },
      integrations: {
        short: 'Drive, CRMs, Slack, Notion',
        long: 'Google Drive, HubSpot, Salesforce, Pipedrive, Slack, Notion + others'
      },
      devices: { short: 'Chrome only', long: 'Browser only (Chrome)' },
      platforms: {
        short: 'Browser-based only',
        long: 'Browser-based only (Meet/Zoom web/Teams web)'
      },
      residency: {
        short: 'Likely US (GCP)',
        long: 'Likely US (Google Cloud); current SOC-2 attestation'
      },
      recording: { short: 'User-controlled' },
      askAI: {
        short: 'Yes — Ask Tactiq AI',
        long: 'Yes — Ask Tactiq AI (single + multi-meeting analysis up to 10 on Team plan)',
        verdict: 'yes'
      },
      mcp: { short: 'Not disclosed' }
    }
  }
]
 
const verdictToIcon: Record<Verdict, 'check' | 'cross' | 'partial' | 'na'> = {
  yes: 'check',
  no: 'cross',
  partial: 'partial',
  na: 'na'
}
 
export function ComparisonTableDemo() {
  const highlightCol = tools.findIndex((t) => t.isJamie)
 
  return (
    <ComparisonTable.Root highlightCol={highlightCol >= 0 ? highlightCol : null}>
      <ComparisonTable.Table>
        <ComparisonTable.Head>
          <ComparisonTable.HeaderRow>
            <ComparisonTable.RowHeaderSlot />
            {tools.map((tool, idx) => (
              <ComparisonTable.HeaderCell key={tool.name} col={idx}>
                {tool.name}
              </ComparisonTable.HeaderCell>
            ))}
          </ComparisonTable.HeaderRow>
        </ComparisonTable.Head>
        <ComparisonTable.Body>
          {features.map((feature, rowIdx) => (
            <ComparisonTable.Row key={feature.key} row={rowIdx}>
              <ComparisonTable.RowHeader row={rowIdx}>{feature.label}</ComparisonTable.RowHeader>
              {tools.map((tool, colIdx) => {
                const value = tool.values[feature.key]
                if (!value) {
                  return (
                    <ComparisonTable.Cell
                      key={`${feature.key}-${tool.name}`}
                      row={rowIdx}
                      col={colIdx}
                    >
                      <span className="text-on-surface-variant/60">—</span>
                    </ComparisonTable.Cell>
                  )
                }
 
                const expandable =
                  value.long && value.long !== value.short ? <p>{value.long}</p> : undefined
 
                return (
                  <ComparisonTable.Cell
                    key={`${feature.key}-${tool.name}`}
                    row={rowIdx}
                    col={colIdx}
                    expandable={expandable}
                  >
                    {value.verdict ? (
                      <ComparisonTable.Icon variant={verdictToIcon[value.verdict]} />
                    ) : null}
                    <span>{value.short}</span>
                  </ComparisonTable.Cell>
                )
              })}
            </ComparisonTable.Row>
          ))}
        </ComparisonTable.Body>
      </ComparisonTable.Table>
      <ComparisonTable.Footer
        left="Scroll horizontally for more tools"
        right="Click any cell with this icon to read more"
      />
    </ComparisonTable.Root>
  )
}

Indexing

Each interactive part takes a numeric index:

  • HeaderCell takes col (0-based)
  • Row and RowHeader take row (0-based, must match)
  • Cell takes both row and col, matching its Row and the HeaderCell above it

The leftmost feature column is not a col. It uses RowHeaderSlot in the header and RowHeader in the body. Body columns start at col={0}. Derive both indices from the same .map() so hover alignment stays consistent.

import { ComparisonTable } from '@jamie/ui/compose/comparison-table'
 
<ComparisonTable.Root>
  <ComparisonTable.Table>
    <ComparisonTable.Head>
      <ComparisonTable.HeaderRow>
        <ComparisonTable.RowHeaderSlot />
        {tools.map((tool, col) => (
          <ComparisonTable.HeaderCell key={tool.id} col={col}>
            {tool.name}
          </ComparisonTable.HeaderCell>
        ))}
      </ComparisonTable.HeaderRow>
    </ComparisonTable.Head>

Body

    <ComparisonTable.Body>
      {features.map((feature, row) => (
        <ComparisonTable.Row key={feature.key} row={row}>
          <ComparisonTable.RowHeader row={row}>{feature.label}</ComparisonTable.RowHeader>
          {tools.map((tool, col) => (
            <ComparisonTable.Cell key={tool.id} row={row} col={col}>
              {tool.values[feature.key]}
            </ComparisonTable.Cell>
          ))}
        </ComparisonTable.Row>
      ))}
    </ComparisonTable.Body>
  </ComparisonTable.Table>
</ComparisonTable.Root>

Highlighted column

Pin and tint a column with highlightCol on Root. It stays sticky next to the feature column while the rest scrolls.

<ComparisonTable.Root highlightCol={ourIndex}>
  {/* … */}
</ComparisonTable.Root>

Expandable cells

expandable adds a chevron that swaps the short content for the expanded node in place. The cell never grows.

<ComparisonTable.Cell row={row} col={col} expandable={<p>{value.long}</p>}>
  {value.short}
</ComparisonTable.Cell>

Verdict icons

Variants: check, cross, partial, na.

<ComparisonTable.Icon variant="check" />

Optional caption with left and right slots. Renders nothing if both are omitted.

<ComparisonTable.Footer left="Scroll for more" right="Click chevrons to expand" />

Anatomy

  • Root accepts highlightCol?: number | null and featureColWidth?: string (default '180px')
  • Table, Head, Body are the semantic wrappers. Table also renders the fullscreen dialog
  • HeaderRow and HeaderCell (col)
  • RowHeaderSlot is the sticky top-left cell and hosts the fullscreen toggle
  • Row (row) and RowHeader (row)
  • Cell (row, col, optional expandable)
  • Footer (left, right)
  • Icon (variant)

Notes

  • Column widths are derived from content. Cell clamps to a min of [88, 180] and a max of 200. HeaderCell clamps to a min of [100, 160] and a max of 160. The widest cell in a column wins.
  • featureColWidth on Root sets the first column width and the sticky offset used by RowHeader and the highlighted column.
  • The component is 'use client', but the rendered DOM is a plain <table>, so it stays SEO and AEO friendly.
  • Used in apps/payload as the comparisonMatrixBlock rich-text block.