# Insight Cards

> Show a set of findings one at a time, with buttons to move between them.

Source: https://ai.honestui.com/docs/components/insight-cards

```tsx
"use client"

import { InsightCard, InsightCards } from "@/components/ai/insight-cards"
import { BarChart, type ChartConfig } from "honestui/charts"
import { LineChart } from "honestui/charts"
import { PieChart } from "honestui/charts"

// Sample figures.
const failedRuns = [
  { week: "Sep 1", failed: 9 },
  { week: "Sep 8", failed: 7 },
  { week: "Sep 15", failed: 8 },
  { week: "Sep 22", failed: 4 },
  { week: "Sep 29", failed: 3 },
  { week: "Oct 6", failed: 2 },
]

const failedRunsConfig = {
  failed: { label: "Failed runs", colors: { light: ["#be123c"], dark: ["#fb7185"] } },
} satisfies ChartConfig

const lateInvoices = [
  { supplier: "harbor", invoices: 10 },
  { supplier: "northwind", invoices: 7 },
  { supplier: "others", invoices: 7 },
]

const lateInvoicesConfig = {
  harbor: { label: "Harbor Paper Co.", colors: { light: ["#1d4ed8"], dark: ["#60a5fa"] } },
  northwind: { label: "Northwind Dairy", colors: { light: ["#b45309"], dark: ["#fbbf24"] } },
  others: { label: "All others", colors: { light: ["#6b7280"], dark: ["#9ca3af"] } },
} satisfies ChartConfig

const replyTimes = [
  { day: "Mon", agent: 42, team: 95 },
  { day: "Tue", agent: 38, team: 110 },
  { day: "Wed", agent: 35, team: 88 },
  { day: "Thu", agent: 31, team: 102 },
  { day: "Fri", agent: 29, team: 97 },
]

const replyTimesConfig = {
  agent: { label: "Agent", colors: { light: ["#047857"], dark: ["#34d399"] } },
  team: { label: "Team", colors: { light: ["#6b7280"], dark: ["#9ca3af"] } },
} satisfies ChartConfig

const chartFrame = "h-44 w-full min-w-0"
const summary = "m-0 text-[var(--hui-color-foreground-base-secondary)]"

/**
 * Each card states its finding, plots it with an Honest UI chart, and then
 * gives the plotted values as text, because a canvas does not expose them.
 */
export function InsightCardsDemo() {
  return (
    <div className="w-full max-w-md min-w-0">
      <InsightCards title="Insights">
        <InsightCard title="Failed runs fell from 9 to 2 a week after the rounding fix.">
          <div className={chartFrame}>
            <BarChart data={failedRuns} config={failedRunsConfig} className="h-full w-full">
              <BarChart.Grid />
              <BarChart.XAxis dataKey="week" />
              <BarChart.Tooltip />
              <BarChart.Bar dataKey="failed" variant="default" />
            </BarChart>
          </div>
          <p className={summary}>
            Failed runs per week, oldest first: {failedRuns.map((row) => row.failed).join(", ")}.
          </p>
        </InsightCard>

        <InsightCard title="Two suppliers account for 71% of late invoices.">
          <div className={chartFrame}>
            <PieChart
              data={lateInvoices}
              dataKey="invoices"
              nameKey="supplier"
              config={lateInvoicesConfig}
              className="h-full w-full"
            >
              <PieChart.Legend />
              <PieChart.Tooltip />
              <PieChart.Pie innerRadius="36%" outerRadius="64%" />
            </PieChart>
          </div>
          <p className={summary}>
            Of 24 late invoices this quarter, Harbor Paper Co. sent 10, Northwind Dairy 7, and all
            other suppliers 7.
          </p>
        </InsightCard>

        <InsightCard title="The agent's first reply is about three times faster than the team's.">
          <div className={chartFrame}>
            <LineChart
              data={replyTimes}
              config={replyTimesConfig}
              xDataKey="day"
              className="h-full w-full"
            >
              <LineChart.Grid />
              <LineChart.XAxis dataKey="day" />
              <LineChart.Legend />
              <LineChart.Tooltip />
              <LineChart.Line dataKey="team" strokeVariant="dashed" strokeWidth={2} />
              <LineChart.Line dataKey="agent" strokeVariant="solid" strokeWidth={2} />
            </LineChart>
          </div>
          <p className={summary}>
            Median minutes to first reply this week: agent 29 to 42, team 88 to 110.
          </p>
        </InsightCard>
      </InsightCards>
    </div>
  )
}

```

