Timace

Feature grid

What it does

Everything it needs, nothing it does not

Three things it does well, in the order people ask about them.

  • Fast to start

    Open it and begin. There is nothing to install and nothing to set up first.

  • Yours to change

    Every value is open, so it can look like your product and not like ours.

  • Leaves clean code

    What you copy out is the file that was running, with nothing generated around it.

  • Reads like a product

    The pictures in these tiles are drawn from your own colours until you swap in real ones.

components/ui/feature-grid.tsx
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>
  );
}
styles/timace-ui.css
: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.