shadcn/ui を使ってみる
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
CLI の実行
npx shadcn-ui@latest init
Configure components.json
の設定のため、質問に答えていく
- Which style would you like to use? › Default or NewYork
どちらのスタイルを利用するか。
選択結果によって、コンポーネントのスタイルが変わる。
どうやら Spacing や Radius などが変わるみたい。
例えば、Badge コンポーネントだと以下のように違いがある。
2.Which color would you like to use as base color? > Slate or Gray or ....
Theme Color をどれにするか
- Would you like to use CSS variables for colors?
色にCSS変数を利用するか
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
components フォルダ、lib/utils.ts ファイル、components.json ファイルが作成される。utils.ts には cn ヘルパーが定義されている。
また、tailwind.config.ts に theme が追加され、global.css に CSS Variables が追加される。
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
とりあえずボタンコンポーネントを追加してみる
npx shadcn-ui@latest add button
ちなみに、以下のようにするとコンポーネントを選択することができる。
npx shadcn-ui@latest add
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
コンポーネントは、@/components/ui
内に追加されるので、一旦そのままのディレクトリ構成で使う。
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
ボタンを表示してみる
import { Button } from "@/components/ui/button";
export default function Home() {
return (
<div className="space-y-5">
<div className="font-bold">Variant</div>
<div className="flex space-x-5">
<Button>Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
</div>
<div className="font-bold">Size</div>
<div className="flex space-x-5">
<Button>Default</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon">Icon</Button>
</div>
</div>
);
}
Variants に関しては、デフォルトだと variant と size のみ。
色は、theme を Slate にしたから、黒基調。
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
theme で揃えてるからか、variant で 色 + 形状を変えているけど、個人的には、appearence(variant) を指定することで形状を、color に primary, danger みたいな値を指定することで、色のバリエーションを変更できるようにしたい。
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
outlinePrimay とかにするしかない?
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
cva の compoundVariants とか使えばうまく実現できるけど、コード自体複雑になるし、そこまでするメリットもないので、やめる
(variant は3種類くらいにしようと思ってるし、color も 2,3 色だしな)
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
ちなみに、compoundVariants 使えばという発想は、Next UI のソースコードから得た
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
タブ気になってるから触ってみる
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
Radix の tabs のデザインを少しいじってるのみ。
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
export default function Home() {
return (
<Tabs defaultValue="tab1" className="w-[600px]">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="tab1">Tab1</TabsTrigger>
<TabsTrigger value="tab2">Tab2</TabsTrigger>
</TabsList>
<TabsContent value="tab1">
Tab1Content
</TabsContent>
<TabsContent value="tab2">
Tab2Content
</TabsContent>
</Tabs>
)
}
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
Radix UI の Tabs コード
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
TabsTrigger と TabsList を改良した
'use client'
import React from 'react'
import * as TabsPrimitive from '@radix-ui/react-tabs'
import { cn } from '@/lib/utils'
import { type VariantProps, cva } from 'class-variance-authority'
const Tabs = TabsPrimitive.Root
const TabsListVariants = cva(
'border-b inline-flex items-center font-medium w-full',
{
variants: {
evenlyDistribution: {
true: 'grid grid-cols-[repeat(auto-fit,minmax(80px,1fr))] justify-center',
},
},
defaultVariants: {
evenlyDistribution: false
}
}
)
export interface TabsListProps extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>,
VariantProps<typeof TabsListVariants>
{}
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
TabsListProps
>(({ evenlyDistribution, className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(TabsListVariants({ evenlyDistribution, className }))}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'border-b-2 border-[#0000] data-[state=active]:border-black px-4 disabled:border-[#0000] text-[#65717b] disabled:text-[#65717b] hover:border-black items-center justify-center font-medium disabled:opacity-50 disabled:cursor-not-allowed data-[state=active]:text-black hover:text-black h-12',
className,
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
'',
className,
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
evenlyDistribution が true の場合
<Tabs defaultValue="tab-a" className="w-[600px]">
<TabsList evenlyDistribution>
<TabsTrigger value="tab-a">Tab A</TabsTrigger>
<TabsTrigger value="tab-b">Tab B</TabsTrigger>
</TabsList>
<TabsContent value="tab-a">
Tab A Content
</TabsContent>
<TabsContent value="tab-b">
Tab B Content
</TabsContent>
</Tabs>
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
evenlyDistribution が false の場合
<Tabs defaultValue="tab-a" className="w-[600px]">
<TabsList>
<TabsTrigger value="tab-a">Tab A</TabsTrigger>
<TabsTrigger value="tab-b">Tab B</TabsTrigger>
</TabsList>
<TabsContent value="tab-a">
Tab A Content
</TabsContent>
<TabsContent value="tab-b">
Tab B Content
</TabsContent>
</Tabs>
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
とりあえず、variants は evenlyDistribution のみ
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
Card コンポーネント
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
Radix に Card コンポーネントがないから自作していて、いたってシンプルな作りになっている。
![mytty](https://res.cloudinary.com/zenn/image/fetch/s--WsNg1xR4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/5d4555fc05.jpeg)
shadcn/ui 風に Heading コンポーネントを自作してみた
import { cn } from '@/lib/utils'
import { VariantProps, cva } from 'class-variance-authority'
import React from 'react'
const headingVariants = cva(
'',
{
variants: {
size: {
sm: 'text-sm',
md: 'text-base',
lg: 'text-xl',
xl: 'text-2xl',
'2xl': 'text-3xl',
},
fontWeight: {
normal: 'font-normal',
medium: 'font-medium',
semibold: 'font-semibold',
bold: 'font-bold',
}
},
defaultVariants: {
size: 'lg',
fontWeight: 'bold'
}
}
)
export interface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement>, VariantProps<typeof headingVariants>{
level?: 1 | 2 | 3 | 4 | 5 | 6
}
const Heading = React.forwardRef<HTMLHeadElement, HeadingProps>(
(
{
level = 1,
children,
className,
size,
fontWeight,
...props
},
ref
) => {
const tag = `h${level}`
const classes = cn(headingVariants({ size, fontWeight }), className)
return React.createElement(
tag,
{className:classes, ref, ...props},
children
)
})
Heading.displayName = 'Heading'
export { Heading, headingVariants }