Mikkel Manniche
Dansk English Deutsch

Vorlage · React · HTML

Bento features

Ein Funktionsbereich als Bento-Raster aus Kacheln, in dem die wichtigste Funktion in einer dunklen Kachel steht.

Vorschau

React

Installiere die Vorlage mit shadcn. Sie landet als normale Komponenten in deinem Projekt, die du selbst anpasst.

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/bento-features.json
npx shadcn@latest add @manniche/bento-features
bento-features.tsx
// Bento features: a section heading over a bento of feature tiles. The first feature is the lead tile: it spans two
// columns and two rows on wide boxes and is the inverted tile, so the one thing the page is about is lifted. Every
// tile has a title, one sentence and a visual slot you fill with whatever you like (DOM, SVG, a figure).
//
// How it reads: one `h2` for the section, a real list of features, and each tile is its own labelled region with an
// `h3`. The visual slot is decorative by default and hidden from screen readers; the sentence carries the meaning.
// Layout follows the box the section sits in (container queries): one column, then two, then a 3 x 3 bento.
// Motion: none of its own. Visuals you pass in decide that and should respect reduced motion.
import { useId, type ComponentProps, type ReactNode } from 'react'
import { cn } from '@/lib/utils'
import { DataTile } from '@/registry/manniche/data-tile/data-tile'

export type BentoFeature = {
  /** A stable key for the tile. */
  id: string
  /** The tile's name, shown as an `h3`. */
  title: ReactNode
  /** One sentence that says what the feature does for the reader. */
  description: ReactNode
  /** A small visual for the tile: DOM, SVG or a figure. Hidden from screen readers unless `visualLabel` is set. */
  visual?: ReactNode
  /** Names the visual for screen readers. Leave it out when the visual only decorates the sentence. */
  visualLabel?: string
}

export type BentoFeaturesProps = Omit<ComponentProps<'section'>, 'children' | 'title'> & {
  /** The section's `h2`. */
  heading: ReactNode
  /** A short line under the heading. */
  intro?: ReactNode
  /** A small mono label above the heading, such as "Features". */
  eyebrow?: ReactNode
  /** The features, lead first. Six fill the bento exactly; other counts reflow without holes at the ends. */
  features: readonly BentoFeature[]
}

export function BentoFeatures({ heading, intro, eyebrow, features, className, ...rest }: BentoFeaturesProps) {
  const headingId = useId()
  return (
    <section aria-labelledby={headingId} className={cn('@container w-full py-12 @3xl:py-20', className)} {...rest}>
      <div className="mx-auto max-w-6xl px-4 sm:px-6">
        <header className="mb-8 grid max-w-2xl gap-3 @3xl:mb-12">
          {eyebrow && <p className="font-mono text-xs tracking-[0.04em] text-muted-foreground tabular-nums">{eyebrow}</p>}
          <h2
            id={headingId}
            className="text-[clamp(30px,5.4cqw,52px)] leading-[1.02] font-extrabold tracking-[-0.035em] text-balance text-foreground"
            style={{ fontFamily: 'var(--font-display, inherit)', fontStretch: '86%' }}
          >
            {heading}
          </h2>
          {intro && <p className="text-base leading-relaxed text-pretty text-muted-foreground">{intro}</p>}
        </header>
        <ul className="grid list-none grid-cols-1 gap-3 p-0 @2xl:grid-cols-2 @2xl:gap-4 @4xl:grid-cols-3">
          {features.map((f, i) => {
            const lead = i === 0
            return (
              <li
                key={f.id}
                className={cn(
                  'flex min-w-0',
                  lead && '@2xl:col-span-2 @4xl:row-span-2',
                  // An odd tile at the end of the two-up layout takes the full row; the 3-up layout needs no help.
                  i === features.length - 1 && i % 2 === 1 && '@2xl:col-span-2 @4xl:col-span-1',
                )}
              >
                <DataTile title={f.title} inverted={lead} className="w-full">
                  <div className={cn('flex h-full flex-col gap-5', lead && '@4xl:gap-8')}>
                    {f.visual && (
                      <div
                        role={f.visualLabel ? 'img' : undefined}
                        aria-label={f.visualLabel}
                        aria-hidden={f.visualLabel ? undefined : true}
                        className={cn(
                          'relative min-h-32 flex-1 overflow-hidden rounded-[14px] bg-muted',
                          lead && 'min-h-48 @4xl:min-h-72',
                        )}
                      >
                        {f.visual}
                      </div>
                    )}
                    <p
                      className={cn(
                        'text-pretty text-muted-foreground',
                        lead ? 'max-w-prose text-base leading-relaxed' : 'text-[15px] leading-normal',
                      )}
                    >
                      {f.description}
                    </p>
                  </div>
                </DataTile>
              </li>
            )
          })}
        </ul>
      </div>
    </section>
  )
}
bento-features-demo.tsx
import { BentoFeatures, type BentoFeature } from '@/components/bento-features'

// Small Tiles-styled illustrations made of DOM and SVG. They only decorate the sentences next to them.
const bar = 'rounded-full bg-foreground/15'

