可能你不需要定制自己的 UI 组件库——shadcn/ui

1,094 阅读3分钟

This is NOT a component library. It's a collection of re-usable components that you can copy and paste into your apps.

shadcn/ui 和传统 UI 组件库最大的区别是 shadcn/ui 不通过 npm 分发使用,而是创建源码文件到你的项目本地,然后自由修改使用。正如官网中解释的那样:Pick the components you need. Copy and paste the code into your project and customize to your needs.

复制源代码的好处

相对于调用组件渲染 UI,将源代码复制到本地修改有几个明显的好处

  • 有了源码后可以根据项目需求自由修改组件的每一个细节,不受原有组件库的限制,尤其是不再需要使用各种 hack 手段覆盖组件内部样式
  • 可以删除项目中不需要的代码,减少冗余,能够根据具体使用场景进行针对性的性能优化
  • 当遇到问题时可以直接在源码层面进行调试,而不是依赖于黑盒组件
  • 不需要担心第三方库的版本更新可能带来的兼容性问题,减少了项目的外部依赖,提高了项目的稳定性和可控性。同时也不依赖于第三方库的更新和维护,可以确保项目长期的可维护性

总而言之 shadcn/ui 虽然需要更多的初始工作和维护努力,但它提供了更大的灵活性和控制力,特别适合那些需要高度定制化的项目

安装

按照官方的 installation 文档安装好依赖,就可以添加组件了。等等!不是说不通过 npm 分发组件吗?怎么还需要安装依赖???

因为 shadcn/ui 底层大量使用了 Radix UI 的无样式组件作为基础,生成的代码使用了 TailwindCSS 的类名,不安装配置 TailwindCSS 组件没有对应的默认样式

如果希望在页面使用一个 Card 组件,在项目执行命令 npx shadcn@latest add card,这时候项目会新增 src/components/ui/card.tsx 文件,内部实现了 Card 组件,我们在项目中可以直接使用

import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"

<Card>
  <CardHeader>
    <CardTitle>Card Title</CardTitle>
    <CardDescription>Card Description</CardDescription>
  </CardHeader>
  <CardContent>
    <p>Card Content</p>
  </CardContent>
  <CardFooter>
    <p>Card Footer</p>
  </CardFooter>
</Card>

用同样的方法简单加点料,就可以做出一个精美的表单了

npx shadcn@latest add button card input label select
import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

export function CardWithForm() {
  return (
    <Card className="w-[350px]">
      <CardHeader>
        <CardTitle>Create project</CardTitle>
        <CardDescription>Deploy your new project in one-click.</CardDescription>
      </CardHeader>
      <CardContent>
        <form>
          <div className="grid w-full items-center gap-4">
            <div className="flex flex-col space-y-1.5">
              <Label htmlFor="name">Name</Label>
              <Input id="name" placeholder="Name of your project" />
            </div>
            <div className="flex flex-col space-y-1.5">
              <Label htmlFor="framework">Framework</Label>
              <Select>
                <SelectTrigger id="framework">
                  <SelectValue placeholder="Select" />
                </SelectTrigger>
                <SelectContent position="popper">
                  <SelectItem value="next">Next.js</SelectItem>
                  <SelectItem value="sveltekit">SvelteKit</SelectItem>
                  <SelectItem value="astro">Astro</SelectItem>
                  <SelectItem value="nuxt">Nuxt.js</SelectItem>
                </SelectContent>
              </Select>
            </div>
          </div>
        </form>
      </CardContent>
      <CardFooter className="flex justify-between">
        <Button variant="outline">Cancel</Button>
        <Button>Deploy</Button>
      </CardFooter>
    </Card>
  )
}

轮播 demo

shadcn/ui 的核心理念是不需要作为一个单一的、完整的组件库来安装。但这并不意味着它的所有组件都是完全零依赖的,某些复杂的组件如轮播等需要额外的库来实现其核心功能

当需要使用特定的组件时,shadcn/ui 会告诉你需要安装哪些额外的依赖。这样只需为实际使用的组件安装必要的依赖,而不是安装一个包含所有可能依赖的大型库

轮播(Carousel)底层依赖 Embla Carousel,通过 cli 命令仍然安逸一键生成源码,自动安装必要的依赖。手工安装时候 shadcn/ui 会给组件的源码及其必要的依赖

使用同样非常简单

import * as React from "react"

import { Card, CardContent } from "@/components/ui/card"
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@/components/ui/carousel"

export function CarouselSize() {
  return (
    <Carousel
      opts={{
        align: "start",
      }}
      className="w-full max-w-sm"
    >
      <CarouselContent>
        {Array.from({ length: 5 }).map((_, index) => (
          <CarouselItem key={index} className="md:basis-1/2 lg:basis-1/3">
            <div className="p-1">
              <Card>
                <CardContent className="flex aspect-square items-center justify-center p-6">
                  <span className="text-3xl font-semibold">{index + 1}</span>
                </CardContent>
              </Card>
            </div>
          </CarouselItem>
        ))}
      </CarouselContent>
      <CarouselPrevious />
      <CarouselNext />
    </Carousel>
  )
}