首页
/ Next.js Radix UI 示例详解:在 App Router 中构建可访问下拉菜单的完整实战

Next.js Radix UI 示例详解:在 App Router 中构建可访问下拉菜单的完整实战

2026-09-06 17:51:20作者:蔡怀权

本篇指南基于 Next.js 仓库中的 examples/radix-ui 官方示例,完整讲解如何在 Next.js(App Router)中引入 Radix UI 无样式可访问组件库:包括通过 create-next-app 一键引导项目、依赖与 Tailwind CSS 配置的组织方式,以及 page.tsx 中 DropdownMenu 下拉菜单的逐块实现细节。读完之后,你将能够独立搭建一个基于 Radix UI + Tailwind CSS 的客户端交互页面,并理解其受控状态、子菜单与无障碍属性等关键机制。

示例定位与官方说明

该示例位于 examples/radix-ui/README.md,其官方说明如下:

This example showcases a few basic Radix UI components

即:演示若干基础 Radix UI 组件与 Next.js 的集成方式。从示例的实际代码来看,它聚焦于 @radix-ui/react-dropdown-menu(下拉菜单)这一组件族,覆盖普通菜单项、禁用项、子菜单(Sub)、复选项(Checkbox)、单选组(RadioGroup)、分隔线与标签等完整能力,是理解 Radix UI "无样式原语 + 自行用 CSS 定制" 设计哲学的典型工程样本。

一键引导项目

按照 README 的 "How to use" 部分,使用 create-next-app 并指定 --example radix-ui 参数,即可通过 npm、Yarn 或 pnpm 三种包管理器拉取该示例并生成同名项目目录:

npx create-next-app --example radix-ui radix-ui-app
yarn create next-app --example radix-ui radix-ui-app
pnpm create next-app --example radix-ui radix-ui-app

三种命令的区别仅在于所用的包管理器;--example radix-ui 决定了从示例仓库克隆 radix-ui 模板,第二个参数 radix-ui-app 是本地新建的目录名。README 同时提供了"Deploy with Vercel"的部署入口,可直接将该项目部署到 Vercel 云端(对应仓库中的 vercel.json 根配置体系)。

项目结构与依赖

示例目录结构非常精简,全部可交互逻辑集中在 App Router 页面中:

examples/radix-ui/
├── app/
│   ├── layout.tsx        # 根布局,引入全局样式
│   └── page.tsx          # 客户端组件:DropdownMenu 主页面
├── styles/
│   └── globals.css       # Tailwind 指令
├── package.json
├── postcss.config.js
├── tailwind.config.js
└── tsconfig.json

依赖清单(package.json

{
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "@radix-ui/react-dropdown-menu": "2.1.1",
    "@radix-ui/react-icons": "1.3.0",
    "next": "latest",
    "react": "latest",
    "react-dom": "latest"
  },
  "devDependencies": {
    "@types/node": "^22.2.0",
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "autoprefixer": "10.4.20",
    "postcss": "8.4.41",
    "tailwindcss": "3.4.9",
    "typescript": "5.5.4"
  }
}

运行时依赖中只有三个与 Radix 集成直接相关:

  • @radix-ui/react-dropdown-menu(精确锁定 2.1.1):下拉菜单的原语组件集合,以命名空间形式导出(DropdownMenu.RootDropdownMenu.Trigger 等);
  • @radix-ui/react-icons(1.3.0):与组件配套的 SVG 图标(本例使用 HamburgerMenuIconDotFilledIconCheckIconChevronRightIcon);
  • next / react / react-dom 均使用 latest,即跟随 Next.js 主干版本。

开发依赖体现样式链路:Tailwind CSS 3.4.9 + PostCSS 8.4.41 + Autoprefixer 10.4.20,配合 TypeScript 5.5.4 做类型检查。

根布局与全局样式

app/layout.tsx 是标准的 App Router 根布局,唯一职责是把全局样式注入并渲染 <html>/<body>

import "../styles/globals.css";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
  }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

注意此处没有 "use client" 指令——根布局保持为服务端组件,而样式定制完全下放到客户端页面。

样式入口 styles/globals.css 只有三行 Tailwind 指令:

@tailwind base;
@tailwind components;
@tailwind utilities;

postcss.config.js 注册了 tailwindcssautoprefixer 两个插件,使 Next.js 在编译 CSS 时能扫描并展开 Tailwind 类名:

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

tailwind.config.js 的关键是 content 字段——Tailwind 只从 App Router 目录中扫描类名,这正是页面中 bg-gradient-to-rhover:bg-gray-700 等原子类能被正确生成的前提:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./app/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

tsconfig.json 采用 Next.js 生成的标准配置:"target": "es5""jsx": "react-jsx""module": "esnext""moduleResolution": "node"include 覆盖 **/*.ts**/*.tsx,与示例中的 TSX 写法完全对齐。

核心实现:DropdownMenu 客户端页面

整个可交互功能全部位于 app/page.tsx,这是理解本示例价值的关键文件。

客户端边界与组件组合

文件首行声明 "use client",把该页面标记为客户端组件(因为需要 useState 与 DOM 事件交互):

"use client";
import { useState } from "react";
import {
  HamburgerMenuIcon,
  DotFilledIcon,
  CheckIcon,
  ChevronRightIcon,
} from "@radix-ui/react-icons";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";

