Skip to content
Marketing block

Integration Directory Grid

What each integration actually does, not a wall of logos — with availability as a text pill rather than a tinted border, and the name as the link so the card is not one enormous one.

208 linesNo dependenciesAdded 24 Aug 2026
  • integrations
  • directory
  • grid
  • ecosystem
  • compatibility

What's included

  • components/integration-grid.tsx
  • No runtime dependencies

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add integration-grid

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Works with what you already use

What each one actually does, rather than a wall of logos.

  • Figma

    Design

    Available

    Match a frame against the catalog and get back the components that already look like it.

  • VS Code

    Editor

    Available

    Install a component from the editor over MCP, without opening a browser.

  • Next.js

    Framework

    Available

    App Router by default — every block is a server component unless it needs not to be.

  • Tailwind CSS

    Styling

    Available

    Utility classes and CSS variables, so a copied block inherits your theme rather than fighting it.

  • shadcn/ui

    Registry

    Available

    A registry the CLI can install from, including a base preset carrying tokens and fonts.

  • Storybook

    Tooling

    Beta

    Generated stories per artifact, so a copied block lands with its states already documented.

  • Vue

    Framework

    Planned

    The effects already work anywhere. Blocks and pages as single-file components are in progress.

  • Sanity

    CMS

    Planned

    Content-driven pages with the copy lifted out into a schema you can edit.

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/integration-grid.tsx
/**
 * <IntegrationGrid> — the "works with your stack" directory.
 *
 * <HeroIntegrations> puts a wall of logos above the fold to make one
 * argument: your tools are on the list. This is the page that argument
 * sends people to, and it has a different job — not "are you here" but
 * "what does it actually do with the thing I use", which a bare logo tile
 * cannot answer.
 *
 * So every cell carries a sentence, and that sentence is the block. A
 * directory of logos with no description is a logo cloud that has been made
 * to scroll for longer.
 *
 * Two things the naive version gets wrong.
 *
 * Status is not a colour. An integration that is in beta or on the roadmap
 * is the single most important thing on the card for someone evaluating,
 * and shipping that as a tinted border means it is invisible to a
 * screen-reader user and to anyone who prints the page. It renders as a
 * text pill.
 *
 * The whole cell is not a link. Wrapping a card in an <a> gives a screen
 * reader one enormous link whose name is every word in the card, and it
 * makes text selection impossible. The name is the link; the card is a
 * container. `focus-within` moves the visible ring out to the cell so the
 * hit target still reads as the whole card.
 *
 * Wordmarks stand in for logos by default — remote SVGs are a layout shift
 * and a licence question, and `logo` takes a node when the real mark is
 * ready.
 */

import * as React from 'react'

export type IntegrationStatus = 'live' | 'beta' | 'planned'

export interface Integration {
  name: string
  description: string
  href?: string
  status?: IntegrationStatus
  category?: string
  /** Replaces the wordmark initial. */
  logo?: React.ReactNode
}

export interface IntegrationGridProps {
  integrations?: Integration[]
  heading?: string
  subheading?: string
  className?: string
}

const DEFAULT_INTEGRATIONS: Integration[] = [
  {
    name: 'Figma',
    description:
      'Match a frame against the catalog and get back the components that already look like it.',
    category: 'Design',
    status: 'live',
  },
  {
    name: 'VS Code',
    description:
      'Install a component from the editor over MCP, without opening a browser.',
    category: 'Editor',
    status: 'live',
  },
  {
    name: 'Next.js',
    description:
      'App Router by default — every block is a server component unless it needs not to be.',
    category: 'Framework',
    status: 'live',
  },
  {
    name: 'Tailwind CSS',
    description:
      'Utility classes and CSS variables, so a copied block inherits your theme rather than fighting it.',
    category: 'Styling',
    status: 'live',
  },
  {
    name: 'shadcn/ui',
    description:
      'A registry the CLI can install from, including a base preset carrying tokens and fonts.',
    category: 'Registry',
    status: 'live',
  },
  {
    name: 'Storybook',
    description:
      'Generated stories per artifact, so a copied block lands with its states already documented.',
    category: 'Tooling',
    status: 'beta',
  },
  {
    name: 'Vue',
    description:
      'The effects already work anywhere. Blocks and pages as single-file components are in progress.',
    category: 'Framework',
    status: 'planned',
  },
  {
    name: 'Sanity',
    description:
      'Content-driven pages with the copy lifted out into a schema you can edit.',
    category: 'CMS',
    status: 'planned',
  },
]

