Bento Grid
Ein Bento-Raster als Layout-Baustein mit Slots. Kacheln mit Medien, Zahlen, Icons und Links in vier lückenlosen Rastern.
Kacheln, die jede Zelle füllen
Ein Bento-Raster setzt Kacheln unterschiedlicher Größe zu einem ruhigen Block zusammen, die großen für das Wichtigste. Dieses ist ein Layout-Baustein mit Slots, du setzt Medien, Zahlen, Icons und Links ein, und die vier Raster füllen jede Zelle auf jeder Bildschirmgröße.
„Große Kachel links“ setzt eine große Kachel mit Bildschirm links und vier kleine daneben, eine getönt und eine dunkel. „Mosaik auf Haarlinien“ legt sechs Kacheln auf Haarlinien, eine breit, eine hoch und zwei halb breit, die Zellen behalten ihre Fläche, während der Inhalt aufsteigt. „Hohe Mitte“ stellt eine hohe Bildkachel zwischen zwei Paare, jede Kachel ein Bild mit Text auf weichem Schatten. „Zahlen und Bilder“ mischt Kennzahlen, eine Notiz und Bilder in drei Reihen, für eine About-Seite oder einen Betrieb in Zahlen.
Gib die größte Kachel der einen Botschaft, die verkauft. Misch zwei oder drei Kachelarten, das hält den Block lebendig. Prüf die Reihenfolge am Handy, sie sollte die Geschichte weiter erzählen.
Variants
Details
Was Bento-Raster mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Layouts
- Bereich
- Layout
- Variants
- 4
- Version
- 1.0.0 · seit 1.2.0
- Datei
- components/ui/bento-grid.tsx
- Bringt mit
- utilskiticonsectionreveal
- npm-Pakete
- Keine
- Sprachen
- Texte kommen über Props
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle BentoGridProps aus components/ui/bento-grid.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
variantoptionalBentoGridVarianthero-leftmosaictall-centerstats-mix
eyebrowoptionalReact.ReactNodetitleoptionalReact.ReactNodedescriptionoptionalReact.ReactNodetilesPflichtBentoTile[]
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
- Kommt ohne Client-State aus. Der Server schreibt sie vollständig, sie ist auch ohne JavaScript lesbar.
Bewegung
- Erscheint beim ersten Sichtkontakt mit einem kurzen Auftritt (kit-rise, useInView).
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 3 Stellen im Code schalten Bewegung dann ab.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 165 Zeilen
'use client';
import { cn, useSpotlight } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';
import { Eyebrow, Section } from '@/components/ui/section';
import { Reveal } from '@/components/ui/reveal';
/*
* A bento grid as a layout piece: tiles of different sizes that fill a
* rectangle without gaps. Every tile is a slot with optional media, a
* figure, an icon and a link, so the same grid holds features, work,
* numbers or places. The variant picks the grid.
*
* hero-left one large tile on the left, four small ones beside it
* mosaic six tiles on hairlines, one tall and two wide
* tall-center a tall picture in the middle between two pairs
* stats-mix figures, a note and pictures mixed in three rows
*
* Phones get one column in the same order, tablets two, with the large
* tiles across both.
*/
export type BentoGridVariant = 'hero-left' | 'mosaic' | 'tall-center' | 'stats-mix';
export interface BentoTile {
eyebrow?: string;
title?: string;
text?: string;
/** A large figure, e.g. "98 %" */
value?: string;
icon?: IconName;
href?: string;
/** Picture or any node for the tile */
media?: React.ReactNode;
/** accent: tinted with the accent color, inverse: dark in both modes */
tone?: 'default' | 'accent' | 'inverse';
}
export interface BentoGridProps {
variant?: BentoGridVariant;
eyebrow?: React.ReactNode;
title?: React.ReactNode;
description?: React.ReactNode;
tiles: BentoTile[];
className?: string;
}
/* Where each tile sits, by position. Phones: one column. */
const SPANS: Record<BentoGridVariant, { grid: string; tiles: string[]; cover: boolean[] }> = {
'hero-left': {
grid: 'sm:grid-cols-2 lg:grid-cols-4 lg:auto-rows-[minmax(15rem,auto)]',
tiles: ['sm:col-span-2 lg:row-span-2', '', '', '', ''],
cover: [false, false, false, false, false],
},
mosaic: {
grid: 'sm:grid-cols-2 lg:grid-cols-6 lg:auto-rows-[minmax(14rem,auto)]',
tiles: ['sm:col-span-2 lg:col-span-4', 'lg:col-span-2 lg:row-span-2', 'lg:col-span-2', 'lg:col-span-2', 'lg:col-span-3', 'sm:col-span-2 lg:col-span-3'],
cover: [false, false, false, false, false, false],
},
'tall-center': {
grid: 'sm:grid-cols-2 lg:grid-cols-3 lg:auto-rows-[minmax(16rem,auto)]',
tiles: ['', 'sm:row-span-2 lg:col-start-2 lg:row-start-1', 'lg:col-start-1 lg:row-start-2', 'lg:col-start-3 lg:row-start-1', 'lg:col-start-3 lg:row-start-2'],
cover: [true, true, true, true, true],
},
'stats-mix': {
grid: 'sm:grid-cols-2 lg:grid-cols-4 lg:auto-rows-[minmax(12rem,auto)]',
tiles: ['sm:col-span-2 lg:row-span-2', '', '', 'lg:col-span-2', '', 'sm:col-span-2 lg:col-span-3'],
cover: [true, false, false, false, false, true],
},
};
const focus = 'outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-ring';
function Tile({ tile, cover, big, wide = false, tall = false, className, index, hairline }: { tile: BentoTile; cover: boolean; big: boolean; wide?: boolean; tall?: boolean; className?: string; index: number; hairline: boolean }) {
const spot = useSpotlight<HTMLElement>();
const tone = tile.tone ?? 'default';
const Tag = tile.href ? 'a' : 'div';
const hasMedia = !!tile.media;
const asCover = cover && hasMedia;
const body = (
<>
{tile.icon && !asCover && (
<span className={cn('mb-5 flex size-10 items-center justify-center rounded-xl border', tone === 'default' ? 'border-border bg-surface text-foreground' : 'border-white/15 bg-white/10 text-current')}>
<Icon name={tile.icon} size={19} />
</span>
)}
{tile.eyebrow && <span className={cn('block text-[12.5px] font-medium', asCover || tone === 'inverse' ? 'text-white/75' : tone === 'accent' ? 'text-[color-mix(in_oklab,var(--primary)_85%,var(--foreground))]' : 'text-[color-mix(in_oklab,var(--primary)_75%,var(--muted-foreground))]')}>{tile.eyebrow}</span>}
{tile.value && <span className={cn('mt-1 block font-semibold leading-none tracking-[-0.04em] tabular-nums', big ? (wide ? 'text-[clamp(3rem,2.2rem+3vw,4.5rem)]' : 'text-[clamp(2.25rem,1.9rem+1.4vw,3rem)] lg:text-[clamp(3rem,2.2rem+3vw,4.5rem)]') : 'text-[clamp(2.25rem,1.9rem+1.4vw,3rem)]')}>{tile.value}</span>}
{tile.title && <span className={cn('block text-balance font-semibold tracking-[-0.02em]', tile.value ? 'mt-3 text-[15px]' : big ? (wide ? 'mt-2 text-[clamp(1.375rem,1.15rem+0.9vw,1.875rem)] leading-tight' : 'mt-1.5 text-[17px] leading-snug lg:mt-2 lg:text-[clamp(1.375rem,1.15rem+0.9vw,1.875rem)] lg:leading-tight') : 'mt-1.5 text-[17px] leading-snug')}>{tile.title}</span>}
{tile.text && <span className={cn('mt-2 block max-w-[34rem] text-pretty text-[14.5px] leading-relaxed', asCover || tone === 'inverse' ? 'text-white/80' : 'text-muted-foreground')}>{tile.text}</span>}
{tile.href && (
<span className={cn('mt-4 inline-flex size-9 items-center justify-center rounded-full border transition-transform duration-300 group-hover:translate-x-1', asCover || tone === 'inverse' ? 'border-white/30 text-white' : 'border-border text-foreground')}>
<Icon name="arrow-right" size={15} />
</span>
)}
</>
);
// On hairlines the cell keeps its surface and only its content rises, so no grey block shows through
const outer = (children: React.ReactNode) =>
hairline ? <div className={cn('flex', className)}>{children}</div> : <Reveal delay={index * 70} className={cn('flex', className)}>{children}</Reveal>;
const inner = (children: React.ReactNode) => (hairline ? <Reveal delay={index * 70} className="relative block p-6">{children}</Reveal> : <span className="relative block p-6">{children}</span>);
return outer(
<Tag
{...(tile.href ? { href: tile.href } : {})}
onPointerMove={spot}
className={cn(
'kit-spotlight group relative flex w-full flex-col overflow-hidden',
hairline ? '' : 'rounded-2xl border shadow-[var(--highlight),var(--shadow-sm)]',
tone === 'accent' ? 'border-[color-mix(in_oklab,var(--primary)_30%,var(--border))] bg-[color-mix(in_oklab,var(--primary)_10%,var(--card))] text-foreground' : tone === 'inverse' ? 'dark border-border bg-background text-foreground' : 'border-border bg-card text-foreground',
asCover ? 'min-h-[18rem] justify-end text-white' : 'justify-between',
tile.href && 'transition-[transform,box-shadow] duration-300 ease-[var(--ease-kit)] hover:-translate-y-0.5 hover:shadow-[var(--highlight),var(--shadow-md)] motion-reduce:hover:translate-y-0',
tile.href && focus,
)}
>
{asCover ? (
<>
<span data-clip-ok className="absolute inset-0 -z-20 block transition-transform duration-700 ease-[var(--ease-kit)] group-hover:scale-[1.04] motion-reduce:transition-none [&>*]:size-full">{tile.media}</span>
<span aria-hidden className="absolute inset-0 -z-10 bg-[linear-gradient(to_top,rgb(0_0_0/0.88),rgb(0_0_0/0.62)_50%,rgb(0_0_0/0.2)_80%,rgb(0_0_0/0.05))]" />
{inner(body)}
</>
) : (
<>
{inner(body)}
{hasMedia && (
// On phones a large screen keeps a tablet width and runs off the right edge of the tile, instead of squeezing its content
<span data-clip-ok className={cn('relative mb-0 ml-6 mt-auto block overflow-hidden rounded-t-xl border border-b-0 border-border bg-muted sm:mr-6', big && !tall ? 'max-sm:aspect-[5/4] max-sm:rounded-tr-none max-sm:border-r-0' : 'mr-6', tall ? 'aspect-[16/10] lg:aspect-auto lg:min-h-[16rem] lg:flex-1' : big ? (wide ? 'sm:aspect-[2/1] lg:aspect-[16/10]' : 'sm:aspect-[16/10]') : 'aspect-[16/9]')}>
<span className={cn('absolute inset-y-0 left-0 block transition-transform duration-700 ease-[var(--ease-kit)] group-hover:-translate-y-1 motion-reduce:transition-none [&>*]:size-full', big && !tall ? 'w-[34rem] sm:w-full' : 'w-full')}>{tile.media}</span>
</span>
)}
</>
)}
</Tag>,
);
}
/**
* A bento grid with slots: large and small tiles that fill a rectangle,
* each with optional media, figure, icon and link.
* <BentoGrid variant="mosaic" tiles={[{ title, text, media }, { value: '98 %', title }]} />
*/
export function BentoGrid({ variant = 'hero-left', eyebrow, title, description, tiles, className }: BentoGridProps) {
const spec = SPANS[variant];
const hairline = variant === 'mosaic';
const big = (i: number) => /row-span-2|col-span-4|col-span-3/.test(spec.tiles[i] ?? '');
return (
<Section className={className}>
{(title || eyebrow) && (
<div className="mb-12 grid gap-6 sm:mb-14 lg:grid-cols-2 lg:items-end lg:gap-16">
<div>
{eyebrow && <Eyebrow>{eyebrow}</Eyebrow>}
{title && <h2 className="text-balance text-h2 font-semibold text-foreground">{title}</h2>}
</div>
{description && <p className="max-w-[30rem] text-pretty text-lead text-muted-foreground lg:justify-self-end">{description}</p>}
</div>
)}
<div className={cn('grid grid-cols-[minmax(0,1fr)]', spec.grid, hairline ? 'gap-px overflow-hidden rounded-2xl border border-border bg-border shadow-[var(--shadow-sm)]' : 'gap-4')}>
{tiles.map((tile, i) => (
<Tile key={i} tile={tile} index={i} cover={spec.cover[i % spec.cover.length] ?? false} big={big(i)} wide={/sm:col-span-2/.test(spec.tiles[i] ?? '')} tall={/^lg:col-span-2 lg:row-span-2$/.test(spec.tiles[i] ?? '')} hairline={hairline} className={spec.tiles[i] ?? ''} />
))}
</div>
</Section>
);
}Häufige Fragen
Wie installiere ich Bento-Raster?
Mit aiw add @ai-website/bento-grid. Der Befehl legt die Datei components/ui/bento-grid.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon, section und reveal. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Bento-Raster?
4 Varianten, Große Kachel links, Mosaik auf Haarlinien, Hohe Mitte und Zahlen und Bilder. Du wählst sie mit der Prop variant, zum Beispiel variant="mosaic".
Kann ein KI-Assistent Bento-Raster einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Bento-Raster mit aiw_components und installieren sie mit aiw_add @ai-website/bento-grid. Nenn im Prompt die Variante, etwa variant="hero-left", dann schreibt der Assistent deine Inhalte direkt in die Props.