Three things it does well, in the order people ask about them.
Open it and begin. There is nothing to install and nothing to set up first.
Every value is open, so it can look like your product and not like ours.
What you copy out is the file that was running, with nothing generated around it.
The pictures in these tiles are drawn from your own colours until you swap in real ones.
import type { ReactNode } from "react";
export interface Feature {
title: string;
body: string;
/** What is drawn in the top of the tile. Without one, the tile draws a small picture of its own. */
visual?: ReactNode;
}
export interface FeatureGridProps {
eyebrow?: ReactNode;
title?: ReactNode;
description?: ReactNode;
features: Feature[];
className?: string;
}
const stage = "relative flex h-(--tui-feature-grid-stage-height) items-center justify-center overflow-hidden rounded-[calc(var(--tui-feature-grid-radius)_-_6px)] bg-(--tui-feature-grid-mark-bg)";
const line = "h-2 rounded-full bg-[color-mix(in_srgb,var(--tui-feature-grid-fg)_12%,transparent)]";
/* Four small pictures, each made of plain boxes, so a tile has something to look at before anyone supplies an image. */
const PICTURES: ReactNode[] = [
// A rising set of bars, the last one in the accent.
<div key="bars" className="flex h-24 items-end gap-2.5">
{[34, 48, 40, 62, 55, 74, 68, 88, 100].map((height, i, all) => (
<span key={i} style={{ height: `${height}%` }} className={`w-5 rounded-t-md ${i === all.length - 1 ? "bg-(--tui-feature-grid-accent)" : "bg-[color-mix(in_srgb,var(--tui-feature-grid-fg)_14%,transparent)]"}`} />
))}
</div>,
// A few people and how many more.
<div key="people" className="flex items-center">
{["AK", "JM", "SR", "LD"].map((initials, i) => (
<span key={initials} className="-ml-2.5 flex size-11 items-center justify-center rounded-full bg-(--tui-feature-grid-card-bg) text-[12px] font-semibold first:ml-0 [box-shadow:0_0_0_3px_var(--tui-feature-grid-mark-bg),0_2px_6px_rgb(0_0_0_/_0.1)]" style={{ zIndex: 4 - i }}>
{initials}
</span>
))}
<span className="-ml-2.5 flex h-11 items-center rounded-full bg-(--tui-feature-grid-accent) px-3.5 text-[12px] font-semibold text-(--tui-feature-grid-card-bg) [box-shadow:0_0_0_3px_var(--tui-feature-grid-mark-bg)]">+12</span>
</div>,
// Three settings, one of them on.
<div key="settings" className="flex w-[70%] flex-col gap-2.5">
{[true, false, true].map((on, i) => (
<div key={i} className="flex items-center justify-between rounded-xl bg-(--tui-feature-grid-card-bg) px-3 py-2.5 [box-shadow:0_1px_2px_rgb(0_0_0_/_0.06)]">
<span className={`${line} ${i === 1 ? "w-16" : "w-24"}`} />
<span className={`flex h-5 w-9 items-center rounded-full p-0.5 ${on ? "justify-end bg-(--tui-feature-grid-accent)" : "bg-[color-mix(in_srgb,var(--tui-feature-grid-fg)_16%,transparent)]"}`}>
<span className="size-4 rounded-full bg-(--tui-feature-grid-card-bg) shadow-sm" />
</span>
</div>
))}
</div>,
// A window with a few lines in it, one picked out.
<div key="window" className="flex w-[78%] flex-col gap-2.5 rounded-xl bg-(--tui-feature-grid-card-bg) p-4 [box-shadow:0_1px_2px_rgb(0_0_0_/_0.06),0_12px_24px_-16px_rgb(0_0_0_/_0.25)]">
<div className="mb-1 flex gap-1.5">
{[0, 1, 2].map((dot) => (
<span key={dot} className="size-2 rounded-full bg-[color-mix(in_srgb,var(--tui-feature-grid-fg)_18%,transparent)]" />
))}
</div>
<span className={`${line} w-[72%]`} />
<span className="h-2 w-[46%] rounded-full bg-(--tui-feature-grid-accent)" />
<span className={`${line} w-[60%]`} />
<span className={`${line} w-[34%]`} />
</div>,
];
/** How wide each tile is, out of six, so the grid reads as wide, narrow, narrow, wide. */
const SPAN = ["lg:col-span-4", "lg:col-span-2", "lg:col-span-2", "lg:col-span-4"];
/**
* What something does, as a grid of tiles of unequal size. Each tile has a
* picture in its top and a heading and a line under it. The pictures are
* drawn from the section's own colours until real ones are supplied.
*/
export function FeatureGrid({ eyebrow, title, description, features, className }: FeatureGridProps) {
return (
<section data-slot="feature-grid" className={["flex w-full flex-col items-center gap-(--tui-feature-grid-gap) bg-(--tui-feature-grid-bg) px-6 py-(--tui-feature-grid-padding-y) font-(family-name:--tui-font) text-(--tui-feature-grid-fg)", className].filter(Boolean).join(" ")}>
{(eyebrow || title || description) && (
<header className="flex max-w-[44rem] flex-col items-center gap-4 text-center">
{eyebrow && <span className="inline-flex h-7 items-center rounded-full px-3 text-[12px] font-semibold tracking-[0.08em] text-(--tui-feature-grid-accent) uppercase [box-shadow:inset_0_0_0_1px_var(--tui-feature-grid-border)]">{eyebrow}</span>}
{title && <h2 className="m-0 text-(length:--tui-feature-grid-title-size) leading-[1.08] font-semibold tracking-[-0.03em] text-balance">{title}</h2>}
{description && <p className="m-0 text-[17px] leading-relaxed text-pretty text-(--tui-feature-grid-muted)">{description}</p>}
</header>
)}
<ul className="m-0 grid w-full max-w-(--tui-feature-grid-width) list-none grid-cols-1 gap-(--tui-feature-grid-card-gap) p-0 sm:grid-cols-2 lg:grid-cols-6">
{features.map((feature, i) => (
<li
key={feature.title}
data-slot="feature-card"
className={`flex flex-col gap-5 rounded-(--tui-feature-grid-radius) bg-(--tui-feature-grid-card-bg) p-1.5 transition-[translate,box-shadow] duration-300 [box-shadow:inset_0_0_0_var(--tui-feature-grid-border-width)_var(--tui-feature-grid-border),0_1px_2px_rgb(0_0_0_/_0.04)] hover:-translate-y-0.5 hover:shadow-(--tui-feature-grid-hover-shadow) ${SPAN[i % SPAN.length]}`}
>
<div aria-hidden={!feature.visual} className={stage}>
{feature.visual ?? PICTURES[i % PICTURES.length]}
</div>
<div className="flex flex-col gap-2 px-(--tui-feature-grid-padding) pb-(--tui-feature-grid-padding)">
<h3 className="m-0 text-[18px] leading-snug font-semibold tracking-[-0.01em]">{feature.title}</h3>
<p className="m-0 text-[15px] leading-relaxed text-(--tui-feature-grid-muted)">{feature.body}</p>
</div>
</li>
))}
</ul>
</section>
);
}:root {
color-scheme: light;
--tui-bg: #ffffff;
--tui-fg: #0a0a0a;
--tui-muted: #f5f5f5;
--tui-muted-fg: #737373;
--tui-border: #e5e5e5;
--tui-primary: #171717;
--tui-primary-fg: #fafafa;
--tui-ring: #a3a3a3;
--tui-success: #16a34a;
--tui-warning: #d97706;
--tui-danger: #dc2626;
--tui-radius: 8px;
--tui-border-width: 1px;
--tui-space: 4px;
--tui-shadow: none;
--tui-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--tui-text: 14px;
--tui-duration: 150ms;
/* Feature grid */
--tui-feature-grid-padding-y: calc(var(--tui-space) * 20);
--tui-feature-grid-gap: calc(var(--tui-space) * 12);
--tui-feature-grid-card-gap: calc(var(--tui-space) * 5);
--tui-feature-grid-width: 1040px;
--tui-feature-grid-padding: calc(var(--tui-space) * 7);
--tui-feature-grid-stage-height: calc(var(--tui-space) * 40);
--tui-feature-grid-radius: calc(var(--tui-radius) * 2.5);
--tui-feature-grid-border-width: var(--tui-border-width);
--tui-feature-grid-hover-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
--tui-feature-grid-title-size: 40px;
--tui-feature-grid-bg: var(--tui-bg);
--tui-feature-grid-card-bg: var(--tui-bg);
--tui-feature-grid-fg: var(--tui-fg);
--tui-feature-grid-muted: var(--tui-muted-fg);
--tui-feature-grid-border: var(--tui-border);
--tui-feature-grid-mark-bg: var(--tui-muted);
--tui-feature-grid-accent: var(--tui-primary);
}
.dark,
[data-theme="dark"] {
color-scheme: dark;
--tui-bg: #0a0a0a;
--tui-fg: #fafafa;
--tui-muted: #1c1c1c;
--tui-muted-fg: #a3a3a3;
--tui-border: #2a2a2a;
--tui-primary: #fafafa;
--tui-primary-fg: #171717;
--tui-ring: #737373;
--tui-success: #22c55e;
--tui-warning: #f59e0b;
--tui-danger: #ef4444;
}Needs Tailwind CSS v4. Import the stylesheet once, after Tailwind.