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.
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.
components/integration-grid.tsxnpx hoverlab add integration-grid
Or over MCP, from your editor's agent — no account needed.
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.
Start a page with this section — add more, order them, and leave with the page source.
What each one actually does, rather than a wall of logos.
Design
Match a frame against the catalog and get back the components that already look like it.
Editor
Install a component from the editor over MCP, without opening a browser.
Framework
App Router by default — every block is a server component unless it needs not to be.
Styling
Utility classes and CSS variables, so a copied block inherits your theme rather than fighting it.
Registry
A registry the CLI can install from, including a base preset carrying tokens and fonts.
Tooling
Generated stories per artifact, so a copied block lands with its states already documented.
Framework
The effects already work anywhere. Blocks and pages as single-file components are in progress.
CMS
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.
/**
* <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>
)
}
bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.Drop it at components/integration-grid.tsx and import it where you need the section:
import { IntegrationGrid } from '@/components/integration-grid'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.
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.
| Prop | Type | Default |
|---|---|---|
integrations | Integration[] | DEFAULT_INTEGRATIONS |
heading | string | 'Works with what you already use' |
subheading | string | 'What each one actually does, rather than a w… |
className | string | '' |
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 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>
What each framework gets across the whole catalog — effects convert properly; this rung is markup.
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.
Want the whole screen instead of this one section? Open a page and copy it entire.
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 SectionsCopying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.
Six tiles on an asymmetric 4x3 grid with per-tile spans, a hover glow and a mobile collapse that follows source order.
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.
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.
A "who this is for" grid: four cards naming an audience, the outcome they want and the bullets that get them there.
A feature section paired with a tabbed fake editor — real selectable code, generated line numbers and a copy button.
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.
An integrations section that says how deep each one goes, instead of a wall of logos that all imply the same thing.