近日,JavaScript Rising Stars 正式公布 2023 年 JavaScript 明星项目榜单,其中 shadcn/ui 位列榜首,2023 年获得了 39.5k Star。本文将深入探讨 shadcn/ui 是什么、使用方式、实现原理,它凭什么能够成为年度最火前端项目!
Shadcn UI 与其他 UI 和组件库如 Material UI、Ant Design、Element UI 的设计理念截然不同。这些库一般通过 npm 包提供对组件的访问,而 Shadcn UI 允许用户将单个 UI 组件的源代码直接下载到项目中,提供了更大的灵活性和定制空间。
按照 Shadcn UI 的说法,Shadcn UI 实际上并不是一个组件库,而是可以复制并粘贴到应用中的可重用组件的集合。
不到一年的时间,Shadcn UI 在 Github 上获得了超过 40k Star。
Shadcn UI 相比其他组件库提供了几个显著的优势,其中最突出的包括:
在决定是否在未来的项目中采用Shadcn UI之前,有几个关键因素值得考虑:
Shadcn UI 提供了很多功能,以增强用户体验。下面就来看看 Shadcn UI 中的几个主要的功能:主题和主题编辑器、暗黑模式、CLI和组件。
Shadcn UI 提供了精选的主题,可以轻松地将其复制并粘贴到应用程序中。可以选择通过代码库手动添加主题标记,或者使用 Shadcn UI 的主题编辑器进行更方便的操作。
主题编辑器允许配置各种属性,如颜色、边框半径和模式(明亮或暗黑)。此外,还可以选择两种样式:默认样式和纽约样式。每种样式都具有独特的组件、动画和图标。默认样式具有较大的输入字段、lucide-react图标和用于动画效果的tailwindcss-animate。而纽约样式则包括较小的按钮、带阴影的卡片和Radix图标。
使用Shadcn UI的图形界面,创建自定义主题也非常简单。编辑器会生成包含自定义样式定义的代码片段,只需将其复制粘贴到应用中即可。
shadcn-ui-theme-editor.gif下面是主题编辑器的代码输出示例,提供了浅色模式和深色模式的样式标记:
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.3rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%;
}
}
Shadcn UI 支持 Next.js 和 Vite 应用的暗黑模式。对于 Next.js 应用,Shadcn UI 使用next-themes
来实现暗黑模式切换功能。当用户在明亮模式和暗黑模式之间切换时,应用会在明亮和暗黑主题标记之间进行切换。
Shadcn UI 的 CLI 可以将库与应用集成,并添加依赖项以及应用相关的tailwind.config.js
配置。使用CLI还可以轻松地向应用程序添加UI组件。
可以选择手动从文档中复制和粘贴每个组件的代码,或者使用CLI进行添加。CLI提供了优秀的开发者体验,是使Shadcn UI更易于使用的一个功能。
截至目前,Shadcn UI 拥有 40 个组件,包括 Accordion(手风琴)、Skeleton(骨架屏)、Table(表格)和Popover(弹出框)等。通过利用 Shadcn UI 预构建的组件,可以节省时间,而不必从头开始构建组件。
下面来看看如何将 Shadcn UI 与 Next.js 集成。
首先,通过运行以下命令创建一个新的 Next.js 应用:
npx create-next-app@latest my-app --typescript --tailwind --eslint
接下来,运行 init 命令来初始化新项目的依赖项:
npx shadcn-ui@latest init
CLI 将提示进行一些配置。以下是配置问题的示例:
Would you like to use TypeScript (recommended)? no / yes
Which style would you like to use? › Default
Which color would you like to use as base color? › Slate
Where is your global CSS file? › › app/globals.css
Do you want to use CSS variables for colors? › no / yes
Where is your tailwind.config.js located? › tailwind.config.js
Configure the import alias for components: › @/components
Configure the import alias for utils: › @/lib/utils
Are you using React Server Components? › no / yes
现在就可以在应用中添加组件了,下面就来添加一个按钮组件。
可以运行以下命令以使用 CLI 添加一个按钮:
npx shadcn-ui@latest add button
CLI 会自动创建一个组件文件夹,只需要从文件夹中导出它:
import { Button } from "@/components/ui/button"
按钮组件的 variant 属性有六种值:default、destructive、outline、secondary、ghost、link。
Shadcn UI 在表单方面不仅提供了 Input、Textarea、Checkbox 和 RadioGroup 等表单组件,还提供了一个Form组件,该组件是 react-hook-form 的包装器。下面来用 shadcn/ui 创建一个登录表单。
Form 组件提供了一些功能:
可以运行以下命令来使用表单组件:
npx shadcn-ui@latest add form input
接下来,添加表单组件:
// use client
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
const FormSchema = z.object({
username: z.string().min(2, { message: "用户名至少两个字" }),
});
export function InputForm() {
const form = useForm({ resolver: zodResolver(FormSchema) });
function onSubmit(data) {
return (
{JSON.stringify(data, null, 2)}
);
}
return (
);
}
Shadcn UI 组件的通用架构如下:
shadcn/ui基于核心原则构建,即组件的设计应与其实现分开。因此,shadcn/ui中的每个组件都具有两层架构。即:
在结构和行为层,组件以无头形式实现,这意味着它们的结构组成和核心行为都被封装在相应的表示中,这意味着组件的结构、布局和核心功能都在这一层进行定义和实现。此外,对于一些复杂的交互,如键盘导航和WAI-ARIA标准兼容性,也在这个层面进行考虑和实现。
为了支持这些复杂的功能和交互,shadcn/ui借助了一些成熟的、无头(无界面)的UI库。Radix UI 就是其中的一个关键库,它在shadcn/ui的代码库中占有重要地位。许多常见的组件,如折叠面板(Accordion)、弹出框(Popover)、选项卡(Tabs)等,都是基于 Radix UI 的实现构建的。
对于满足大多数组件需求,原生浏览器元素和Radix UI组件已经足够了。但在某些情况下,需要使用更专业的无头UI库来满足特定需求。
其中一种情况是表单处理。为了处理表单,shadcn/ui提供了一个基于React Hook Form无头表单库的Form组件。这个组件负责管理表单的状态,而shadcn/ui则通过组合的方式,利用React Hook Form提供的基元进行了进一步的封装。
对于表格视图的处理,shadcn/ui 选择了 Tanstack React Table 这个无头表格库。它的Table
和DataTable
组件都是基于这个库构建的。Tanstack React Table 提供了丰富的 API,用于处理表格视图的各种交互,如过滤、排序和虚拟化。
另外,对于一些复杂的日期选择组件,如日历视图、DateTime选择器和DateRange选择器,shadcn/ui 选择了 React Day Picker 这个库作为基础组件,以实现这些组件的无头层。这些组件往往难以正确实现,但通过使用 React Day Picker, shadcn/ui 确保了它们的正确性和易用性。
TailwindCSS 是 shadcn/ui 样式层的核心。颜色、边框半径等属性值作为CSS变量存放在global.css文件中,以便于全局管理。这些变量可以跨设计系统共享,使用 Figma 等设计工具时,可以追踪并同步Figma的变量。
为了区分组件的样式,shadcn/ui引入了Class Variance Authority(CVA)。CVA提供了一个强大的API,允许我们为每个组件定制其样式。
在探讨了 shadcn/ui 的架构后,下面来深入了解一些组件的具体实现,先从最简单的组件开始。
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
);
export interface BadgeProps extends React.HTMLAttributes, VariantProps {}
function Badge({ className, variant, ...props }: BadgeProps) {
return ;
}
export { Badge, badgeVariants };
组件的实现始于对 class-variance-authority 中的 cva
函数的调用,它被用于声明组件的不同变体。
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
);
cva函数的第一个参数为
调用cva函数后会返回另一个函数,该函数可根据条件为各个变体应用相应的样式。将其存储在名为badgeVariants的变量中,以便在向组件传递变体名称作为属性时,能够利用它应用正确的样式。
接下来,我们可以找到定义组件类型的BadgeProps接口:
export interface BadgeProps extends React.HTMLAttributes, VariantProps {}
Badge 组件基于HTML的div元素。为了方便使用,该组件被设计为div元素的扩展。这一目标通过扩展React.HTMLAttributes
function Badge({ className, variant, ...props }: BadgeProps) {
return ;
}
最终,我们得到了定义 Badge 的函数组件。在此组件中,除了 className 和 variant 之外的所有 props 都被收集到一个对象中,并通过扩展语法传递给底层的 div 元素。这使得组件使用者能够与 div 元素上可用的所有 props 进行交互。
值得注意的是,组件中处理样式应用的方式。variant 的值被传递到 badgeVariants 函数中,该函数返回一个包含渲染组件变体所需的所有实用程序类名的 class 字符串。此外,还有一个名为 cn 的函数,它将前述函数的返回值和传递到 className 中的值合并,然后计算为 div 元素的 className 属性。
cn 函数是 shadcn/ui 提供的一个特殊实用函数,用于管理实用程序类。接下来,我们将深入探讨它的实现。
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
这个实用函数是两个库的结合体,用于管理实用程序类。第一个库是 clsx
。它提供了通过 className
连接来有条件地应用样式到组件的能力。
import React from "react";
const Link = ({ isActive, children }: { isActive: boolean, children: React.ReactNode }) => {
return {children};
};
从上述代码中可以看到 clsx 独立使用的情形。在默认情况下,只有 text-lg 实用类被应用于 Link 组件。但当将 isActive 属性传递给组件并设置为 true 时,text-blue-500 实用类也会被应用于该组件。
然而,在某些情况下,仅使用 clsx 无法实现我们的目标。
import React from "react";
import clsx from "clsx";
const Link = ({ isActive, children }: { isActive: boolean, children: React.ReactNode }) => {
return {children};
};
在此情况下,元素默认应用了颜色实用类 text-grey-800。我们的目标是在 isActive 变为 true 时将文本颜色更改为 blue-500。但由于 CSS 的层叠性质,Tailwind 中的 text-grey-800 应用的颜色样式无法被修改。
此时就需要使用 tailwind-merge库。使用 tailwind-merge 修改上述代码:
import React from "react";
import { twMerge } from "tailwind-merge";
import clsx from "clsx";
const Link = ({ isActive, children }: { isActive: boolean, children: React.ReactNode }) => {
return {children};
};
clsx的输出现在将通过tailwind-merge进行处理。tailwind-merge将解析类字符串并进行浅层样式定义合并。这意味着text-grey-800被替换为text-blue-500,从而确保元素能体现出新的条件样式应用。
这种方法有助于确保在实现变体时不会发生任何样式冲突。由于className属性也经过了cn工具的处理,如果需要,可以轻松覆盖任何样式。但这也存在一个权衡之处。使用cn开启了组件使用者临时覆盖样式的可能性。这将使一定程度的责任转移到代码审查步骤上,以验证cn没有被滥用。另一方面,如果根本不需要启用这种行为,可以修改组件仅使用clsx。
在分析Badge组件的实现时,可以发现一些与 SOLID 原则相关的模式:
在分析了 Badge 组件的实现之后,我们对 shadcn/ui 的一般架构有了更详细的了解。但这是一个纯显示级别的组件。下面来看看其他一些交互式组件。
下面是 Switch 组件的具体实现:
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }
Switch 组件是用于在两个选项之间进行选择的交互式组件,与仅用于显示的 Badge 组件不同。Switch 组件能够响应用户的操作并切换其状态,为用户提供即时的反馈。
用户与开关的交互主要通过点击实现。构建一个能够响应指针事件的开关相对简单,但要使其也能响应键盘输入和屏幕阅读器,实现起来就更为复杂。以下是开关组件的一些预期行为:
在代码中,可以看到开关的实际结构是通过使用
值得注意的是,RadixUI 组件没有提供任何样式。因此,经过 cn 实用函数处理后,样式直接应用于该组件的 className 属性上。如有需要,还可以使用 cva 为组件创建变体。这种灵活的样式管理方式使得开发者能够根据项目需求进行定制化设计,提高用户体验。
这里我们讨论了 shadcn/ui 的一般架构,这种实现方式同样应用在 shadcn/ui 的其它组件中。不过,某些组件的行为和实现会稍微复杂一些,比如:
shadcn/ui 在前端开发领域中引入了一种创新的范例。它倡导一种新的思维方式,即开发者可以拥有组件的实现权,而不仅仅是依赖于抽象化的第三方包。通过这种方式,开发者能够仅暴露所需的元素,从而更好地控制组件的行为和外观。
在应用设计系统时,shadcn/ui 鼓励开发者跳出预先构建的组件库所限制的固定 API 表面。相反,它鼓励开发者构建自己的设计系统,并提供足够良好的默认设置,以便开发者可以根据自己的需求进行自定义。这种灵活性使得开发者能够更好地适应不同的项目需求,并在设计过程中拥有更大的自由度。
Shadcn UI 为开发者提供了一种全新的体验,与现有的组件库相比,它如一阵清风般令人耳目一新。它不仅加快了开发速度,还为开发者提供了对组件的精细控制,使他们能够创造出独特且富有创意的用户界面。
当然,没有任何库能满足所有需求,但基于当前的行业趋势,Shadcn UI 无疑已成为前端生态系统中的佼佼者。许多大型公司,如 Vercel,已经采纳了这一解决方案。例如,Vercel 的 v0 应用利用 Shadcn UI、Tailwind CSS 等来生成 UI 代码,这些代码可供开发人员直接复制并粘贴到其项目中。
尽管 Shadcn UI 仍是一个相对较新的工具,但随着时间的推移,期待看到其功能和组件的进一步丰富和完善。你是否会考虑在未来的项目中采用 Shadcn UI 呢?