Skip to content
goodtabs UI
BrowseBrowse components

Hairline grid

A rounded frame whose cells share 1px hairlines, with no rounding inside.

npx shadcn@latest add https://ui.goodtabs.co/r/hairline-grid.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/hairline-grid

01Curated by hand
02Tokens only
03Both themes
04Keyboard first
05Static pages
06No tracking

Six cells in three columns, each with a mono number and a label.

Two columns, mixed content

Components60Every one checked in light and dark before it ships.
Install with one commandThe registry speaks shadcn, so npx shadcn add works in any project.
  • npx shadcn@latest add @goodtabs/hairline-grid
  • npx shadcn@latest add @goodtabs/stat-card
  • npx shadcn@latest add @goodtabs/copy-button

Equal rows with a stat, a flush image slot, a feature and a list.

Four columns of icons

Search
Command menu
Saved
New this week
Dark mode
Shortcuts
Tags
Fast

Eight small cells, icon and label, two columns on phones.

Usage

import { HairlineGrid, HairlineCell } from "@/components/ui/hairline-grid"

<HairlineGrid columns={3}>
  <HairlineCell>First</HairlineCell>
  <HairlineCell>Second</HairlineCell>
  <HairlineCell>Third</HairlineCell>
</HairlineGrid>

Props

PropTypeDefaultDescription
columns2 | 3 | 43Columns from sm up on HairlineGrid. Always one column below sm.
equalRowsbooleanfalseGive every row the height of the tallest one.
padding"none" | "sm" | "default" | "lg""default"Inner padding on HairlineCell: 0, 12, 16 or 24px.

Source

hairline-grid/hairline-grid.tsx
import * as React from "react";
import { cn } from "@/lib/utils";

type Columns = 2 | 3 | 4;

type HairlineGridProps = React.ComponentProps<"div"> & {
  /** Columns from the sm breakpoint up. One column below it. */
  columns?: Columns;
  /** Give every row the height of the tallest one. */
  equalRows?: boolean;
};

const COLUMNS: Record<Columns, string> = {
  2: "sm:grid-cols-2",
  3: "sm:grid-cols-2 lg:grid-cols-3",
  4: "sm:grid-cols-2 lg:grid-cols-4",
};

/**
 * One rounded frame whose cells share 1px hairlines, with no rounding inside.
 * Each cell draws its right and bottom edge; the grid hangs 1px over the frame so the outer edges hide.
 */
export function HairlineGrid({ columns = 3, equalRows = false, className, children, ...props }: HairlineGridProps) {
  return (
    <div
      data-slot="hairline-grid"
      data-columns={columns}
      className={cn("w-full overflow-hidden rounded-lg border bg-card text-card-foreground", className)}
      {...props}
    >
      <div className={cn("-mr-px -mb-px grid grid-cols-1", COLUMNS[columns], equalRows && "auto-rows-fr")}>
        {children}
      </div>
    </div>
  );
}

type HairlineCellProps = React.ComponentProps<"div"> & {
  /** Inner padding: none, 12px, 16px or 24px. */
  padding?: "none" | "sm" | "default" | "lg";
};

const PADDING = { none: "p-0", sm: "p-3", default: "p-4", lg: "p-6" } as const;

/** A cell. Put anything inside; it draws the hairlines itself. */
export function HairlineCell({ padding = "default", className, ...props }: HairlineCellProps) {
  return (
    <div
      data-slot="hairline-cell"
      className={cn("min-w-0 border-r border-b", PADDING[padding], className)}
      {...props}
    />
  );
}

For AI agents

Hand your agent this page, the markdown at /components/hairline-grid/prompt.md, or the Copy AI prompt button above.

When to use

  • Feature grids, stat strips and category tiles on marketing pages
  • Any set of equal items that should read as one surface, not many cards
  • Bento layouts where the lines, not shadows, separate the parts

When not to use

  • A single item: use a card
  • Items that need their own hover, link or shadow: use separate cards with gap
  • Tabular data with headers: use a table

Agent notes

  • Server component with no dependencies; cells are plain divs, so pass className for flex or text styles
  • Columns go 1, then 2 from sm, then the chosen count from lg; a partial last row is fine
  • Each cell draws its own right and bottom hairline and the grid hangs 1px over the frame, so never add borders to cells
  • Use padding="none" for images or anything that should run edge to edge