## Overview [#overview]

Insight Cards shows several findings in the space of one. Each card states a finding as a sentence and then shows the evidence for it: a chart, a figure, a short explanation. Two buttons move between the cards.

It never advances by itself. The Beautiful UI original rotated on a timer, which moves content a person may be reading.

Insight Cards draws no charts itself. Put an Honest UI [chart](https://www.honestui.com/docs/charts) in each card, as the preview above does with a bar, a pie, and a line chart.

## Anatomy [#anatomy]

<Anatomy title="InsightCards">
  <AnatomyItem name="Heading" description="The title, the position, and the Previous and Next buttons" />

  <AnatomyItem name="InsightCard" description="One finding and its evidence" />
</Anatomy>

Installing `insight-cards` brings [Button](https://www.honestui.com/docs/components/button) along as a registry dependency.

## Behavior [#behavior]

### Moving between cards [#moving-between-cards]

One card is shown at a time. Previous and Next are inactive at the ends. With a single card, the position and the buttons are left out.

### Height [#height]

Cards can differ in height, and the content below moves when they do. Giving every chart the same height, as the preview does, keeps the cards close to one size.

## Accessibility [#accessibility]

The component is a section named by its heading. The position, such as "2 of 3", is a polite live region, so moving to another card announces it. Previous and Next have names, and at either end they are marked inactive without losing focus.

Each card is an article. State the finding in `title`, and give the plotted values as text under the chart. A chart is a canvas: its tooltip needs a pointer, and its values are not exposed to screen readers.

| Key                                  | Action                         |
| ------------------------------------ | ------------------------------ |
| <kbd>Tab</kbd>                       | Move to Previous and Next      |
| <kbd>Enter</kbd> or <kbd>Space</kbd> | Show the previous or next card |

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;@honestai/insight-cards&#x22;]" />
  

  
    
      
        Add the components it is built from.
      

      <CliBlock commands="[&#x22;button&#x22;]" />

      
        Copy and paste this file into your project.
      

      ### components/ai/insight-cards.tsx

```tsx
"use client"

import * as React from "react"
import { ChevronLeft as ChevronLeftIcon, ChevronRight as ChevronRightIcon } from "honestui/icons"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"

export type InsightCardsLabels = {
  previous: string
  next: string
  position: (current: number, total: number) => string
}

export const defaultInsightCardsLabels: InsightCardsLabels = {
  previous: "Previous insight",
  next: "Next insight",
  position: (current, total) => `${current} of ${total}`,
}

export type InsightCardsProps = Omit<React.ComponentProps<"section">, "title"> & {
  /** The heading, such as "Insights". */
  title: string
  /** One `InsightCard` for each insight. */
  children: React.ReactNode
  labels?: Partial<InsightCardsLabels>
}

/**
 * A set of findings shown one at a time, with buttons to move between them.
 * It never advances by itself.
 */
function InsightCards({ title, children, labels: labelsProp, className, ...props }: InsightCardsProps) {
  const labels = { ...defaultInsightCardsLabels, ...labelsProp }
  const cards = React.Children.toArray(children)
  const [index, setIndex] = React.useState(0)
  const current = Math.min(index, Math.max(cards.length - 1, 0))
  const headingId = React.useId()

  if (cards.length === 0) return null

  return (
    <section
      aria-labelledby={headingId}
      data-slot="insight-cards"
      className={cn(
        "flex w-full min-w-0 flex-col gap-[var(--hui-space-3)] text-[var(--hui-color-foreground-base-primary)] [font-size:var(--hui-font-size-small)] [line-height:var(--hui-line-height-small)]",
        className
      )}
      {...props}
    >
      <div className="flex items-center gap-[var(--hui-space-3)]">
        <p id={headingId} className="m-0 [font-weight:var(--hui-font-weight-semibold)]">
          {title}
        </p>
        {cards.length > 1 && (
          <>
            <span
              aria-live="polite"
              className="text-[var(--hui-color-foreground-base-secondary)]"
            >
              {labels.position(current + 1, cards.length)}
            </span>
            <div className="ms-auto flex items-center gap-[var(--hui-space-1)]">
              <Button
                variant="link"
                size="icon-sm"
                aria-label={labels.previous}
                // aria-disabled keeps focus on the button at either end.
                aria-disabled={current === 0}
                className="aria-disabled:cursor-not-allowed aria-disabled:opacity-50"
                onClick={() => setIndex(Math.max(current - 1, 0))}
              >
                <ChevronLeftIcon aria-hidden="true" />
              </Button>
              <Button
                variant="link"
                size="icon-sm"
                aria-label={labels.next}
                aria-disabled={current === cards.length - 1}
                className="aria-disabled:cursor-not-allowed aria-disabled:opacity-50"
                onClick={() => setIndex(Math.min(current + 1, cards.length - 1))}
              >
                <ChevronRightIcon aria-hidden="true" />
              </Button>
            </div>
          </>
        )}
      </div>
      {cards[current]}
    </section>
  )
}

export type InsightCardProps = Omit<React.ComponentProps<"article">, "title"> & {
  /** The finding, stated as a sentence. */
  title: React.ReactNode
}

/** One insight: its finding, then the evidence, such as a chart or a figure. */
function InsightCard({ title, className, children, ...props }: InsightCardProps) {
  return (
    <article
      data-slot="insight-card"
      className={cn(
        "flex min-w-0 flex-col gap-[var(--hui-space-4)] rounded-[var(--hui-radius-4)] border-[0.5px] border-[var(--hui-color-border-base-primary)] bg-[var(--hui-color-background-base-primary)] p-[var(--hui-space-5)] shadow-[var(--hui-shadow-feather)]",
        className
      )}
      {...props}
    >
      <p className="m-0 break-words [font-size:var(--hui-font-size-regular)] [font-weight:var(--hui-font-weight-medium)] [letter-spacing:var(--hui-letter-spacing-regular)] [line-height:var(--hui-line-height-regular)]">
        {title}
      </p>
      {children}
    </article>
  )
}

export { InsightCards, InsightCard }

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import { BarChart, type ChartConfig } from "honestui/charts"
import "honestui/charts.css"

import { InsightCard, InsightCards } from "@/components/ai/insight-cards"

const config = {
  failed: { label: "Failed runs", colors: { light: ["#be123c"], dark: ["#fb7185"] } },
} satisfies ChartConfig

<InsightCards title="Insights">
  <InsightCard title={insight.finding}>
    <div className="h-44 w-full">
      <BarChart data={insight.rows} config={config} className="h-full w-full">
        <BarChart.Grid />
        <BarChart.XAxis dataKey="week" />
        <BarChart.Tooltip />
        <BarChart.Bar dataKey="failed" />
      </BarChart>
    </div>
    <p>{insight.summary}</p>
  </InsightCard>
</InsightCards>
```

Give the chart a container with a height, or it has no room to draw. Charts come from the `honestui` package; see [chart installation](https://www.honestui.com/docs/charts/installation).

## Examples [#examples]

### A single insight [#a-single-insight]

With one card, there is nothing to move between.

```tsx
import { InsightCard, InsightCards } from "@/components/ai/insight-cards"

export function InsightCardsSingleExample() {
  return (
    <div className="w-full max-w-md min-w-0">
      <InsightCards title="Latest insight">
        <InsightCard title="Export totals now match the ledger on all 1,204 invoices.">
          <p className="m-0 text-[var(--hui-color-foreground-base-secondary)]">
            With one card, the position and the buttons are left out.
          </p>
        </InsightCard>
      </InsightCards>
    </div>
  )
}

```

## API reference [#api-reference]

### InsightCards [#insightcards]

Accepts native section props beyond those listed. Its children are the cards.

| Prop     | Type                          | Default  | Description                         |
| -------- | ----------------------------- | -------- | ----------------------------------- |
| `title`  | string                        | Required | The heading.                        |
| `labels` | `Partial<InsightCardsLabels>` | English  | Button names and the position text. |

### InsightCard [#insightcard]

Accepts native article props beyond those listed. Its children are the evidence.

| Prop    | Type      | Default  | Description                        |
| ------- | --------- | -------- | ---------------------------------- |
| `title` | ReactNode | Required | The finding, stated as a sentence. |
