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
| Prop | Type | Default | Description |
|---|---|---|---|
| columns | 2 | 3 | 4 | 3 | Columns from sm up on HairlineGrid. Always one column below sm. |
| equalRows | boolean | false | Give 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