从源码结构看,页面先用若干带 Tailwind 类名的小组件对 Radix 原语做了一层轻量封装,再在 Home 中组合使用:

  • RightSlot:菜单项右侧槽位(显示快捷键提示,如 ⌘+T),利用 ml-auto 实现右对齐;
  • DropdownMenuItem:对 DropdownMenu.Item 的样式封装,并在 props.disabled 为真时追加 text-gray-500,直观体现了 Radix 原语"透传 props、样式自定"的特性;
  • DropdownMenuCheckboxItem:复选项的样式封装;
  • DropdownMenuItemIndicator:包裹 DropdownMenu.ItemIndicator,该原语只在项被选中/勾选时渲染其 children(如 CheckIconDotFilledIcon),把"选中指示器"的显隐逻辑交给 Radix 内部状态机,页面代码无需手动判断;
  • SeparatorDropdownMenu.Separator 的一像素分隔线。

受控状态

Home 组件用三个 useState 维护下拉菜单的受控状态:

export default function Home() {
  const [bookmarksChecked, setBookmarksChecked] = useState(true);
  const [urlsChecked, setUrlsChecked] = useState(false);
  const [person, setPerson] = useState("pedro");
  • 两个布尔值分别控制 "Show Bookmarks"(初始勾选)与 "Show Full URLs"(初始未勾选)两个复选项;
  • person 是单选组当前值,初始为 "pedro",可选值还有 "pablo"

这些状态通过 checked / onCheckedChangevalue / onValueChange 回接到 Radix 原语上,形成完整的受控闭环。

触发器:asChild 与原生 button

触发器部分展示了 Radix 最常用的 asChild 模式:

<DropdownMenu.Root>
  <DropdownMenu.Trigger
    asChild
    className="bg-white text-xs rounded-3xl flex items-center h-8 px-2 relative select-none"
  >
    <button
      aria-label="Customise options"
      className="h-8 w-8 inline-flex items-center justify-center shadow-lg"
    >
      <HamburgerMenuIcon />
    </button>
  </DropdownMenu.Trigger>
  ...

asChildTrigger 不额外渲染一层 DOM 元素,而是把事件处理与 ARIA 属性合并进其唯一的子元素(原生 <button>)。这里还保留了 aria-label="Customise options",说明图标按钮的无障碍名依赖开发者显式提供——这正是 Radix "行为原语自带可访问性、视觉层需自行负责" 设计哲学的体现。

菜单内容与定位参数

<DropdownMenu.Content
  sideOffset={5}
  className="bg-white rounded p-1 shadow-lg"
>

Content 通过 sideOffset={5} 指定弹出面板与触发器之间 5px 的偏移;背景、圆角、内边距与阴影全部交给 Tailwind 类名实现,印证了"Radix 不负责样式" 的定位。

菜单主体依次组织了以下原语:

  1. 普通项与禁用项DropdownMenuItem(含 ⌘+T⌘+N 快捷键槽位),以及一个 disabled 的 "New Private Window";

  2. 子菜单DropdownMenu.Sub + SubTrigger + SubContent,子面板使用 sideOffset={2}alignOffset={-5} 微调相对父菜单的弹出位置,内部再嵌套 "Save Page As…"、"Create Shortcut…" 等项及一条 Separator

  3. 复选项

    <DropdownMenuCheckboxItem
      checked={bookmarksChecked}
      onCheckedChange={setBookmarksChecked}
    >
      <DropdownMenuItemIndicator>
        <CheckIcon />
      </DropdownMenuItemIndicator>
      Show Bookmarks <RightSlot>⌘+B</RightSlot>
    </DropdownMenuCheckboxItem>
    

    勾选后的 CheckIconItemIndicator 自动显隐,状态变更通过 onCheckedChange 回写 useState

  4. 标签与单选组

    <DropdownMenu.Label className="pl-6 leading-6 text-xs text-gray-700">
      Contributors
    </DropdownMenu.Label>
    
    <DropdownMenu.RadioGroup value={person} onValueChange={setPerson}>
      <DropdownMenuRadioItem value="pedro">
        <DropdownMenuItemIndicator>
          <DotFilledIcon />
        </DropdownMenuItemIndicator>
        Pedro Sanchez
      </DropdownMenuRadioItem>
      <DropdownMenuRadioItem value="pablo">
        ...
      </DropdownMenuRadioItem>
    </DropdownMenu.RadioGroup>
    

    RadioGroupvalue/onValueChange 受控,当前选中项的 DotFilledIcon 同样经由 ItemIndicator 呈现。

整个组件树为 Root → Trigger + Content → (Item | Sub → SubContent → Item | CheckboxItem | Label | RadioGroup → RadioItem | Separator),是 @radix-ui/react-dropdown-menu 各原语组合的标准示范。

本地运行与验证

引导项目(或直接进入 examples/radix-ui)后,按 package.json 中的 scripts 运行:

npm run dev     # next dev,开发模式(Fast Refresh)
npm run build   # next build,生产构建
npm run start   # next start,启动生产服务器

启动后打开首页,点击居中的汉堡菜单按钮,即可验证:普通项、禁用项、二级子菜单、复选项勾选回写、单选组切换,以及 aria-label、焦点管理等行为。若只关心 Radix 集成是否成功,重点检查打开菜单时 Content 的弹出定位(sideOffset/alignOffset 是否生效)与 ItemIndicator 的显隐是否符合受控状态。

小结

examples/radix-ui 用最小依赖集合演示了一条清晰的集成路径:create-next-app --example radix-ui 引导项目 → App Router 客户端页面挂载 Radix 原语 → Tailwind CSS(经 PostCSS 链路)完成全部视觉定制。对需要在 Next.js 中构建下拉菜单、复选项、单选组等可访问交互组件的开发者而言,app/page.tsx 可直接作为可运行参考实现;对想定制其他 Radix 组件(如 Dialog、Popover)的场景,照搬"原语封装 + asChild + 受控状态 + Tailwind 类名"这一套模式即可平滑迁移。

登录后查看全文
热门项目推荐
相关项目推荐