Next.js Radix UI 示例详解:在 App Router 中构建可访问下拉菜单的完整实战
本篇指南基于 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.Root、DropdownMenu.Trigger等);@radix-ui/react-icons(1.3.0):与组件配套的 SVG 图标(本例使用HamburgerMenuIcon、DotFilledIcon、CheckIcon、ChevronRightIcon);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 注册了 tailwindcss 与 autoprefixer 两个插件,使 Next.js 在编译 CSS 时能扫描并展开 Tailwind 类名:
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
tailwind.config.js 的关键是 content 字段——Tailwind 只从 App Router 目录中扫描类名,这正是页面中 bg-gradient-to-r、hover: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(如CheckIcon、DotFilledIcon),把"选中指示器"的显隐逻辑交给 Radix 内部状态机,页面代码无需手动判断;Separator:DropdownMenu.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 / onCheckedChange 与 value / 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>
...
asChild 让 Trigger 不额外渲染一层 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 不负责样式" 的定位。
菜单主体依次组织了以下原语:
-
普通项与禁用项:
DropdownMenuItem(含⌘+T、⌘+N快捷键槽位),以及一个disabled的 "New Private Window"; -
子菜单:
DropdownMenu.Sub+SubTrigger+SubContent,子面板使用sideOffset={2}与alignOffset={-5}微调相对父菜单的弹出位置,内部再嵌套 "Save Page As…"、"Create Shortcut…" 等项及一条Separator; -
复选项:
<DropdownMenuCheckboxItem checked={bookmarksChecked} onCheckedChange={setBookmarksChecked} > <DropdownMenuItemIndicator> <CheckIcon /> </DropdownMenuItemIndicator> Show Bookmarks <RightSlot>⌘+B</RightSlot> </DropdownMenuCheckboxItem>勾选后的
CheckIcon由ItemIndicator自动显隐,状态变更通过onCheckedChange回写useState; -
标签与单选组:
<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>RadioGroup以value/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 类名"这一套模式即可平滑迁移。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00