Bruno DelorenceBruno
Appearance
Appearance

Choose your reading theme.

Contact

Insight card

New

A feature card where data sources stream through a funnel of light into a scanner and come out as folders of findings.

insight-card/insight-card.tsx
Live · interactive

Analyze → Summarize → Act

Turn raw data into clear insights and actionable next steps.

The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.

When to use

  • A feature grid or landing page, where one card has to say “raw data in, insights out” at a glance.
  • Introducing an analysis, summary or reporting feature, with its three kinds of output named on the folders.
  • A hero or empty state that needs motion to feel alive, but must stay calm: it slows under the pointer and stops off screen.
  • Pages that switch between light and dark: the card follows the page, or the colorScheme you give it.

When not to use

  • Showing real numbers or results. The picture is an illustration; use a chart or a stat card.
  • Many cards side by side all moving. Keep one animated and give the rest autoplay={false}, or use still pictures.
  • Long titles or lines: they are cut to one line (two when narrow) with an ellipsis. Use a plain text card.
  • An action the reader must take. The card has no button besides Pause; use a call to action.
  • Showing a real pipeline’s progress. Use a progress bar or a step list that reflects real work.

You can add this component with the shadcn CLI or by hand:

Into components/insight-card of any React project, with no setup:

Terminal
npx shadcn@latest add https://brunodelorence.com/r/insight-card.json

Either way it needs React 19 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.

Usage

features.tsx
import { InsightCard } from './insight-card';export function Features() {  return (    <InsightCard      autoplay={false}      title="Analyze → Summarize → Act"      text="Turn raw data into clear insights and actionable next steps."      outputs={['Customer Feedback', 'Revenue Summary', 'Post-Sales Insight']}    />  );}

Each state below is a live instance, locked so you can study it.

The pieces it is built from, each with its measurements read live.

A feature card for a data product. Round badges of sources glide in from the left through a funnel of light and dotted particles, into a glowing scanner line. Folders of findings step out of it on the right, one every three sources. The loop runs only while the card is on screen, slows under the pointer, and can be paused.

  • The funnel, the blue beyond the scanner and its soft streaks of light are one static SVG that never changes.
  • The moving rows use the Web Animations API, each shifted by its transform alone. The sources glide steadily, one a second, and the particles drift with them; the folders step on by one, eased, every third source. Each row holds two rounds of its drawings and loops by exactly one round, so the loop has no seam.
  • A haze of white light at the funnel’s wide end swells as every third source comes in, then drifts on and fades. It is a gradient box moved by transform and opacity, clipped to the funnel’s shape.
  • The scanner hides what passes it. The sources’ row is clipped at the scanner’s line, the folders come out from under a band of frost just past it, and the particles are clipped to the funnel by a polygon computed from its two curves.
  • The loop plays only while the card is on screen in a visible tab. Hovering the card eases its speed to about a third. A button pauses it, and under reduced motion it starts as a still frame.
  • The stage, the card and its words are light-dark() tokens, so they follow the page’s color-scheme, or the colorScheme prop; the picture (funnel, haze, badges, folders, scanner) is content and stays as it is, and so does the pause button’s glass, which sits on it.
PropTypeDefaultDescription
titlestring'Analyze → Summarize → Act'The card’s title.
textstring'Turn raw data into clear insights and actionable next steps.'The line under the title.
outputsreadonly [string, string, string]['Customer Feedback', 'Revenue Summary', 'Post-Sales Insight']The three folders that come out of the scanner, in turn.
onPausedChange(paused: boolean) => void—Called when the reader pauses (true) or plays (false) the flow with its button.
autoplaybooleantrueStarts the flow on its own when the card comes on screen. When false, it waits, still, for Play.
colorScheme'light' | 'dark' | 'system'—Its colours: light, dark, or the reader’s system setting. Without it the component follows the page’s color-scheme, which is light unless the page sets one.
classNamestring—Extra class for the root, a stage of its own.

The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.

