跳至正文
组件

Grid Layout线条网格布局

用全宽横线、竖线容器与点阵分隔带,为内容建立清晰的秩序。

正在加载…

安装

pnpm dlx shadcn@latest add https://yiui.dev/r/grid-layout.json

用法

每个页面使用一个 GridContainer,在其中依次排列 GridSection 与 GridSeparator。相邻行只绘制底线,避免重复描边。

app/page.tsx
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 适合局部强调或区分不同类型的内容。

正在加载…

编号标签

label 在分隔带左侧显示一段等宽小字,后接虚线,可以和任意纹路组合;pattern="none" 只保留编号和虚线。

正在加载…

交点十字

crosshair 控制容器竖线与横线交点处的「+」:GridSeparator 默认关闭,传 crosshair 开启;GridSection 传 crosshair 会在底线两端绘制。也可以直接使用 GridCrosshair 原语,放在任何 position: relative 的容器的四个角。

正在加载…

给带自己边框的卡片放十字时传 anchor="border",十字会对齐卡片的边框而不是底线。

分栏

GridColumns 默认两栏,栏间是虚线,窄屏自动堆叠。要三栏就用 className 覆盖列数:sm:grid-cols-3。每栏的内边距自己写,一行一个 GridSection。

正在加载…

侧边编号栏

章节类页面可以用「左侧窄编号栏 + 右侧内容」的两栏结构:宽屏用竖线隔开,窄屏编号移到标题上方。一章一个 GridSection,相邻章节只靠底线分开。

正在加载…

API

五个组件都支持对应 HTML 元素的原生属性,包括 className、children、id 和无障碍属性。

GridContainer

GridContainer 没有自定义 Props;该组件继承原生元素的全部属性。

GridSection

GridSeparator

pattern 可选值:dots(默认)、ruler、hatch、grid、plus、wave、none。同时导出 gridSeparatorVariants,类型 GridSeparatorPattern。

GridColumns

GridColumns 没有自定义 Props;该组件继承原生元素的全部属性。

GridCrosshair

position 可选 top-left(默认)、top-right、bottom-left、bottom-right,同时导出 gridCrosshairVariants。

data-slot 与状态

组件data-slot其他属性
GridContainergrid-container—
GridSectiongrid-sectiondata-top-line(true / false)
GridSeparatorgrid-separatordata-pattern;内部还有 grid-separator-pattern、grid-separator-label
GridColumnsgrid-columns—
GridCrosshairgrid-crosshairdata-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 安装会一并写入该规则。

源码

components/ui/grid-layout/grid-layout.tsx
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 }