function Board() {
  return (
    <div className="absolute inset-0 grid grid-cols-3 gap-2 p-3 @4xl:gap-3 @4xl:p-5">
      {[3, 2, 1].map((n, col) => (
        <div key={col} className="grid content-start gap-2 rounded-[10px] bg-card/60 p-2">
          <span className="h-2 w-8 rounded-full bg-foreground/30" />
          {Array.from({ length: n + 1 }).map((_, i) => (
            <span
              key={i}
              className={`block rounded-[8px] bg-card p-2 shadow-[0_0_0_1px_color-mix(in_oklab,var(--foreground)_9%,transparent)] ${col === 1 && i === 0 ? 'outline-2 outline-primary' : ''}`}
            >
              <span className={`${bar} block h-1.5 w-10/12`} />
              <span className={`${bar} mt-1.5 block h-1.5 w-1/2`} />
            </span>
          ))}
        </div>
      ))}
    </div>
  )
}

function Review() {
  return (
    <div className="absolute inset-0 grid content-center gap-2 p-4">
      {[70, 90, 55].map((w, i) => (
        <span key={i} className="flex items-center gap-2">
          <span className="size-5 flex-none rounded-full bg-foreground/20 font-mono text-[10px] leading-5 text-center">{i + 1}</span>
          <span className={`${bar} h-2`} style={{ width: `${w}%` }} />
        </span>
      ))}
    </div>
  )
}

function Handover() {
  return (
    <svg viewBox="0 0 160 72" className="absolute inset-0 size-full p-4" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
      <rect x="6" y="18" width="44" height="36" rx="8" className="text-foreground/40" />
      <rect x="110" y="18" width="44" height="36" rx="8" className="text-foreground/40" />
      <path d="M56 36h42M92 30l6 6-6 6" className="text-foreground" />
    </svg>
  )
}

function Roadmap() {
  return (
    <div className="absolute inset-0 grid content-center gap-2.5 p-4 font-mono text-[11px] text-muted-foreground tabular-nums">
      {[
        ['Mar', 40, 32],
        ['Apr', 24, 0],
        ['May', 16, 48],
      ].map(([m, w, x]) => (
        <span key={m} className="grid grid-cols-[2rem_1fr] items-center gap-2">
          {m}
          <span className="relative h-2 rounded-full bg-foreground/10">
            <span className="absolute inset-y-0 rounded-full bg-foreground/60" style={{ width: `${w}%`, left: `${x}%` }} />
          </span>
        </span>
      ))}
    </div>
  )
}

function Notes() {
  return (
    <div className="absolute inset-0 grid content-center gap-2 p-4">
      <span className={`${bar} h-2 w-full`} />
      <span className={`${bar} h-2 w-10/12`} />
      <span className="flex items-center gap-1.5">
        <span className={`${bar} h-2 w-5/12`} />
        <span className="h-4 w-px bg-primary" />
      </span>
    </div>
  )
}

function Archive() {
  return (
    <div className="absolute inset-0 flex items-center justify-center gap-2 p-4 font-mono text-[11px] text-muted-foreground tabular-nums">
      {['v1', 'v2', 'v3'].map((v, i) => (
        <span key={v} className={`rounded-full px-3 py-1 ${i === 2 ? 'bg-foreground text-background' : 'bg-card'}`}>
          {v}
        </span>
      ))}
    </div>
  )
}

const FEATURES: BentoFeature[] = [
  {
    id: 'board',
    title: 'One board for the whole project',
    description:
      'Briefs, drafts and decisions sit in the same place as the work, so nobody in Munich or Aalborg has to ask where the latest version lives.',
    visual: <Board />,
  },
  { id: 'review', title: 'Review in order', description: 'Comments land in the order they were made, one owner each.', visual: <Review /> },
  { id: 'handover', title: 'Clean handovers', description: 'Hand a task over with its context attached.', visual: <Handover /> },
  { id: 'roadmap', title: 'A plan you can read', description: 'Weeks on one line each, with nothing hidden in a sub-menu.', visual: <Roadmap /> },
  { id: 'notes', title: 'Notes beside the work', description: 'Write next to the thing you are writing about.', visual: <Notes /> },
  { id: 'archive', title: 'Every version kept', description: 'Go back to any earlier version without asking anyone.', visual: <Archive /> },
]

export default function BentoFeaturesDemo() {
  return (
    <BentoFeatures
      eyebrow="Halden Studio"
      heading="Everything a small studio needs to ship together."
      intro="Six tools that stay out of the way. Example copy for a demo company."
      features={FEATURES}
    />
  )
}

HTML

Die ganze Seite als eine Datei mit eingebautem CSS und ohne JavaScript. Umschalter, Tabs und Filter funktionieren allein mit HTML und CSS.

In neuem Tab öffnen

Die Farbe in deiner eigenen App

Wenn shadcn bereits eingerichtet ist, ergeben diese Variablen dieselbe Farbe. Hell in :root, dunkel in .dark.

:root {
  --primary: oklch(0.52 0.2 262);
  --primary-foreground: oklch(0.99 0 0);
  --accent: oklch(0.95 0.034 262);
  --accent-foreground: oklch(0.36 0.140 262);
  --ring: oklch(0.52 0.2 262);
}

.dark {
  --primary: oklch(0.74 0.150 262);
  --primary-foreground: oklch(0.18 0.024 262);
  --accent: oklch(0.29 0.050 262);
  --accent-foreground: oklch(0.9 0.050 262);
  --ring: oklch(0.74 0.150 262);
}

Installieren

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/bento-features.json

Abhängigkeiten

    Dateien

    • bento-features.tsx
    • bento-features.html

    Manniche Lab · Open Source

    Kostenlose Werkzeuge für KI-Agenten und Webentwicklung.