import {
GridColumns,
GridContainer,
GridSection,
GridSeparator,
} from "@/components/ui/grid-layout/grid-layout"
// 圆点取自 --flow-* 色调,与主题色无关,亮暗模式下都清楚
const PROJECTS = [
{ name: "线条网格", note: "页面骨架,一次到位。", tag: "UI", tone: "blue" },
{
name: "流程图示",
note: "会动的请求链路图。",
tag: "Diagram",
tone: "violet",
},
{ name: "实时时间", note: "时区、时差与表盘。", tag: "Time", tone: "amber" },
] as const
const NOTES = [
{ date: "2026.10", title: "为什么我用线条而不是卡片排版" },
{ date: "2026.08", title: "把博客改成纯静态导出的几个坑" },
]
/** 一屏作品集首页缩影:标题区、点阵与刻度尺分隔带、三栏项目与笔记列表,只用五个布局原语搭成。 */
export default function GridLayoutDemo() {
return (
<GridContainer className="bg-background [--grid-width:40rem]">
<GridSection className="px-4 py-4 sm:px-6 sm:py-5" crosshair>
<div className="flex items-center justify-between gap-3">
<p className="font-mono text-xs tracking-[0.08em] whitespace-nowrap text-muted-foreground">
TRAVINS / 2026
</p>
<span className="flex items-center gap-1.5 rounded-full border border-edge px-2 py-0.5 text-[11px] whitespace-nowrap text-muted-foreground">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-(--flow-green)"
/>
接受新项目
</span>
</div>
<h3 className="mt-3 text-2xl font-semibold tracking-tight">
让内容,落在线条之间。
</h3>
<p className="mt-1.5 text-sm leading-6 text-muted-foreground">
独立开发者,做界面与组件。
</p>
</GridSection>
<GridSeparator label="01 / PROJECTS" />
<GridSection>
<GridColumns className="sm:grid-cols-3">
{PROJECTS.map(({ name, note, tag, tone }) => (
<article
key={name}
className="flex flex-col gap-1 px-4 py-3.5 max-sm:flex-row max-sm:items-center max-sm:gap-3 max-sm:py-2.5 sm:px-5"
>
<p className="flex w-20 shrink-0 items-center gap-1.5 font-mono text-[11px] text-muted-foreground sm:w-auto">
<span
aria-hidden="true"
className="size-1.5 rounded-full"
style={{ backgroundColor: `var(--flow-${tone})` }}
/>
{tag}
</p>
<h4 className="text-sm font-medium">{name}</h4>
<p className="text-xs leading-5 text-muted-foreground max-sm:hidden">
{note}
</p>
</article>
))}
</GridColumns>
</GridSection>
<GridSeparator pattern="ruler" label="02 / NOTES" />
<GridSection className="divide-y divide-dashed divide-edge">
{NOTES.map(({ date, title }, index) => (
<div
key={title}
className={`flex items-baseline gap-4 px-4 py-2.5 sm:px-6 ${index > 0 ? "max-sm:hidden" : ""}`}
>
<time className="shrink-0 font-mono text-xs text-muted-foreground">
{date}
</time>
<span className="min-w-0 truncate text-sm">{title}</span>
</div>
))}
</GridSection>
</GridContainer>
)
}
安装
pnpm dlx shadcn@latest add https://yiui.dev/r/grid-layout.json用法
每个页面使用一个 GridContainer,在其中依次排列 GridSection 与 GridSeparator。相邻行只绘制底线,避免重复描边。
import {
GridContainer,
GridSection,
GridSeparator,
GridColumns,
} from "@/components/ui/grid-layout/grid-layout"
export default function Page() {
return (
<GridContainer>
<GridSection className="px-4 py-6">个人介绍</GridSection>
<GridSeparator />
<GridSection>
<GridColumns>
<div className="p-4">项目</div>
<div className="p-4">文章</div>
</GridColumns>
</GridSection>
</GridContainer>
)
}GridSection 默认只画底部横线;需要给第一行补一条顶部横线时传 topLine。
示例
分隔带纹路
GridSeparator 用 pattern 切换纹路。页面里的主分隔用默认的 dots(点阵);ruler、hatch、grid、plus、wave 适合局部强调或区分不同类型的内容。
import {
GridContainer,
GridSection,
GridSeparator,
type GridSeparatorPattern,
} from "@/components/ui/grid-layout/grid-layout"
const patterns: { pattern: GridSeparatorPattern; note: string }[] = [
{ pattern: "dots", note: "点阵 · 默认" },
{ pattern: "ruler", note: "刻度尺" },
{ pattern: "hatch", note: "斜纹 · 局部强调" },
{ pattern: "grid", note: "坐标细网格" },
{ pattern: "plus", note: "十字阵列" },
{ pattern: "wave", note: "信号波形" },
]
export default function GridSeparatorPatternsDemo() {
return (
<GridContainer className="bg-background [--grid-width:32rem]">
{patterns.map(({ pattern, note }) => (
<div key={pattern}>
<GridSection className="flex items-baseline justify-between px-4 py-3">
<code className="font-mono text-xs">
pattern="{pattern}"
</code>
<span className="text-xs text-muted-foreground">{note}</span>
</GridSection>
<GridSeparator pattern={pattern} />
</div>
))}
</GridContainer>
)
}
编号标签
label 在分隔带左侧显示一段等宽小字,后接虚线,可以和任意纹路组合;pattern="none" 只保留编号和虚线。
import {
GridContainer,
GridSection,
GridSeparator,
} from "@/components/ui/grid-layout/grid-layout"
export default function GridSeparatorLabelDemo() {
return (
<GridContainer className="bg-background [--grid-width:32rem]">
<GridSection className="px-4 py-6">
<p className="text-sm text-muted-foreground">
给分隔带加一个编号,像图纸上的章节标记。
</p>
</GridSection>
<GridSeparator label="01 / OVERVIEW" />
<GridSection className="px-4 py-6">
<p className="text-sm text-muted-foreground">
编号可以和任意纹路组合。
</p>
</GridSection>
<GridSeparator pattern="hatch" label="02 / PROJECTS" />
<GridSection className="px-4 py-6">
<p className="text-sm text-muted-foreground">
只想要编号和虚线时用 none。
</p>
</GridSection>
<GridSeparator pattern="none" label="03 / NOTES" crosshair />
</GridContainer>
)
}
交点十字
crosshair 控制容器竖线与横线交点处的「+」:GridSeparator 默认关闭,传 crosshair 开启;GridSection 传 crosshair 会在底线两端绘制。也可以直接使用 GridCrosshair 原语,放在任何 position: relative 的容器的四个角。
import {
GridContainer,
GridCrosshair,
GridSection,
GridSeparator,
} from "@/components/ui/grid-layout/grid-layout"
export default function GridCrosshairDemo() {
return (
<GridContainer className="bg-background [--grid-width:32rem]">
{/* GridSection:在底线两端的交点画十字 */}
<GridSection className="px-4 py-6 sm:px-6" crosshair>
<p className="font-mono text-xs text-muted-foreground">
{"<GridSection crosshair />"}
</p>
</GridSection>
{/* GridSeparator:四个角都有十字 */}
<GridSeparator pattern="none" label="GridSeparator crosshair" crosshair />
{/* GridCrosshair:放在带 relative 的卡片四角,anchor="border" 对齐卡片自己的边框 */}
<GridSection className="px-4 py-8 sm:px-6">
<div className="relative border border-edge p-5">
<GridCrosshair position="top-left" anchor="border" />
<GridCrosshair position="top-right" anchor="border" />
<GridCrosshair position="bottom-left" anchor="border" />
<GridCrosshair position="bottom-right" anchor="border" />
<p className="font-medium">单独的十字</p>
<p className="mt-1 text-sm text-muted-foreground">
四个角各放一个 GridCrosshair,像图纸上的定位标记。
</p>
</div>
</GridSection>
</GridContainer>
)
}
给带自己边框的卡片放十字时传 anchor="border",十字会对齐卡片的边框而不是底线。
分栏
GridColumns 默认两栏,栏间是虚线,窄屏自动堆叠。要三栏就用 className 覆盖列数:sm:grid-cols-3。每栏的内边距自己写,一行一个 GridSection。
import {
GridColumns,
GridContainer,
GridSection,
} from "@/components/ui/grid-layout/grid-layout"
const stats = [
{ value: "5", label: "布局原语" },
{ value: "6", label: "分隔纹路" },
{ value: "14", label: "颜色主题" },
]
const cards = [
{ title: "窄屏堆叠", note: "默认两栏,窄屏自动变成一栏,栏间换成横向虚线。" },
{ title: "栏数可调", note: "用 className 改列数,比如三栏写 sm:grid-cols-3。" },
]
export default function GridColumnsDemo() {
return (
<GridContainer className="bg-background [--grid-width:32rem]">
{/* 三栏:覆盖默认的两列 */}
<GridSection>
<GridColumns className="sm:grid-cols-3">
{stats.map(({ value, label }) => (
<div key={label} className="p-4 sm:p-6">
<p className="font-mono text-2xl font-semibold tracking-tight">
{value}
</p>
<p className="mt-1 text-sm text-muted-foreground">{label}</p>
</div>
))}
</GridColumns>
</GridSection>
{/* 两栏:默认行为 */}
<GridSection>
<GridColumns>
{cards.map(({ title, note }) => (
<div key={title} className="p-4 sm:p-6">
<p className="font-medium">{title}</p>
<p className="mt-1 text-sm leading-6 text-muted-foreground">
{note}
</p>
</div>
))}
</GridColumns>
</GridSection>
</GridContainer>
)
}
侧边编号栏
章节类页面可以用「左侧窄编号栏 + 右侧内容」的两栏结构:宽屏用竖线隔开,窄屏编号移到标题上方。一章一个 GridSection,相邻章节只靠底线分开。
import {
GridContainer,
GridSection,
} from "@/components/ui/grid-layout/grid-layout"
const chapters = [
{
no: "01",
title: "起点",
body: "从一个只有首页的个人站开始,先把线条与留白定下来。",
},
{
no: "02",
title: "抽象",
body: "反复出现的结构收成五个原语,再往上搭组件与区块。",
},
{
no: "03",
title: "发布",
body: "整理成 registry,用一条命令装进任何 shadcn 项目。",
},
]
export default function GridSectionSidebarDemo() {
return (
<GridContainer className="bg-background [--grid-width:32rem]">
{chapters.map(({ no, title, body }) => (
<GridSection
key={no}
className="grid md:grid-cols-[9rem_1fr] md:divide-x md:divide-edge"
>
{/* 编号栏:窄屏移到标题上方,宽屏用竖线与内容隔开 */}
<div className="px-4 pt-4 sm:px-6 md:py-5">
<p className="font-mono text-xs tracking-[0.08em] text-muted-foreground">
{no}
</p>
<h4 className="mt-1 font-medium">{title}</h4>
</div>
<p className="px-4 pt-1 pb-4 text-sm leading-7 text-muted-foreground sm:px-6 md:py-5">
{body}
</p>
</GridSection>
))}
</GridContainer>
)
}
API
五个组件都支持对应 HTML 元素的原生属性,包括 className、children、id 和无障碍属性。
GridContainer
GridContainer 没有自定义 Props;该组件继承原生元素的全部属性。
GridSection
在行的顶部补一条全屏横线(默认只画底线)
类型boolean | undefinedfalse在底线与左右竖线的两个交点处绘制「+」
类型boolean | undefinedfalseGridSeparator
纹路:dots、ruler、hatch、grid、plus、wave,none 为只留线条
类型"dots" | "grid" | "hatch" | "none" | "plus" | "ruler" | "wave" | null | undefined"dots"左侧的等宽小字编号(如 02 / PROJECTS),像标签一样贴在纹路上;可与任意纹路组合
React.ReactNode是否在四个交点绘制十字
类型boolean | undefinedfalsepattern 可选值:dots(默认)、ruler、hatch、grid、plus、wave、none。同时导出 gridSeparatorVariants,类型 GridSeparatorPattern。
GridColumns
GridColumns 没有自定义 Props;该组件继承原生元素的全部属性。
GridCrosshair
十字所在的角:top-left、top-right、bottom-left、bottom-right
类型"bottom-left" | "bottom-right" | "top-left" | "top-right" | null | undefined"top-left"十字的定位基准。line:底线由 screen-line-bottom 画在元素内侧(GridSection / GridSeparator);border:元素自身有四边 border
类型"border" | "line" | null | undefined"line"position 可选 top-left(默认)、top-right、bottom-left、bottom-right,同时导出 gridCrosshairVariants。
data-slot 与状态
| 组件 | data-slot | 其他属性 |
|---|---|---|
GridContainer | grid-container | — |
GridSection | grid-section | data-top-line(true / false) |
GridSeparator | grid-separator | data-pattern;内部还有 grid-separator-pattern、grid-separator-label |
GridColumns | grid-columns | — |
GridCrosshair | grid-crosshair | data-position |
这些 data-slot 都可以作为父级选择器的锚点,例如 [&>[data-slot=grid-section]]:px-4。
可访问性
- 这些组件只提供容器,不添加
role,也不改变文档结构:GridSection渲染真正的<section>,GridContainer、GridSeparator、GridColumns渲染<div>。 - 行是页面骨架,语义需要自己补:给
GridSection加aria-label或配合aria-labelledby指向行内标题,读屏才会把这一行念成区域。原生属性会原样透传。 GridSeparator与GridCrosshair是纯装饰,自带aria-hidden,不会被读屏朗读,也不进入 Tab 顺序;纹路没有动画。label同样会被一并隐藏,需要读屏识别的章节名请写在行内的标题里。- 线条与文字的颜色都来自主题变量(
--edge、border-edge),在亮暗模式和 14 个颜色主题下对比度一致,不依赖颜色传达信息。
注意事项
容器宽度
通过父元素上的 --grid-width 变量调整宽度,默认 48rem。文档页面使用 80rem。GridColumns 在窄屏下堆叠,sm 起显示两列。
视口比容器窄时,GridContainer 两侧各留出一段空白(gutter):竖线向内缩,而全宽横线、分隔带仍延伸到屏幕边缘,内容像被框在线里。gutter 是两侧之和,窄屏默认 2rem、sm 起 3rem,容器达到最大宽度后自然居中,不再有空白。通过 --grid-gutter 变量覆盖,例如 className="[--grid-gutter:0px]" 让竖线贴到屏幕边缘;该变量同时作用于窄屏与 sm 以上。
主题与工具类
安装命令会注册 --edge 变量与 border-edge 主题映射,以及以下 Tailwind CSS v4 工具类:两个横线工具类 screen-line-top、screen-line-bottom;六个分隔带纹理工具类 pattern-ruler、pattern-hatch、pattern-dots、pattern-grid、pattern-plus、pattern-wave(供 GridSeparator 使用);两个面积底纹 bg-hatch、bg-dots,可直接铺在任意元素上。线条与纹路的颜色都来自 --edge / --foreground,随颜色主题和亮暗模式变化。
全宽横线需要页面的 html 设置 overflow-x: clip;Registry 安装会一并写入该规则。
源码
import type * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
// 像素约定:竖线是 GridContainer 的 border-x(位于内容区外 1px),
// 上方横线是上一行的底线(同样在外 1px),本行底线由 screen-line-bottom 画在内侧最后 1px。
// 十字为 11px 方框,线在第 5 px,所以用整数偏移让中心正好压在线条像素上,不用 translate(-50%)。
const gridCrosshairVariants = cva(
"pointer-events-none absolute z-10 size-[11px] before:absolute before:inset-y-0 before:left-[5px] before:w-px before:bg-foreground/45 after:absolute after:inset-x-0 after:top-[5px] after:h-px after:bg-foreground/45",
{
variants: {
/**
* 十字所在的角:top-left、top-right、bottom-left、bottom-right
* @default "top-left"
*/
position: {
"top-left": "-top-[6px] -left-[6px]",
"top-right": "-top-[6px] -right-[6px]",
"bottom-left": "-bottom-[5px] -left-[6px]",
"bottom-right": "-right-[6px] -bottom-[5px]",
},
/**
* 十字的定位基准。line:底线由 screen-line-bottom 画在元素内侧(GridSection / GridSeparator);border:元素自身有四边 border
* @default "line"
*/
anchor: { line: "", border: "" },
},
compoundVariants: [
{ anchor: "border", position: "bottom-left", className: "-bottom-[6px]" },
{
anchor: "border",
position: "bottom-right",
className: "-bottom-[6px]",
},
],
defaultVariants: { position: "top-left", anchor: "line" },
},
)
// 纹理层:高 31px(不覆盖底线),左端在左竖线外 4096px,平铺原点因此正好落在竖线上
const gridSeparatorVariants = cva(
"absolute top-0 bottom-px -left-[4097px] -right-[4096px]",
{
variants: {
pattern: {
ruler: "pattern-ruler",
hatch: "pattern-hatch",
dots: "pattern-dots",
grid: "pattern-grid",
plus: "pattern-plus",
wave: "pattern-wave",
none: "",
},
},
defaultVariants: { pattern: "dots" },
},
)
type GridSeparatorPattern = NonNullable<
VariantProps<typeof gridSeparatorVariants>["pattern"]
>
/**
* 交点十字:绘制在容器竖线与横线交点处的小「+」,纯装饰。
* 放在带 `position: relative` 的行(GridSection、GridSeparator)内,由 position 指定四个角之一。
*/
function GridCrosshair({
className,
position,
anchor,
...props
}: React.ComponentProps<"span"> & VariantProps<typeof gridCrosshairVariants>) {
return (
<span
aria-hidden
data-slot="grid-crosshair"
data-position={position ?? "top-left"}
className={cn(gridCrosshairVariants({ position, anchor }), className)}
{...props}
/>
)
}
/**
* 页面主容器:固定宽度 + 左右竖线。
* 宽度可通过 CSS 变量 --grid-width 覆盖(默认 48rem)。
* 外宽多 1px,使左右两条竖线的间距正好等于 --grid-width(纹路周期都能整除它)。
* 视口窄于容器时,两侧各留出 gutter:竖线向内缩,全宽横线仍延伸到屏幕边缘,形成「线框中的内容」。
* gutter 是两侧之和,用 CSS 变量 --grid-gutter 覆盖(默认窄屏 2rem、sm 起 3rem;设为 0px 则竖线贴边)。
*/
function GridContainer({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="grid-container"
className={cn(
"mx-auto w-[calc(100%-var(--grid-gutter,2rem))] max-w-[calc(var(--grid-width,48rem)+1px)] border-x border-edge sm:w-[calc(100%-var(--grid-gutter,3rem))]",
className,
)}
{...props}
/>
)
}
/**
* 一行内容:底部一条横跨整个屏幕的分隔线。
* 放在 GridContainer 内使用。
*/
function GridSection({
className,
topLine = false,
crosshair = false,
children,
...props
}: React.ComponentProps<"section"> & {
/**
* 在行的顶部补一条全屏横线(默认只画底线)
* @default false
*/
topLine?: boolean
/**
* 在底线与左右竖线的两个交点处绘制「+」
* @default false
*/
crosshair?: boolean
}) {
return (
<section
data-slot="grid-section"
data-top-line={topLine}
className={cn(
"screen-line-bottom",
topLine && "screen-line-top",
className,
)}
{...props}
>
{children}
{crosshair ? (
<>
<GridCrosshair position="bottom-left" />
<GridCrosshair position="bottom-right" />
</>
) : null}
</section>
)
}
/**
* 分隔带:全屏宽度的纹理 + 底部横线,用于主题切换处。
* 默认是点阵纹路;需要时可用 crosshair 在交点绘制「+」。
*/
function GridSeparator({
className,
pattern,
label,
crosshair,
...props
}: React.ComponentProps<"div"> &
VariantProps<typeof gridSeparatorVariants> & {
/**
* 纹路:dots、ruler、hatch、grid、plus、wave,none 为只留线条
* @default "dots"
*/
pattern?: GridSeparatorPattern | null
/** 左侧的等宽小字编号(如 `02 / PROJECTS`),像标签一样贴在纹路上;可与任意纹路组合 */
label?: React.ReactNode
/**
* 是否在四个交点绘制十字
* @default false
*/
crosshair?: boolean
}) {
const resolved = pattern ?? "dots"
const showCrosshair = crosshair ?? false
return (
<div
aria-hidden
data-slot="grid-separator"
data-pattern={resolved}
className={cn("screen-line-bottom h-8 w-full", className)}
{...props}
>
{resolved === "ruler" ? (
// 刻度尺带一层极淡的底色,让分隔带读作一条「带子」
<div className="absolute top-0 -right-[4096px] bottom-px -left-[4097px] bg-foreground/[0.025]" />
) : null}
<div
data-slot="grid-separator-pattern"
className={gridSeparatorVariants({ pattern: resolved })}
/>
{label ? (
<div
data-slot="grid-separator-label"
className="absolute inset-y-0 left-4 flex items-center sm:left-6"
>
<span className="bg-background px-1.5 font-mono text-[11px] leading-4 tracking-[0.08em] whitespace-nowrap text-muted-foreground">
{label}
</span>
</div>
) : null}
{showCrosshair ? (
<>
<GridCrosshair position="top-left" />
<GridCrosshair position="top-right" />
<GridCrosshair position="bottom-left" />
<GridCrosshair position="bottom-right" />
</>
) : null}
</div>
)
}
/**
* 分栏:栏与栏之间用虚线分隔,窄屏自动堆叠。
*/
function GridColumns({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="grid-columns"
className={cn(
"grid grid-cols-1 divide-y divide-dashed divide-edge sm:grid-cols-2 sm:divide-x sm:divide-y-0",
className,
)}
{...props}
/>
)
}
export {
GridColumns,
GridContainer,
GridCrosshair,
GridSection,
GridSeparator,
gridCrosshairVariants,
gridSeparatorVariants,
}
export type { GridSeparatorPattern }