Base UI 无样式组件 + Vite + 原生 CSS:从 vite-css 示例看完整接入流程 📅 发布时间:2026/9/15 22:53:33 👁 浏览次数: Base UI 无样式组件 Vite 原生 CSS从 vite-css 示例看完整接入流程【免费下载链接】base-uiUnstyled UI components for building accessible web apps and design systems. From the creators of Radix, Floating UI, and Material UI.项目地址: https://gitcode.com/GitHub_Trending/ba/base-uiBase UI 是一套无样式unstyled的 React UI 组件库其核心思想是把交互逻辑、可访问性与视觉呈现彻底解耦让开发者用自己熟悉的技术栈Tailwind、CSS Modules、纯 CSS 乃至任意预处理器完成外观设计。仓库中的examples/vite-css示例正是一个以原生 CSS CSS Modules为样式方案的 Vite 接入样板它以最小的工程配置演示了如何在 Vite TypeScript 项目中安装 Base UI、启动带 HMR 的开发服务器、通过data-*状态属性为组件编写纯 CSS 样式并跑通生产构建与 ESLint 检查。读完本文你将掌握 Base UI 在 Vite 场景下的完整接入路径并理解无样式组件的样式挂载点与底层实现原理可直接迁移到自己的项目中使用。示例定位一份最小可运行的 Vite 接入样板示例根目录的 README.md 对自身的定位描述得非常简洁This template provides a minimal setup to get React Base UI (styled with plain CSS) working in Vite with HMR and some ESLint rules.翻译过来它要解决的三个核心诉求是React Base UI 能跑起来依赖base-ui/react以latest版本引入见 package.json样式用纯 CSS 完成不引入任何 UI 框架或原子化 CSS 库通过原生 CSS 与 CSS Modules 实现全部视觉效果开发体验完整开箱即用的 Vite HMR热模块替换与 ESLint 规则集。整个示例的源码结构非常精简是典型的 Vite React TS 脚手架布局examples/vite-css/ ├── index.html ├── package.json ├── vite.config.ts ├── eslint.config.js ├── tsconfig.json / tsconfig.app.json / tsconfig.node.json └── src/ ├── main.tsx # React 入口挂载 App / ├── App.tsx # 页面组件演示 Switch 的受控用法 ├── App.css / index.css ├── assets/ # 三枚 Logo 图Vite / React / Base UI └── widgets/ ├── Switch.tsx # 包装后的 Base UI Switch 组件 └── Switch.module.css # 该组件的纯 CSS 样式其中src/widgets/Switch.tsx是全文唯一真正使用 Base UI 组件的地方也是理解无样式组件 自定义样式这套模式的最佳范本。环境准备Node.js 与包管理器要求原文档在 Prerequisites 一节明确了运行前提Node.js需要 20.19 或 22.12Vite 8 与较新的工具链对运行时版本有要求过旧的 Node 版本无法启动包管理器示例以pnpm为准同样可以使用 npm / yarn。需要说明的是本仓库是一个 pnpm workspace 单仓库见根目录 pnpm-workspace.yamlexamples/vite-css是其中的一个私有private: true示例包。因此在仓库根目录执行pnpm install时依赖会按 workspace 统一解析如果把这个目录单独复制出去使用同样可以用pnpm install安装其独立依赖。四步跑通安装、开发、构建、预览原文档给出了从零到预览的完整命令链下面结合 package.json 中的 scripts 逐一展开命令对应的 script作用pnpm install—安装全部依赖pnpm devvite启动 Vite 开发服务器默认监听http://localhost:5173支持 HMRpnpm buildtsc -b vite build先做 TypeScript 工程编译检查再执行生产构建pnpm previewvite preview本地预览生产构建产物需先执行过pnpm buildpnpm linteslint .对整个工程执行 ESLint 检查1. 安装依赖pnpm install依赖清单来源package.jsondependenciesbase-ui/reactlatest、clsx用于条件拼接 className、react/react-dom^19devDependenciesvite^8、vitejs/plugin-react、typescript^5.9、eslint^10及typescript-eslint、eslint-plugin-react-hooks、eslint-plugin-react-refresh等。2. 启动开发服务器pnpm devVite 会启动开发服务器并在浏览器打开http://localhost:5173。得益于 Vite 的 HMR修改src/App.tsx保存后页面会即时热更新——示例页面上的提示文案 Editsrc/App.tsxand save to test HMR 就是在引导你验证这一体验。3. 生产构建pnpm build注意这里的构建脚本是tsc -b vite build它比单纯的vite build多了一步 TypeScript 工程引用编译先通过tsc -b对 tsconfig.json 中引用的tsconfig.app.json与tsconfig.node.json两个子工程做增量类型检查再交给 Vite 打包从而保证产物在类型层面是安全的。4. 预览生产构建pnpm previewvite preview会在本地起一个静态服务器用于预览dist产物验证生产构建效果。原文档特别提醒请先执行pnpm build再执行pnpm preview否则没有可预览的构建产物。5. 代码检查pnpm lint对应eslint .使用扁平配置flat config核心规则来自eslint/js的 recommended 集、typescript-eslint的 recommended 集以及 React Hooks 规则与react-refresh/only-export-components对仅导出常量的文件放行详细规则可见 eslint.config.js。工程骨架与关键配置解读Vite 配置极简vite.config.ts 只有一个 React 插件没有任何额外配置import { defineConfig } from vite; import react from vitejs/plugin-react; export default defineConfig({ plugins: [react()], });这说明 Base UI 是无样式组件不要求任何编译期插件或 CSS 预处理钩子接入成本趋近于零。TypeScript 配置严格模式根 tsconfig.json 采用 project references 结构分别指向tsconfig.app.json应用代码与tsconfig.node.jsonVite 配置等 Node 侧代码。其中 tsconfig.app.json 启用了strict、noUnusedLocals、noUnusedParameters、erasableSyntaxOnly、noFallthroughCasesInSwitch等严格选项并使用moduleResolution: bundler与jsx: react-jsx与 Vite 的打包模型完全匹配。样式基调CSS 变量 明暗主题src/index.css 定义了全站的 CSS 自定义属性CSS Variables用oklch()颜色函数组织了一套灰度色阶--color-gray-50到--color-gray-950与强调色--color-blue、--color-red并通过prefers-color-scheme媒体查询在亮色/暗色模式下切换同一组变量的取值。这套 token 体系随后被 Switch 组件的样式直接消费——这正是无样式组件 自建设计系统的典型做法设计 token 由应用层持有组件库完全不感知。核心示例深读用纯 CSS 为无样式 Switch 写样式1. 包装组件逻辑透传样式自持src/widgets/Switch.tsx 只有十余行是一个典型的薄包装组件import { Switch as BaseSwitch } from base-ui/react/switch; import clsx from clsx; import styles from ./Switch.module.css; export function Switch(props: SwitchProps) { return ( BaseSwitch.Root {...props} className{clsx(styles.Switch, props.className)} BaseSwitch.Thumb className{styles.Thumb} / /BaseSwitch.Root ); } export type SwitchProps BaseSwitch.Root.Props;几个值得注意的设计细节复合组件Compound Component结构BaseSwitch.Root与BaseSwitch.Thumb是独立的子部件应用层负责把它们组合成最终形态——在这里只有Root轨道Thumb滑块props 全量透传{...props}把受控/非受控的checked、defaultChecked、onCheckedChange、disabled等全部转发给Root包装层不拦截任何逻辑clsx拼接 className既允许调用方通过className继续追加外部样式又保证内部样式类styles.Switch生效类型复用export type SwitchProps BaseSwitch.Root.Props让包装组件的类型签名与 Base UI 原生 API 完全一致无需手写类型。2. 样式文件状态数据属性驱动外观src/widgets/Switch.module.css 是全文的视觉核心它演示了 Base UI 最重要的样式约定组件状态通过data-*数据属性暴露到 DOM 上CSS 只需针对这些属性编写规则。轨道.Switch的样式要点.Switch { position: relative; display: flex; appearance: none; border: 0; margin: 0; padding: 1px; width: 2.5rem; height: 1.5rem; border-radius: 1.5rem; background-image: linear-gradient(to right, var(--color-gray-700) 35%, var(--color-gray-200) 65%); background-size: 6.5rem 100%; background-position-x: 100%; transition-property: background-position, box-shadow; transition-timing-function: cubic-bezier(0.26, 0.75, 0.38, 0.45); transition-duration: 125ms; }这里没有使用任何 JS 参与动画轨道通过一个6.5rem宽的渐变背景是组件宽度2.5rem的约 2.6 倍配合background-position-x位移来模拟填充效果。而状态切换完全由数据属性驱动[data-checked] { background-position-x: 0%; } [data-checked]:active { background-color: var(--color-gray-500); }滑块.Thumb同样依赖data-checked完成位移.Thumb { aspect-ratio: 1 / 1; height: 100%; border-radius: 100%; background-color: white; transition: translate 150ms ease; [data-checked] { translate: 1rem 0; } }此外样式还覆盖了完整的交互态细节焦点可见性:focus-visible时通过::before伪元素绘制 2px 外环outline: 2px solid var(--color-blue)保证键盘用户有清晰的焦点指示明暗主题适配media (prefers-color-scheme: light)与dark两套分支分别调整内阴影、描边色与渐变配色嵌套语法使用了 CSS 嵌套Native Nesting这在现代浏览器与 Vite 8 的默认 CSS 处理链路中已可直接使用。3. 使用方视角受控回调src/App.tsx 中的用法展示了受控模式的回调接入const [count, setCount] React.useState(0); Switch onCheckedChange{() setCount((c) c 1)} / pFlicked the Switch {count} time{count ! 1 ? s : }./p每次拨动开关onCheckedChange被触发计数递增——逻辑层React 状态与视觉层CSS Modules互不耦合。源码印证Switch 的无样式底层实现深入到组件库源码可以验证示例中的样式挂载点从何而来。base-ui/react的 Switch 实现位于 packages/react/src/switch/root/SwitchRoot.tsx其 JSDoc 明确说明Renders aspanelement and a hiddeninputbeside.即默认渲染为一个roleswitch的spanSwitchRoot.tsx旁边跟随一个视觉隐藏的input typecheckboxSwitchRoot.tsx。这种可见节点 隐藏原生控件的结构是 Base UI 保证可访问性的基础屏幕阅读器读到的是带roleswitch、aria-checked、aria-labelledby的语义节点表单提交则依赖隐藏 input。核心交互逻辑集中在 input 的onChangeSwitchRoot.tsxconst nextChecked event.currentTarget.checked; const eventDetails createChangeEventDetails(REASONS.none, event.nativeEvent); onCheckedChange?.(nextChecked, eventDetails); if (eventDetails.isCanceled) { return; } setCheckedState(nextChecked);也就是说状态更新以隐藏 input 的 change 事件为唯一事实来源回调onCheckedChange拿到(nextChecked, eventDetails)其中eventDetails支持.cancel()取消本次状态变更——这一点在 SwitchRoot.test.tsx 中有对应测试用例should report keyboard modifier event properties、eventDetails.cancel() 等场景。而示例样式依赖的data-checked属性同样有测试背书测试断言 Switch 根节点与 Thumb 节点在选中时都携带data-checked属性、未选中时不存在该属性SwitchRoot.test.tsx。这就解释了为什么.Switch[data-checked]与.Thumb[data-checked]的 CSS 规则能在运行时精确命中状态——状态数据属性是 Base UI 组件对外暴露样式挂载点的官方约定。从示例到实战把这套模式迁移到自己的项目结合本示例可以提炼出一套可复制的 Base UI 样式化工作流保持薄包装像widgets/Switch.tsx一样用一层薄组件透传 Base UI 的 props只做组合RootThumb等与 className 注入不改动任何交互逻辑样式只认数据属性不要在组件里写我处于选中态的 JS 分支而是直接编写[data-checked]、[data-disabled]、[data-highlighted]等 CSS 规则——组件状态变化会自动同步到 DOM 属性上把设计 token 放应用层参考 src/index.css 的做法用 CSS 变量集中管理色板、间距、圆角组件样式消费变量换主题只改 token覆盖完整交互态示例中的:focus-visible焦点环、prefers-color-scheme明暗适配、transition动效都是无障碍与观感层面值得完整保留的细节类型随组件走导出ComponentProps BaseUI.X.Props这样的别名让包装组件的 TypeScript 类型与 Base UI 官方 API 保持同步更新。这套模式不限于 SwitchBase UI 的菜单、Dialog、Select、Tooltip 等组件同样遵循复合部件 数据属性状态 零内联样式的约定vite-css 示例正是进入这套体系最轻量的一扇门。【免费下载链接】base-uiUnstyled UI components for building accessible web apps and design systems. From the creators of Radix, Floating UI, and Material UI.项目地址: https://gitcode.com/GitHub_Trending/ba/base-ui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考