Open23

shadcn/ui を使ってみる

myttymytty

CLI の実行

npx shadcn-ui@latest init

Configure components.jsonの設定のため、質問に答えていく

  1. 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 をどれにするか

  1. Would you like to use CSS variables for colors?

色にCSS変数を利用するか

myttymytty

components フォルダ、lib/utils.ts ファイル、components.json ファイルが作成される。utils.ts には cn ヘルパーが定義されている。

また、tailwind.config.ts に theme が追加され、global.css に CSS Variables が追加される。

myttymytty

とりあえずボタンコンポーネントを追加してみる

npx shadcn-ui@latest add button

ちなみに、以下のようにするとコンポーネントを選択することができる。

 npx shadcn-ui@latest add
myttymytty

コンポーネントは、@/components/ui内に追加されるので、一旦そのままのディレクトリ構成で使う。

myttymytty

ボタンを表示してみる

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 にしたから、黒基調。

myttymytty

theme で揃えてるからか、variant で 色 + 形状を変えているけど、個人的には、appearence(variant) を指定することで形状を、color に primary, danger みたいな値を指定することで、色のバリエーションを変更できるようにしたい。

myttymytty

cva の compoundVariants とか使えばうまく実現できるけど、コード自体複雑になるし、そこまでするメリットもないので、やめる
(variant は3種類くらいにしようと思ってるし、color も 2,3 色だしな)

myttymytty

タブ気になってるから触ってみる
https://ui.shadcn.com/docs/components/tabs

myttymytty
"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 }

myttymytty
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>
  )
}

myttymytty

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 }

myttymytty

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>

myttymytty

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>

myttymytty

Card コンポーネント
https://ui.shadcn.com/docs/components/card

myttymytty
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 }

myttymytty

Radix に Card コンポーネントがないから自作していて、いたってシンプルな作りになっている。

myttymytty

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 }