😄

Reactとshadcn/uiのSidebarでサイドバーを作る

2024/10/26に公開

Reactとshadcn/uiのSidebarでサイドバーを作る

初期設定

npx shadcn@latest add sidebar

で必要なセットアップが完了するのでとても簡単

<SidebarProvider>で挟むことで開閉状態が共有できる。useContextが使われてた。公式ドキュメントではapp/layout.tsxでアプリのroot上に追加とある。

実装

仕様

今回実装するサイドバーの仕様を決めてみる

  1. メニューアイテムは公式ドキュメントの配列データをそのまま転用
  2. Inboxのところのみ通知数が表示されるようにする
  3. メニューのラベリングは"アプリケーション"と表示する
  4. 同じデータを使用して折り畳み可能な"ヘルプ"というラベリングのメニューを表示する
  5. デスクトップでは横からぬるっとサイドバー、モバイルでは画面が薄暗くなってサイドバーを表示
  6. 現在のページがわかりやすいように背景色を変える(今回はHome)

コード

先にcollapsibleを追加してください。
2. Inboxのところのみ通知数が表示されるようにする

<SidebarMenuBadge>24</SidebarMenuBadge>
  1. メニューのラベリングは"アプリケーション"と表示する
<SidebarGroupLabel>アプリケーション</SidebarGroupLabel>
  1. 同じデータを使用して折り畳み可能な"ヘルプ"というラベリングのメニューを表示する
    collapsibleというコンポーネントを使用することで折りたたむことができるメニューが実現可能
<Collapsible defaultOpen className="group/collapsible">
  <SidebarGroup>
    <SidebarGroupLabel asChild>
      <CollapsibleTrigger>
        ヘルプ
        <ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
      </CollapsibleTrigger>
    </SidebarGroupLabel>
    <CollapsibleContent>
      <SidebarGroupContent>
        <SidebarMenu>
          <SidebarMenuItem>
            aaaaaa
          </SidebarMenuItem>
        </SidebarMenu>
      </SidebarGroupContent>
    </CollapsibleContent>
  </SidebarGroup>
</Collapsible>
  1. デスクトップでは横からぬるっとサイドバー、モバイルでは画面が薄暗くなってサイドバーを表示
    variantというpropsを渡すことで変更できる。他にも"floating" | "inset"があるが、insetが何かわからん。
<Sidebar side="right" variant="sidebar">
{/**
  * -------------------
  **/}
</Sidebar>
  1. 現在のページがわかりやすいように背景色を変える(今回はHome)

これだけで背景色が変わってアクティブなサイドバーアイテムがわかりやすくなる

<SidebarMenuButton asChild isActive></SidebarMenuButton>
コード全文
interface Items {
  title: string;
  url: string;
  icon: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>;
  notifications?: number;
  isActive?: boolean;
}

// Menu items.
const items: Items[] = [
  {
    title: "Home",
    url: "/",
    icon: HomeIcon,
    isActive: true,
  },
  {
    title: "Inbox",
    url: "/inbox",
    icon: Inbox,
    notifications: 24
  },
  {
    title: "Calendar",
    url: "/calendar",
    icon: Calendar,
  },
  {
    title: "Search",
    url: "/search",
    icon: Search,
  },
  {
    title: "Settings",
    url: "/setting",
    icon: Settings,
  },
]

// ------------------------------------ //
<SidebarTrigger />
  <Sidebar side="right" variant="sidebar">
    <SidebarHeader>
      <h3 className="text-lg font-semibold text-foreground">テスト</h3>
    </SidebarHeader>
    <SidebarContent>
      <SidebarGroup>
        <SidebarGroupLabel>アプリケーション</SidebarGroupLabel>
        <SidebarGroupContent>
          <SidebarMenu>
            {items.map((item) => (
              <SidebarMenuItem key={item.title}>
                <SidebarMenuButton asChild>
                  <a href={item.url}>
                    <item.icon />
                    <span>{item.title}</span>
                  </a>
                </SidebarMenuButton>
                {item.notifications && (
                  <SidebarMenuBadge>24</SidebarMenuBadge>
                )}
              </SidebarMenuItem>
            ))}
          </SidebarMenu>
        <Collapsible defaultOpen className="group/collapsible">
          <SidebarGroup>
            <SidebarGroupLabel asChild>
              <CollapsibleTrigger>
                ヘルプ
                <ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
              </CollapsibleTrigger>
            </SidebarGroupLabel>
            <CollapsibleContent>
              <SidebarGroupContent>
                <SidebarMenu>
                  {items.map((item) => (
                    <SidebarMenuItem key={item.title}>
                      <SidebarMenuButton asChild>
                        <a href={item.url}>
                          <item.icon />
                          <span>{item.title}</span>
                        </a>
                      </SidebarMenuButton>
                      {item.notifications && (
                        <SidebarMenuBadge>24</SidebarMenuBadge>
                      )}
                    </SidebarMenuItem>
                  ))}
                </SidebarMenu>
              </SidebarGroupContent>
            </CollapsibleContent>
          </SidebarGroup>
        </Collapsible>
        </SidebarGroupContent>
      </SidebarGroup>
    </SidebarContent>
  </Sidebar>

モバイルサイズの時、以下のようなエラーが出ると思う。
おそらくSidebar内のSheetのTitle, Descriptionを使っていないせいで下記のようなエラーが出ると予想。

Dialog.tsx:540 Warning: Missing `Description` or `aria-describedby={undefined}` for {DialogContent}.
client.ts:59 `DialogContent` requires a `DialogTitle` for the component to be accessible for screen reader users.

If you want to hide the `DialogTitle`, you can wrap it with our VisuallyHidden component.

For more information, see 

エラー文が気になる場合はSidebar内のSheetに以下を記述して無理やりエラーを解消する

<SheetTitle asChild>
  <span className="sr-only">サイドバー</span>
</SheetTitle>
<SheetDescription asChild>
  <span className="sr-only">サイドバー</span>
</SheetDescription>

swrなどでデータ取得までのローディング中はスケルトンローダーを表示したりできるらしい。

実際の画面

実際の画面

参考

感想

カスタマイズ性が高く、サイドバーも簡単に作ることができ、とても面白い。色々なバリエーションのサイドバーが作ることができるのでますますshadcn/uiから離れることができなくなりそう

GitHubで編集を提案

Discussion