const STATUS_STYLE: Record<IntegrationStatus, { label: string; klass: string }> = {
  live: {
    label: 'Available',
    klass:
      'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400',
  },
  beta: {
    label: 'Beta',
    klass: 'border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-400',
  },
  planned: {
    label: 'Planned',
    klass: 'border-border/60 bg-muted/60 text-muted-foreground',
  },
}

export function IntegrationGrid({
  integrations = DEFAULT_INTEGRATIONS,
  heading = 'Works with what you already use',
  subheading = 'What each one actually does, rather than a wall of logos.',
  className = '',
}: IntegrationGridProps) {
  return (
    <section
      className={`mx-auto w-full max-w-6xl px-4 py-16 sm:px-6 sm:py-24 ${className}`}
    >
      {heading ? (
        <div className="mx-auto mb-12 max-w-2xl text-center">
          <h2 className="text-balance text-3xl font-bold tracking-tight sm:text-4xl">
            {heading}
          </h2>
          {subheading ? (
            <p className="mt-3 text-muted-foreground">{subheading}</p>
          ) : null}
        </div>
      ) : null}

      <ul className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
        {integrations.map((item) => {
          const status = STATUS_STYLE[item.status ?? 'live']
          return (
            <li
              key={item.name}
              className="flex min-w-0 flex-col rounded-2xl border border-border/60 bg-card/60 p-5 transition-colors hover:border-border focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-primary"
            >
              <div className="flex min-w-0 flex-wrap items-start justify-between gap-3">
                <div className="flex min-w-0 items-center gap-3">
                  <div
                    aria-hidden
                    className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border/60 bg-background text-sm font-bold text-muted-foreground"
                  >
                    {item.logo ?? item.name.charAt(0)}
                  </div>
                  <div className="min-w-0">
                    {item.name ? (
                      <h3 data-stress-ignore className="truncate font-semibold leading-tight">
                        {item.href ? (
                          <a
                            href={item.href}
                            // The ring lives on the cell via focus-within, so
                            // the link itself does not draw a second one.
                            className="outline-none hover:underline"
                          >
                            {item.name}
                          </a>
                        ) : (
                          item.name
                        )}
                      </h3>
                    ) : null}
                    {item.category ? (
                      <p className="break-words text-xs text-muted-foreground">
                        {item.category}
                      </p>
                    ) : null}
                  </div>
                </div>

                <span
                  className={`min-w-0 max-w-full break-words rounded-full border px-2 py-0.5 text-xs font-medium ${status.klass}`}
                >
                  {status.label}
                </span>
              </div>

              <p className="mt-3 min-w-0 break-words text-sm leading-relaxed text-muted-foreground">
                {item.description}
              </p>
            </li>
          )
        })}
      </ul>
    </section>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Nothing to install. No component library, no icon package.
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/integration-grid.tsx and import it where you need the section:

import { IntegrationGrid } from '@/components/integration-grid'

Customize

2 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
integrationsIntegration[]DEFAULT_INTEGRATIONS
headingstring'Works with what you already use'
subheadingstring'What each one actually does, rather than a w…
classNamestring''

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

integration-grid.html
<!--
  Integration Directory Grid — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block has no interactive behaviour, so nothing is missing.
-->
<section class="mx-auto w-full max-w-6xl px-4 py-16 sm:px-6 sm:py-24 ">
  <div class="mx-auto mb-12 max-w-2xl text-center">
    <h2 class="text-balance text-3xl font-bold tracking-tight sm:text-4xl">Works with what you already use</h2>
    <p class="mt-3 text-muted-foreground">What each one actually does, rather than a wall of logos.</p>
  </div>
  <ul class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
    <li class="flex min-w-0 flex-col rounded-2xl border border-border/60 bg-card/60 p-5 transition-colors hover:border-border focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-primary">
      <div class="flex min-w-0 flex-wrap items-start justify-between gap-3">
        <div class="flex min-w-0 items-center gap-3">
          <div aria-hidden="true" class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border/60 bg-background text-sm font-bold text-muted-foreground">F</div>
          <div class="min-w-0">
            <h3 data-stress-ignore="true" class="truncate font-semibold leading-tight">Figma</h3>
            <p class="break-words text-xs text-muted-foreground">Design</p>
          </div>
        </div>
        <span class="min-w-0 max-w-full break-words rounded-full border px-2 py-0.5 text-xs font-medium border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400">Available</span>
      </div>
      <p class="mt-3 min-w-0 break-words text-sm leading-relaxed text-muted-foreground">Match a frame against the catalog and get back the components that already look like it.</p>
    </li>
    <li class="flex min-w-0 flex-col rounded-2xl border border-border/60 bg-card/60 p-5 transition-colors hover:border-border focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-primary">
      <div class="flex min-w-0 flex-wrap items-start justify-between gap-3">
        <div class="flex min-w-0 items-center gap-3">
          <div aria-hidden="true" class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border/60 bg-background text-sm font-bold text-muted-foreground">V</div>
          <div class="min-w-0">
            <h3 data-stress-ignore="true" class="truncate font-semibold leading-tight">VS Code</h3>
            <p class="break-words text-xs text-muted-foreground">Editor</p>
          </div>
        </div>
        <span class="min-w-0 max-w-full break-words rounded-full border px-2 py-0.5 text-xs font-medium border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400">Available</span>
      </div>
      <p class="mt-3 min-w-0 break-words text-sm leading-relaxed text-muted-foreground">Install a component from the editor over MCP, without opening a browser.</p>
    </li>
    <li class="flex min-w-0 flex-col rounded-2xl border border-border/60 bg-card/60 p-5 transition-colors hover:border-border focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-primary">
      <div class="flex min-w-0 flex-wrap items-start justify-between gap-3">
        <div class="flex min-w-0 items-center gap-3">
          <div aria-hidden="true" class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border/60 bg-background text-sm font-bold text-muted-foreground">N</div>
          <div class="min-w-0">
            <h3 data-stress-ignore="true" class="truncate font-semibold leading-tight">Next.js</h3>
            <p class="break-words text-xs text-muted-foreground">Framework</p>
          </div>
        </div>
        <span class="min-w-0 max-w-full break-words rounded-full border px-2 py-0.5 text-xs font-medium border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400">Available</span>
      </div>
      <p class="mt-3 min-w-0 break-words text-sm leading-relaxed text-muted-foreground">App Router by default — every block is a server component unless it needs not to be.</p>
    </li>
    <li class="flex min-w-0 flex-col rounded-2xl border border-border/60 bg-card/60 p-5 transition-colors hover:border-border focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-primary">
      <div class="flex min-w-0 flex-wrap items-start justify-between gap-3">
        <div class="flex min-w-0 items-center gap-3">
          <div aria-hidden="true" class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border/60 bg-background text-sm font-bold text-muted-foreground">T</div>
          <div class="min-w-0">
            <h3 data-stress-ignore="true" class="truncate font-semibold leading-tight">Tailwind CSS</h3>
            <p class="break-words text-xs text-muted-foreground">Styling</p>
          </div>
        </div>
        <span class="min-w-0 max-w-full break-words rounded-full border px-2 py-0.5 text-xs font-medium border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400">Available</span>
      </div>
      <p class="mt-3 min-w-0 break-words text-sm leading-relaxed text-muted-foreground">Utility classes and CSS variables, so a copied block inherits your theme rather than fighting it.</p>
    </li>
    <li class="flex min-w-0 flex-col rounded-2xl border border-border/60 bg-card/60 p-5 transition-colors hover:border-border focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-primary">
      <div class="flex min-w-0 flex-wrap items-start justify-between gap-3">
        <div class="flex min-w-0 items-center gap-3">
          <div aria-hidden="true" class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border/60 bg-background text-sm font-bold text-muted-foreground">s</div>
          <div class="min-w-0">
            <h3 data-stress-ignore="true" class="truncate font-semibold leading-tight">shadcn/ui</h3>
            <p class="break-words text-xs text-muted-foreground">Registry</p>
          </div>
        </div>
        <span class="min-w-0 max-w-full break-words rounded-full border px-2 py-0.5 text-xs font-medium border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400">Available</span>
      </div>
      <p class="mt-3 min-w-0 break-words text-sm leading-relaxed text-muted-foreground">A registry the CLI can install from, including a base preset carrying tokens and fonts.</p>
    </li>
    <li class="flex min-w-0 flex-col rounded-2xl border border-border/60 bg-card/60 p-5 transition-colors hover:border-border focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-primary">
      <div class="flex min-w-0 flex-wrap items-start justify-between gap-3">
        <div class="flex min-w-0 items-center gap-3">
          <div aria-hidden="true" class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border/60 bg-background text-sm font-bold text-muted-foreground">S</div>
          <div class="min-w-0">
            <h3 data-stress-ignore="true" class="truncate font-semibold leading-tight">Storybook</h3>
            <p class="break-words text-xs text-muted-foreground">Tooling</p>
          </div>
        </div>
        <span class="min-w-0 max-w-full break-words rounded-full border px-2 py-0.5 text-xs font-medium border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-400">Beta</span>
      </div>
      <p class="mt-3 min-w-0 break-words text-sm leading-relaxed text-muted-foreground">Generated stories per artifact, so a copied block lands with its states already documented.</p>
    </li>
    <li class="flex min-w-0 flex-col rounded-2xl border border-border/60 bg-card/60 p-5 transition-colors hover:border-border focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-primary">
      <div class="flex min-w-0 flex-wrap items-start justify-between gap-3">
        <div class="flex min-w-0 items-center gap-3">
          <div aria-hidden="true" class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border/60 bg-background text-sm font-bold text-muted-foreground">V</div>
          <div class="min-w-0">
            <h3 data-stress-ignore="true" class="truncate font-semibold leading-tight">Vue</h3>
            <p class="break-words text-xs text-muted-foreground">Framework</p>
          </div>
        </div>
        <span class="min-w-0 max-w-full break-words rounded-full border px-2 py-0.5 text-xs font-medium border-border/60 bg-muted/60 text-muted-foreground">Planned</span>
      </div>
      <p class="mt-3 min-w-0 break-words text-sm leading-relaxed text-muted-foreground">The effects already work anywhere. Blocks and pages as single-file components are in progress.</p>
    </li>
    <li class="flex min-w-0 flex-col rounded-2xl border border-border/60 bg-card/60 p-5 transition-colors hover:border-border focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-primary">
      <div class="flex min-w-0 flex-wrap items-start justify-between gap-3">
        <div class="flex min-w-0 items-center gap-3">
          <div aria-hidden="true" class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border/60 bg-background text-sm font-bold text-muted-foreground">S</div>
          <div class="min-w-0">
            <h3 data-stress-ignore="true" class="truncate font-semibold leading-tight">Sanity</h3>
            <p class="break-words text-xs text-muted-foreground">CMS</p>
          </div>
        </div>
        <span class="min-w-0 max-w-full break-words rounded-full border px-2 py-0.5 text-xs font-medium border-border/60 bg-muted/60 text-muted-foreground">Planned</span>
      </div>
      <p class="mt-3 min-w-0 break-words text-sm leading-relaxed text-muted-foreground">Content-driven pages with the copy lifted out into a schema you can edit.</p>
    </li>
  </ul>
</section>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

Used in these pages

Want the whole screen instead of this one section? Open a page and copy it entire.

7 more blocks in Feature Sections

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse Feature Sections

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More Feature Sections blocks

View category
Open the full page for this block

Bento Feature Grid

Six tiles on an asymmetric 4x3 grid with per-tile spans, a hover glow and a mobile collapse that follows source order.

Feature Sections114 lines1 dep
Open the full page for this block

Alternating Feature Rows

A full row per feature with the media side flipping each time, so the eye re-enters the page instead of skimming a stack of identical blocks.

Feature Sections161 lines1 dep
Open the full page for this block

Icon Feature Grid

Uniform cells, no spans, no state — the grid for nine things that matter equally, where a bento would be a wall and tabs would be a filing cabinet.

Feature Sections150 lines1 dep
Open the full page for this block

Audience Persona Cards

A "who this is for" grid: four cards naming an audience, the outcome they want and the bullets that get them there.

Feature Sections145 lines1 dep
Open the full page for this block

Editor Window Showcase

A feature section paired with a tabbed fake editor — real selectable code, generated line numbers and a copy button.

Feature Sections164 lines1 dep
Open the full page for this block

Tabbed Feature Showcase

Features as a real tablist — arrow keys, roving focus and a drawn product panel per tab — for the section where four features would otherwise fight for one scroll.

Feature Sections217 lines1 dep
Open the full page for this block

Integration Depth Split

An integrations section that says how deep each one goes, instead of a wall of logos that all imply the same thing.

Feature Sections157 linesNo deps