insight-card.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useEffect, useId, useRef, useState, type CSSProperties, type PointerEvent } from 'react';import { Backdrop, FolderDrawing, GLYPHS, SCANNER, funnelPolygon } from './drawings';import styles from './insight-card.module.css';/* * Insight card: a feature card whose picture shows raw data turned into insights. Round badges of * sources stream in from the left through a funnel of light, into a glowing scanner; folders of * findings slide out of it on the right. * * - Every size is a multiple of one unit, the card's width over 538, so the picture keeps its *   proportions at any width. The funnel, its glow and the blue beyond are one static SVG. * - Three rows move, each a row of fixed drawings shifted by its transform with the Web *   Animations API: the sources glide on steadily, one a second; the dotted particles in the *   funnel drift with them; the folders step on by one, eased, every third source. Each row holds *   two rounds of its drawings and loops by exactly one round, so the loop has no seam. * - The scanner hides what passes it: the sources' row is clipped at its line, and the folders' *   row is clipped there too; a band of frost just past the line whitens them as they come out. * - It plays only while on screen in a visible tab. Hovering the card slows it to a third, the *   speed eased in and out. A button pauses it; under reduced motion it starts still, as a frame. * - Light or dark: the stage, the card and its words are light-dark() tokens in the stylesheet, so
insight-card.module.css
/* Insight card (insight-card.tsx): a card on a stage of its own, its picture above its title.   Every size is a multiple of --u, the card's width over 538, so the card scales as one drawing;   the stage is a size container and the card's width follows it, up to 540px. The scene is one   layout boundary of a size computed from that width, so nothing inside it lays out the page.   Motion. Three rows move by their transform alone, each on a layer of its own: the sources, the   particles and the folders. Their clips and the mask that fades the folders in are fixed boxes   around them; the scanner and its glows are fixed gradients over them.   Colours. The interface's are the tokens below, each light-dark(light, dark): the card takes the   color-scheme it inherits from the page (light unless the page says otherwise), or the one its   colorScheme prop sets (data-color-scheme). The picture (drawings.tsx, and the haze, particles,   badges, frost and scanner here) is content: the same in both. --text and the rows' places, set   on the scene and the picture, are sizes, so no token below takes those names. */.stage {  /* The stage, and the card on it: white on white by day, the card a step lighter at night. */  --surface: #fff;  --surface-2: #fff;  /* The title, the line under it, and the stage's own text. */  --ink: #383838;  --ink-muted: #6b6b6b;  --ink-soft: #484848;
drawings.tsx
/* * The insight card's drawings: the backdrop of its picture (the funnel on the left, the light * streaks on the right), the glyphs of the sources that flow in, and the folders that come out. * Every drawing is static; the card moves the boxes that hold them. *//** The picture's box, in the units every size of the card is a multiple of. */export const MEDIA = { width: 512, height: 277 };/** Where the scanner stands, across the picture. */export const SCANNER = 223;/** The funnel's upper and lower edges, from the left side to the scanner. */export const FUNNEL_TOP = 'M0 0C70 50 130 95 223 105';export const FUNNEL_BOTTOM = 'M0 277C60 240 120 195 223 191';/** A point of a cubic Bézier curve. */const bezier = (t: number, a: number, b: number, c: number, d: number) =>  (1 - t) ** 3 * a + 3 * (1 - t) ** 2 * t * b + 3 * (1 - t) * t ** 2 * c + t ** 3 * d;/** * The inside of the funnel, kept `inset` units clear of its edges (a negative inset reaches past * them), as a CSS polygon in shares of the left part of the picture (the scanner's width by the * picture's height). */
  • The picture is one image whose label describes the flow and names the folders; all its drawings are hidden from screen readers.
  • Pause animation is a real toggle button with aria-pressed and a visible focus ring. Pausing and playing are announced in a polite live region.
  • Under reduced motion the card starts as a still frame, and plays only if the reader presses the button.
  • The loop stops off screen and in hidden tabs.