1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Shadcn/ui Sidebarが新たに追加されました。 25種のコンポーネント "It works with Next.js, Remix, Vite & Laravel."

Last updated at Posted at 2024-11-16

この記事はShadcn/uiの SidebarコンポーネントをNext.jsで動作確認をしています。

SidebarコンポーネントをXで公表

Sidebar - shadcn/ui のドキュメント

Sidebar

sidebar.tsx

Sidebar.tsx-25コンポーネントを紹介します。
Next.js、Remix、Vite、Laravelで動作します。

サイドバー - shadcn/ui


この記事のコードについて

リポジトリのファイル配置

(sidebar-publice)フォルダ
Shadcn/uiの Sidebarコンポーネント 公式のサンプル

(sidebar)
自分が試した、Shadcn/UIのサイドバーコンポーネント

リポジトリ内のファイル
src
├── app
│   ├── (sidebar-publice) 公式のサンプル
│   │   ├── sample01
│   │   │   └── page.tsx
...
│   │   └── sample15
│   │       └── page.tsx
│   ├── (sidebar) 自分が試したサンプル
│   │   ├── 01
│   │   │   ├── layout.tsx
│   │   │   └── page.tsx
...
│   │   └── 23
│   │       ├── layout.tsx
│   │       └── page.tsx
│   ├── dashboard
│   ├── layout.tsx
│   └── page.tsx
├── components
│   ├── ui
│   │   └── **.tsx (Shadcn/UIのコンポーネント)
│   ├── app-sidebar_01.tsx
...
│   ├── app-sidebar_24.tsx
│   ├── app-sidebar_sample01.tsx
...
│   ├── app-sidebar_sample14.tsx
│   ├── calendars.tsx
│   ├── date-picker.tsx
│   ├── nav-actions.tsx
│   ├── nav-favorites.tsx
│   ├── nav-main.tsx
│   ├── nav-projects.tsx
│   ├── nav-secondary.tsx
│   ├── nav-user.tsx
│   ├── nav-workspaces.tsx
│   ├── search-form.tsx
│   ├── settings-dialog.tsx
│   ├── sidebar-left.tsx
│   ├── sidebar-opt-in-form.tsx
│   ├── sidebar-right.tsx
│   ├── team-switcher.tsx
│   └── version-switcher.tsx
├── hooks
│   └── use-mobile.tsx
└── lib
    └── utils.ts

公式サンプルのルール 15種類

公式のサンプルはガッツリ作り込まれています。

Shadcn/UIのサイドバーのサンプルは全部で15種類、コンポーネントは25種類あります。

このBlog記事ではその15種類の公式サンプルのフォルダ名を

sidebar-01
sidebar-**と数字をつけて見ていきます。

app-sidebar_sample01.tsx
他に複数のファイルがある時があります。

自分が作ったサンプルのルール

公式サンプルはわかりにくかったので、わかりやすくしてみました。

リポジトリで、自分が作ったサンプルのフォルダ名は

01

01のサイドバーのファイル名
components/app-sidebar_01.tsx
02のサイドバーのファイル名
components/app-sidebar_02_01.tsx
(以下略)

と数字のみのフォルダ名にファイルを入れています。

※この記事に書いてあるコードは公式ドキュメントのコードになります。

※公式ドキュメントで重複しているサイドバーのファイル名には番号を振って識別できるようにしています。

※クッキーに保存されているデータは、他のサイドバーのサンプルに影響を与えています。

GitHub

masakinihirota/ShadcnUI_sidebar

25種のコンポーネントを使ったサンプル。
コードはこちらで動作確認できます。

サイドバーサンプル.PNG

Installation

Next.js 15、 Shadcn/UIのインストール

# Next.js 15
npx create-next-app@latest [app name]

# Shadcn/UI 公式
npx shadcn@latest add sidebar

# Shadcn/UI 公式 サイドバーコンポーネントのサンプル
# ※このサンプルをインストールする時サンプルが重複する時はすべて上書きをします。(重要)
npx shadcn@latest add sidebar-01
npx shadcn@latest add sidebar-02
npx shadcn@latest add sidebar-03
npx shadcn@latest add sidebar-04
npx shadcn@latest add sidebar-05
npx shadcn@latest add sidebar-06
npx shadcn@latest add sidebar-07
npx shadcn@latest add sidebar-08
npx shadcn@latest add sidebar-09
npx shadcn@latest add sidebar-10
npx shadcn@latest add sidebar-11
npx shadcn@latest add sidebar-12
npx shadcn@latest add sidebar-13
npx shadcn@latest add sidebar-14
npx shadcn@latest add sidebar-15

# Shadcn/UI 自作コードで必要なライブラリ
# 01-
npx shadcn@latest add sidebar

# 07
npx shadcn@latest add dropdown-menu

# 11
npx shadcn@latest add collapsible

shadcn/ui サイドバー用のコンポーネント一覧

sidebar.tsx
<Sidebar>
<SidebarContent>
<SidebarFooter>
<SidebarGroup>
<SidebarGroupAction>
<SidebarGroupContent>
<SidebarGroupLabel>
<SidebarHeader>
<SidebarInput>
<SidebarInset>
<SidebarMenu>
<SidebarMenuAction>
<SidebarMenuBadge>
<SidebarMenuButton>
<SidebarMenuItem>
<SidebarMenuSkeleton>
<SidebarMenuSub>
<SidebarMenuSubButton>
<SidebarMenuSubItem>
<SidebarProvider>
<SidebarRail>
<SidebarSeparator>
<SidebarTrigger>
useSidebar <<React Hooks


公式のサンプル

sidebar-01

メニューをすべて
バージョン変更機能がある

sidebar-02

メニューをすべて
メニューを折りたたむ機能

sidebar-03

divタグの追加

を追加

sidebar-04

コンポーネントにカスタムスタイルを適用します。
タイトルの追加

sidebar-05

+折りたたみメニュー機能

SidebarRail は、サイドバーの中で主要なコンテンツを配置するための領域を提供するコンポーネントです。これにより、サイドバー内のアイテムが整理され、ユーザーが簡単にナビゲートできるようになります。

「レール」という言葉は、サイドバーのコンテキストでは、サイドバーの主要な構造部分やコンテンツを配置するための領域を指します。具体的には、サイドバーの中でメニューアイテムやナビゲーションリンクなどが配置される部分です。

sidebar-06

ポップアップメニュー

nav-main.tsx
sidebar-opt-in-form.tsx
の追加
サブスクライブ機能(枠だけ)をメニューに表示

sidebar-07

>折りたたみメニュー機能

メニューをグループ分け

nav-main.tsx
nav-projects.tsx
nav-user.tsx
team-switcher.tsx
の追加

sidebar-08

2個目のメニューの追加

nav-main.tsx
nav-projects.tsx
nav-secondary.tsx
の追加

sidebar-09

アイコンメニュー
メールメニュー
折りたたみ

sidebar-10

3面メニュー
グループ
ポップアップメニュー

nav-actions.tsx
nav-favorites.tsx
nav-main.tsx
nav-secondary.tsx
nav-workspaces.tsx
team-switcher.tsx

sidebar-11

深さ2段メニュー
メニュー内容は普通

sidebar-12

カレンダー
ボタン
calendars.tsx
date-picker.tsx
nav-user.tsx

sidebar-13

Open Dialog
画面全体にメニューが表示されるタイプ
タブレット等の画面が狭いメニュー
画面全体にメニューを表示するタイプ
左側のメニューが固定
右側はフリーエリア
dialog.tsx
settings-dialog.tsx

sidebar-14

右側に開く
ほかは普通?

sidebar-15

左右のメニュー
フルコンボ

自分が試してみたサンプル

Structure

構造

  • Sidebar- サイドバー コンテナー全体。

  • SidebarGroup- 内のセクションSidebarContent。

  • SidebarHeaderそしてSidebarFooter- サイドバーの上部と下部に固定されます。

  • SidebarContent- スクロール可能なコンテンツ。

  • SidebarProvider- 折りたたみ可能な状態を処理します。

  • SidebarTrigger- のトリガーSidebar。開閉します。

sidebar-structure.png


sample

リポジトリからインストールをしてリンクボタンを押すと、番号と一致したライブラリを使用したサンプル(XXへのリンク XXXX)が見れます。
(Homeボタンを押せばTOPメニュー(http://localhost:3000/)に戻ります。)

01 サイドバーのサンプル

左サイドメニューを閉じて開くだけです。
左サイドメニューの項目などはありません。

最初は閉じてますが、左上のメニューのアイコンを押すことで
左側にサイドバーが開閉されます。

これが基本形です。

app/layout.tsx
components/app-sidebar.tsx
を用意します。

app/layout.tsx
import { AppSidebar } from "@/components/app-sidebar_01";
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";

export default function Layout({ children }: { children: React.ReactNode }) {
		// このLayoutファイルではサイドバーのメニュー等を表示、操作するだけです。
		// サイドバーの中の機能はapp-sidebar_**.tsxに書かれています。
		return (
		// Shadcn/UIのサイドバーを使うために必ず必要です。
		<SidebarProvider>
			{/* サイドバーの中身 */}
			<AppSidebar />
			<main>
				{/* サイドバーのトリガー */}
				<SidebarTrigger />
				{children}
			</main>
		</SidebarProvider>
	);
}

components/app-sidebar_01.tsx
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarHeader,
} from "@/components/ui/sidebar"

export function AppSidebar() {
  return (
    <Sidebar>
      <SidebarHeader />
      <SidebarContent>
        <SidebarGroup />
        <SidebarGroup />
      </SidebarContent>
      <SidebarFooter />
    </Sidebar>
  )
}

01のサイドバーはこれで完成です。

02-1 02-2 Your First Sidebar

最初のサイドバー

  • 02-1 は01からオプションをすべて除いた最低限のもの。
  • 02-2 は全てのオプションを付けたフル装備のもの。

メニュー付きの折りたたみ可能なサイドバー

app/layout.tsx
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
import { AppSidebar } from "@/components/app-sidebar"

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <SidebarProvider>
      <AppSidebar />
      <main>
        <SidebarTrigger />
        {children}
      </main>
    </SidebarProvider>
  )
}

components/app-sidebar_02_01.tsx
import { Sidebar, SidebarContent } from "@/components/ui/sidebar"

export function AppSidebar() {
  return (
    <Sidebar>
      <SidebarContent />
    </Sidebar>
  )
}

👆02_01 これは最小限のサイドバー設定です。

👇02_02 👆この最小限のサイドバーにオプション全部盛りしています。

components/app-sidebar_02_02.tsx
import { Calendar, Home, Inbox, Search, Settings } from "lucide-react"

import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
} from "@/components/ui/sidebar"

// Menu items.
const items = [
  {
    title: "Home",
    url: "#",
    icon: Home,
  },
  {
    title: "Inbox",
    url: "#",
    icon: Inbox,
  },
  {
    title: "Calendar",
    url: "#",
    icon: Calendar,
  },
  {
    title: "Search",
    url: "#",
    icon: Search,
  },
  {
    title: "Settings",
    url: "#",
    icon: Settings,
  },
]

export function AppSidebar() {
  return (
    <Sidebar>
      <SidebarContent>
        <SidebarGroup>
          <SidebarGroupLabel>Application</SidebarGroupLabel>
          <SidebarGroupContent>
            <SidebarMenu>
              {items.map((item) => (
                <SidebarMenuItem key={item.title}>
                  <SidebarMenuButton asChild>
                    <a href={item.url}>
                      <item.icon />
                      <span>{item.title}</span>
                    </a>
                  </SidebarMenuButton>
                </SidebarMenuItem>
              ))}
            </SidebarMenu>
          </SidebarGroupContent>
        </SidebarGroup>
      </SidebarContent>
    </Sidebar>
  )
}

これで、最初のサイドバーが作成されました。

※asChildとは

asChildを使うと、外側のコンポーネント(親要素)は名前だけを借りて、実際の機能や動作は内側のタグ(子要素)が実行します。
これにより、親要素のスタイルや機能を保持しつつ、異なるHTML要素やカスタムコンポーネントを使用することができます。

<Button asChild>
  <Link href="/login">Login</Link> {/* Linkを子要素として渡す */}
</Button>

👆このようにasChildプロパティを使うことで、見た目はButtonタグですが、実際にはLinkタグのように、このLink機能が実行されます。

Components

コンポーネント

sidebar.tsxのコンポーネントはコンポーザブルに作られており、提供されたコンポーネントを組み合わせてサイドバーを構築します。
また、DropdownMenuやCollapsible、Dialogなどの他のshadcn/uiコンポーネントともうまく組み合わせることができます。

sidebar.tsxのコードを変更自由に行ってください。
sidebar.tsxを出発点として、あなた独自のメニューを作ってください。

次のセクションでは、各コンポーネントとその使い方について説明します。

幅の変更方法

サイドバーの幅を設定するには、プロパティの--sidebar-widthおよびCSS 変数を使用できます。--sidebar-width-mobilestyle

components/ui/sidebar.tsx
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"

components/ui/sidebar.tsx
<SidebarProvider
  style={{
    "--sidebar-width": "20rem",
    "--sidebar-width-mobile": "20rem",
  }}
>
  <Sidebar />
</SidebarProvider>

Keyboard Shortcut

キーボードショートカット

キーボードショートカットを設定します。

Windows
ctrl+b

components/ui/sidebar.tsx
const SIDEBAR_KEYBOARD_SHORTCUT = "b"

03 Persisted State

03へのリンク 開閉を記憶

ページ状態の保存

SidebarProvider は、ページのリロードとサーバ側のレンダリングにわたってサイドバーの状態を永続化させます。
サイドバーの現在の状態を保存するためにクッキーを使用します。
サイドバーの状態が変更されると、sidebar:stateという名前のデフォルトのクッキーが現在の開閉状態で設定されます。
このクッキーは、サイドバーの状態を復元するために、その後のページロードで読み込まれます。

Next.jsでサイドバーの状態を保持するには、app/layout.tsxでSidebarProviderを次のように設定します:

app/layout.tsx
import { cookies } from "next/headers"

import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
import { AppSidebar } from "@/components/app-sidebar"

export async function Layout({ children }: { children: React.ReactNode }) {
  const cookieStore = await cookies()
  const defaultOpen = cookieStore.get("sidebar:state")?.value === "true"

  return (
    <SidebarProvider defaultOpen={defaultOpen}>
      <AppSidebar />
      <main>
        <SidebarTrigger />
        {children}
      </main>
    </SidebarProvider>
  )
}

クッキーの名前は、sidebar.tsxのSIDEBAR_COOKIE_NAME変数を更新することで変更できます。

components/ui/sidebar.tsx
const SIDEBAR_COOKIE_NAME = "sidebar:state"

Sidebar

サイドバー

import { Sidebar } from "@/components/ui/sidebar"

export function AppSidebar() {
  return <Sidebar />
}

👆基本的な使い方。
これらにオプション設定をしていきます。

04 side

左/右 サイドバーの表示位置を変更します。

app-sidebar_04.tsx
import { Sidebar } from "@/components/ui/sidebar";

export function AppSidebar() {
	// <Sidebar side="left | right" />
	return <Sidebar side="right" />;
}

05 variant

variant プロパティを使うと、サイドバーの見た目を変えることができます。

サイドバーには3つのバリアントがあります。

  • sidebar: 通常のサイドバーで、主に画面の横に固定されている。
  • floating: 浮いているように見えるサイドバーで、他のコンテンツの上に重なる感じです。
  • inset: 内側に配置されるサイドバーで、メインコンテンツの周りに収まります。

使うときは、
のように指定します。

で囲みます。

<SidebarProvider>
  <Sidebar variant="inset" />
  <SidebarInset>
    <main>{children}</main>
  </SidebarInset>
</SidebarProvider>

// <Sidebar variant="sidebar | floating | inset" />
<Sidebar variant="inset">

sidebar:
デフォルトのサイドバーのバリアントです。
通常のサイドバーのように、コンテンツの左または右側に固定して配置されます。
コンテンツがスクロールしても、サイドバーは同じ位置にいます。

floating:
このバリアントは、サイドバーが画面の上に浮かんで表示されます。
メインコンテンツの上にメニューが重なって表示されるからコンテンツの一部が見えなくなることもあります。
視覚的に際立たせるために使用できます。

inset:
このバリアントでは、サイドバーがメインコンテンツに埋め込まれます。
メインコンテンツは、SidebarInsetコンポーネントでラップする必要があります。
サイドバーとメインコンテンツ間のより緊密な統合を作成するために使用できます。

inset - CSS: カスケーディングスタイルシート | MDN

https://developer.mozilla.org/ja/docs/Web/CSS/inset

それぞれのバリアントは、アプリケーションの特定のニーズやデザインの好みに応じて異なる視覚効果とレイアウトを提供します。

06 collapsible

折りたたみ

import { Sidebar } from "@/components/ui/sidebar"

export function AppSidebar() {
  return <Sidebar collapsible="offcanvas | icon | none" />
}

サイドバーの折りたたみ方を決めています。

collapsible プロパティ には、 offcanvas 、 icon 、 none の3つの値を設定できます。

offcanvas: 画面の端からスライドして表示・非表示になる、よくあるサイドバーです。

icon: サイドバーが折りたたまれて、アイコンだけが残されて表示されます。

none: サイドバーは折りたたまれません。常に表示されます。

React Hooks useSidebar

useSidebar はサイドバーを制御するために使用されます。

import { useSidebar } from "@/components/ui/sidebar"

export function AppSidebar() {
  const {
    state,
    open,
    setOpen,
    openMobile,
    setOpenMobile,
    isMobile,
    toggleSidebar,
  } = useSidebar()
}

useSidebar を使うと、サイドバーの状態や動作を簡単に制御できる 便利なプロパティ が使えるようになります。

state: 今のサイドバーの状態がわかります。「展開」してるのか「折りたたまれてる」のか、一目瞭然です。

open: サイドバーが開いているかどうかを教えてくれます。 true なら開いてるし、 false なら閉じています。

setOpen: サイドバーを開いたり閉じたりします、 true を渡せば開き、 false を渡せば閉じます。

openMobile: モバイル版のサイドバーが開いているかどうかを教えてくれます。これも true か false で判断します。

setOpenMobile: モバイル版のサイドバーを開いたり閉じたりします setOpen と同じように、 true か false を渡すだけです。

isMobile: 今使ってるのがモバイル端末かどうかを教えてくれます。モバイル用のレイアウトにする必要があるかどうかの判断に便利です。

toggleSidebar: サイドバーを開いたり閉じたりする 切り替え をしてくれます。いちいち open や setOpen を操作しなくても、これを使えばワンタッチで切り替えられます。

  • このようにuseSidebar を使えば、サイドバーを思い通りに動かせるようになります。

useSidebar プロパティ日本語解説

useSidebar フックは、サイドバーを制御するための様々なプロパティを提供します。これらのプロパティを使用することで、サイドバーの開閉状態、モバイルでの表示、状態の切り替えなどをJavaScriptで操作できます。

プロパティ 説明
state expanded/collapsed サイドバーの現在の状態 (展開または折りたたみ) を示します。
open boolean サイドバーが開いているかどうかを示す真偽値です。
setOpen (open: boolean) => void サイドバーの開閉状態を設定します。引数に true を渡すと開き、 false を渡すと閉じます。
openMobile boolean モバイル版のサイドバーが開いているかどうかを示す真偽値です。
setOpenMobile (open: boolean) => void モバイル版のサイドバーの開閉状態を設定します。 setOpen と同様です。
isMobile boolean 現在のデバイスがモバイル端末かどうかを示す真偽値です。
toggleSidebar () => void デスクトップとモバイルの両方で、サイドバーの開閉状態を切り替えます。

これらのプロパティは、 useSidebar フックを呼び出すことで取得できます。

import { useSidebar } from "@/components/ui/sidebar"

export function AppSidebar() {
  const {
    state,
    open,
    setOpen,
    openMobile,
    setOpenMobile,
    isMobile,
    toggleSidebar,
  } = useSidebar()
}

取得したプロパティは、サイドバーの動作をカスタマイズする際に役立ちます。
例えば、 isMobile プロパティを使用して、モバイル端末でのみ特定のスタイルを適用したり、 toggleSidebar プロパティを使用して、ボタンクリックでサイドバーの開閉状態を切り替えたりすることができます。

07 SidebarHeader

サイドバーのヘッダー

ヘッダーにドロップダウンメニューが使えます。

app-sidebar_07.tsx

08 SidebarFooter

サイドバーのフッター

サイドバーにスティッキーフッターを追加するには、SidebarFooterコンポーネントを使用します。

次の例では、SidebarFooterにを追加しています。

components/app-sidebar_08.tsx

09 SidebarContent

サイドバーのコンテンツを構成する基本形

つまり、メニューの項目など

SidebarContentコンポーネントは、サイドバーのコンテンツをラップするために使用されます。

ここに、SidebarGroupコンポーネントを追加します。
スクロール可能です。

import { Sidebar, SidebarContent } from "@/components/ui/sidebar"

export function AppSidebar() {
  return (
    <Sidebar>
      <SidebarContent>
        <SidebarGroup />
        <SidebarGroup />
      </SidebarContent>
    </Sidebar>
  )
}

メニューの構成の基本形です。


を囲みます。

10 SidebarGroup

サイドバーのグループ

サイドバー内にセクションを作成します。

ラベル(Application)を作り、+のアイコンを添えます。

サイドバー内のグループはで囲みます。
その中で

でラベルを書きます。
で動作を書きます。
SidebarGroupのContentsを囲みます。

import { Sidebar, SidebarContent, SidebarGroup } from "@/components/ui/sidebar"
import { Plus } from "lucide-react" // プラスアイコンのインポート

export function AppSidebar() {
  return (
    <Sidebar>
      <SidebarContent>
        <SidebarGroup>
          <SidebarGroupLabel>Application</SidebarGroupLabel>
          <SidebarGroupAction>
            <Plus /> <span className="sr-only">Add Project</span>
          </SidebarGroupAction>
          <SidebarGroupContent></SidebarGroupContent>
        </SidebarGroup>
      </SidebarContent>
    </Sidebar>
  )
}

className="sr-only"は、スクリーンリーダーなどの支援技術向けにテキストを提供するもので、視覚的には表示されません。

11 Collapsible SidebarGroup

折りたたみ可能なサイドバーグループ

グループを折りたたみます。

app-sidebar_11.tsx
import {
	SidebarGroup,
	SidebarGroupContent,
	SidebarGroupLabel,
} from "@/components/ui/sidebar";
import {
	Collapsible,
	CollapsibleContent,
	CollapsibleTrigger,
} from "@radix-ui/react-collapsible";

import { ChevronDown } from "lucide-react";

export function AppSidebar() {
  return (
    <Collapsible defaultOpen className="group/collapsible">
      <SidebarGroup>
        <SidebarGroupLabel asChild>
          <CollapsibleTrigger>
            Help
            <ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
          </CollapsibleTrigger>
        </SidebarGroupLabel>
        <CollapsibleContent>
          <SidebarGroupContent />
          メニュー項目1
          <br />
          メニュー項目2
          <br />
          メニュー項目3
        </CollapsibleContent>
      </SidebarGroup>
    </Collapsible>
  )
}

12 SidebarGroupAction

SidebarGroupAction コンポーネントを使用して、SidebarGroup にアクション ボタンを追加します。

app-sidebar_12.tsx
import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupAction,
  SidebarGroupContent,
  SidebarGroupLabel,
} from "@/components/ui/sidebar";

import { Plus } from "lucide-react";

export function AppSidebar() {
  return (
    <Sidebar>
      <SidebarContent>
        <SidebarGroup>
          <SidebarGroupLabel asChild>Projects</SidebarGroupLabel>
          <SidebarGroupAction title="Add Project">
            <Plus /> <span className="sr-only">Add Project</span>
          </SidebarGroupAction>
          <SidebarGroupContent >
            メニュー項目1
            <br />
            メニュー項目2
            <br />
            メニュー項目3
          </SidebarGroupContent>
        </SidebarGroup>
      </SidebarContent>
    </Sidebar>
  )
}


13 SidebarMenu

SidebarMenuコンポーネントは、SidebarGroup内にメニューを構築するために使用されます。

SidebarMenuコンポーネントは、

コンポーネントで構成されています。

sidebar-menu.png

app-sidebar_13.tsx
import {
	Sidebar,
	SidebarContent,
	SidebarGroup,
	SidebarGroupContent,
	SidebarGroupLabel,
	SidebarMenu,
	SidebarMenuButton,
	SidebarMenuItem,
} from "@/components/ui/sidebar";

const projects = [
	{
		name: "Project 1",
		url: "/project1",
		icon: "Activity",
	},
	{
		name: "Project 2",
		url: "/project2",
		icon: "Activity",
	},
	{
		name: "Project 3",
		url: "/project3",
		icon: "Activity",
	},
];

export function AppSidebar() {
	return (
		<Sidebar>
			<SidebarContent>
				<SidebarGroup>
					<SidebarGroupLabel>Projects</SidebarGroupLabel>
					<SidebarGroupContent>
						<SidebarMenu>
							{projects.map((project) => (
								<SidebarMenuItem key={project.name}>
									<SidebarMenuButton asChild>
										<a href={project.url}>
											<project.icon />
											<span>{project.name}</span>
										</a>
									</SidebarMenuButton>
								</SidebarMenuItem>
							))}
						</SidebarMenu>
					</SidebarGroupContent>
				</SidebarGroup>
			</SidebarContent>
		</Sidebar>
	);
}

SidebarMenuButton

SidebarMenuButtonコンポーネントは、SidebarMenuItem内のメニューボタンをレンダリングするために使用されます。

Link or Anchor

デフォルトでは、SidebarMenuButtonはボタンをレンダリングしますが、asChildプロパティを使用して、Linkやaタグのような別のコンポーネントをレンダリングすることができます。

<SidebarMenuButton asChild>
  <a href="#">Home</a>
</SidebarMenuButton>

Icon and Label

<SidebarMenuButton asChild>
  <a href="#">
    <Home />
    <span>Home</span>
  </a>
</SidebarMenuButton>

isActive

<SidebarMenuButton asChild isActive>
  <a href="#">Home</a>
</SidebarMenuButton>

14 SidebarMenuAction

SidebarMenuActionコンポーネントは、SidebarMenuItem内のメニューアクションをレンダリングするために使用されます。

このボタンは、SidebarMenuButtonとは独立して動作します。

たとえば、をクリック可能なリンクとして、をボタンとして使用することができます。

app-sidebar_14.tsx
import {
  Sidebar,
  SidebarMenuAction,
  SidebarMenuButton,
  SidebarMenuItem,
} from "@/components/ui/sidebar";

import { Home, Plus } from "lucide-react";

export function AppSidebar() {
  return (
    <Sidebar>
      <SidebarMenuItem>
        <SidebarMenuButton asChild>
          <a href="/">
            <Home />
            <span>Home</span>
          </a>
        </SidebarMenuButton>
        <SidebarMenuAction>
          <Plus /> <span className="sr-only">Add Project</span>
        </SidebarMenuAction>
      </SidebarMenuItem>
    </Sidebar>
  );
}

15 DropdownMenu

以下は、SidebarMenuActionコンポーネントがDropdownMenuをレンダリングする例です。

<SidebarMenuItem>
  <SidebarMenuButton asChild>
    <a href="#">
      <Home />
      <span>Home</span>
    </a>
  </SidebarMenuButton>
  <DropdownMenu>
    <DropdownMenuTrigger asChild>
      <SidebarMenuAction>
        <MoreHorizontal />
      </SidebarMenuAction>
    </DropdownMenuTrigger>
    <DropdownMenuContent side="right" align="start">
      <DropdownMenuItem>
        <span>Edit Project</span>
      </DropdownMenuItem>
      <DropdownMenuItem>
        <span>Delete Project</span>
      </DropdownMenuItem>
    </DropdownMenuContent>
  </DropdownMenu>
</SidebarMenuItem>

三点リーダーのアイコンを押すと、ドロップダウンメニューが使えます。

16 SidebarMenuSub

SidebarMenuSubコンポーネントは、SidebarMenu内のサブメニューを表示するために使用します。

サブメニュー項目を表示するには、とを使用します。

<SidebarMenuItem>
  <SidebarMenuButton />
  <SidebarMenuSub>
    <SidebarMenuSubItem>
      <SidebarMenuSubButton />
    </SidebarMenuSubItem>
    <SidebarMenuSubItem>
      <SidebarMenuSubButton />
    </SidebarMenuSubItem>
  </SidebarMenuSub>
</SidebarMenuItem>

※ベース

app-sidebar_16.tsx
import {
	Sidebar,
	SidebarContent,
	SidebarGroup,
	SidebarGroupContent,
	SidebarGroupLabel,
	SidebarMenu,
	SidebarMenuButton,
	SidebarMenuItem,
	SidebarMenuSub,
	SidebarMenuSubButton,
	SidebarMenuSubItem,
} from "@/components/ui/sidebar";

const projects = [
	{
		name: "Project 1",
		url: "/project1",
		icon: "Activity",
	},
	{
		name: "Project 2",
		url: "/project2",
		icon: "Activity",
	},
	{
		name: "Project 3",
		url: "/project3",
		icon: "Activity",
	},
];

export function AppSidebar() {
	return (
		<Sidebar>
			<SidebarContent>
				<SidebarGroup>
					<SidebarGroupLabel>Projects</SidebarGroupLabel>
					<SidebarGroupContent>
						<SidebarMenu>
							{projects.map((project) => (
								<SidebarMenuItem key={project.name}>
									<SidebarMenuButton asChild>
										<a href={project.url}>
											<project.icon />
											<span>{project.name}</span>
										</a>
									</SidebarMenuButton>
									<SidebarMenuSub>
										<SidebarMenuSubItem>
											<SidebarMenuSubButton />
										</SidebarMenuSubItem>
										<SidebarMenuSubItem>
											<SidebarMenuSubButton />
										</SidebarMenuSubItem>
									</SidebarMenuSub>
								</SidebarMenuItem>
							))}
						</SidebarMenu>
					</SidebarGroupContent>
				</SidebarGroup>
			</SidebarContent>
		</Sidebar>
	);
}

17 Collapsible SidebarMenu

折りたたみ可能なサイドバーメニュー

SidebarMenuコンポーネントを折りたたみ可能にするには、SidebarMenuコンポーネントとSidebarMenuSubコンポーネントをCollapsibleで囲みます。

<SidebarMenu>
  <Collapsible defaultOpen className="group/collapsible">
    <SidebarMenuItem>
      <CollapsibleTrigger asChild>
        <SidebarMenuButton />
      </CollapsibleTrigger>
      <CollapsibleContent>
        <SidebarMenuSub>
          <SidebarMenuSubItem />
        </SidebarMenuSub>
      </CollapsibleContent>
    </SidebarMenuItem>
  </Collapsible>
</SidebarMenu>

※ベース

app-sidebar_17.tsx
import * as React from "react";
import { GalleryVerticalEnd } from "lucide-react";

import {
	Sidebar,
	SidebarContent,
	SidebarGroup,
	SidebarHeader,
	SidebarMenu,
	SidebarMenuButton,
	SidebarMenuItem,
	SidebarMenuSub,
	SidebarMenuSubButton,
	SidebarMenuSubItem,
} from "@/components/ui/sidebar";
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@radix-ui/react-collapsible";

// This is sample data.
const data = {
	navMain: [
		{
			title: "Getting Started",
			url: "#",
			items: [
				{
					title: "Installation",
					url: "#",
				},
				{
					title: "Project Structure",
					url: "#",
				},
			],
		},
		{
			title: "Building Your Application",
			url: "#",
			items: [
				{
					title: "Routing",
					url: "#",
				},
				{
					title: "Data Fetching",
					url: "#",
					isActive: true,
				},
				{
					title: "Rendering",
					url: "#",
				},
				{
					title: "Caching",
					url: "#",
				},
				{
					title: "Styling",
					url: "#",
				},
				{
					title: "Optimizing",
					url: "#",
				},
				{
					title: "Configuring",
					url: "#",
				},
				{
					title: "Testing",
					url: "#",
				},
				{
					title: "Authentication",
					url: "#",
				},
				{
					title: "Deploying",
					url: "#",
				},
				{
					title: "Upgrading",
					url: "#",
				},
				{
					title: "Examples",
					url: "#",
				},
			],
		},
		{
			title: "API Reference",
			url: "#",
			items: [
				{
					title: "Components",
					url: "#",
				},
				{
					title: "File Conventions",
					url: "#",
				},
				{
					title: "Functions",
					url: "#",
				},
				{
					title: "next.config.js Options",
					url: "#",
				},
				{
					title: "CLI",
					url: "#",
				},
				{
					title: "Edge Runtime",
					url: "#",
				},
			],
		},
		{
			title: "Architecture",
			url: "#",
			items: [
				{
					title: "Accessibility",
					url: "#",
				},
				{
					title: "Fast Refresh",
					url: "#",
				},
				{
					title: "Next.js Compiler",
					url: "#",
				},
				{
					title: "Supported Browsers",
					url: "#",
				},
				{
					title: "Turbopack",
					url: "#",
				},
			],
		},
		{
			title: "Community",
			url: "#",
			items: [
				{
					title: "Contribution Guide",
					url: "#",
				},
			],
		},
	],
};

export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
	return (
		<Sidebar variant="floating" {...props}>
			<SidebarHeader>
				<SidebarMenu>
					<SidebarMenuItem>
						<SidebarMenuButton size="lg" asChild>
							<a href="#">
								<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
									<GalleryVerticalEnd className="size-4" />
								</div>
								<div className="flex flex-col gap-0.5 leading-none">
									<span className="font-semibold">Documentation</span>
									<span className="">v1.0.0</span>
								</div>
							</a>
						</SidebarMenuButton>
					</SidebarMenuItem>
				</SidebarMenu>
			</SidebarHeader>
			<SidebarContent>
				<SidebarGroup>
					<SidebarMenu className="gap-2">
						{data.navMain.map((item) => (
							<Collapsible key={item.title} defaultOpen className="group/collapsible">
								<SidebarMenuItem>
									<CollapsibleTrigger asChild>
										<SidebarMenuButton asChild>
											<a href={item.url} className="font-medium">
												{item.title}
											</a>
										</SidebarMenuButton>
									</CollapsibleTrigger>
									{item.items?.length ? (
										<CollapsibleContent>
											<SidebarMenuSub className="ml-0 border-l-0 px-1.5">
												{item.items.map((subItem) => (
													<SidebarMenuSubItem key={subItem.title}>
														<SidebarMenuSubButton asChild isActive={subItem.isActive}>
															<a href={subItem.url}>{subItem.title}</a>
														</SidebarMenuSubButton>
													</SidebarMenuSubItem>
												))}
											</SidebarMenuSub>
										</CollapsibleContent>
									) : null}
								</SidebarMenuItem>
							</Collapsible>
						))}
					</SidebarMenu>
				</SidebarGroup>
			</SidebarContent>
		</Sidebar>
	);
}

SidebarMenuBadge

SidebarMenuBadge コンポーネントは、SidebarMenuItem 内にバッジをレンダリングするために使用されます。

<SidebarMenuItem>
  <SidebarMenuButton />
  <SidebarMenuBadge>24</SidebarMenuBadge>
</SidebarMenuItem>

18 SidebarMenuSkeleton

SidebarMenuSkeletonコンポーネントは、SidebarMenuのスケルトンをレンダリングするために使用します。

React Server Components、SWRまたはreact-queryを使用するときに、ロード状態を表示するために使用できます。

app-sidebar_18.tsx
import { Sidebar, SidebarContent, SidebarMenu, SidebarMenuItem, SidebarMenuSkeleton } from "@/components/ui/sidebar";

export function AppSidebar() {
  return (
    <Sidebar>
      <SidebarContent>
        <SidebarMenu>
          {Array.from({ length: 5 }).map((_, index) => (
            <SidebarMenuItem key={index}>
              <SidebarMenuSkeleton />
            </SidebarMenuItem>
          ))}
        </SidebarMenu>
      </SidebarContent>
    </Sidebar>
  );
}

SidebarSeparator

サイドバーの中に区切り線を表示します。

<Sidebar>
  <SidebarHeader />
  <SidebarSeparator />
  <SidebarContent>
    <SidebarGroup />
    <SidebarSeparator />
    <SidebarGroup />
  </SidebarContent>
</Sidebar>

app-sidebar_18-2.tsx
import { Sidebar, SidebarContent, SidebarSeparator, SidebarGroup, SidebarHeader, } from "@/components/ui/sidebar";

export function AppSidebar() {
  return (
    <Sidebar>
      <SidebarHeader />
      <SidebarSeparator />
      1
      <SidebarSeparator />
      2
      <SidebarSeparator />
      3
      <SidebarSeparator />
      <SidebarContent>
        <SidebarGroup />
        <SidebarSeparator />
        <SidebarGroup />
      </SidebarContent>
    </Sidebar>
  );
}


19 SidebarTrigger

サイドバーを切り替えるボタンをレンダリングするには、SidebarTriggerコンポーネントを使用します。

SidebarTriggerコンポーネントは、SidebarProvider内で使用する必要があります。

<SidebarProvider>
  <Sidebar />
  <main>
    <SidebarTrigger />
  </main>
</SidebarProvider>

app-sidebar_19.tsx
import { Sidebar, SidebarContent } from "@/components/ui/sidebar";

export function AppSidebar() {
	return (
		<Sidebar>
			<SidebarContent />
		</Sidebar>
	);
}

19/layout.tsx
import { AppSidebar } from "@/components/app-sidebar_19";
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";

export default function Layout({ children }: { children: React.ReactNode }) {
	return (
		<SidebarProvider>
			<AppSidebar />
			<main>
				<SidebarTrigger />
				{children}
			</main>
		</SidebarProvider>
	);
}

20 Custom Trigger

カスタムトリガーを作成するには、useSidebarフックを使用します。

app-sidebar_20.tsx
import { useSidebar } from "@/components/ui/sidebar"

export function CustomTrigger() {
  const { toggleSidebar } = useSidebar()

  return <button onClick={toggleSidebar}>Toggle Sidebar</button>
}

20/layout.tsx
"use client";
// 👆必要

import { AppSidebar } from "@/components/app-sidebar_20";
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { useSidebar } from "@/components/ui/sidebar";

function CustomTrigger() {
	const { toggleSidebar } = useSidebar();

	return (
		<button type="button" onClick={toggleSidebar}>
			Toggle Sidebar
		</button>
	);
}

export default function Layout({ children }: { children: React.ReactNode }) {
	return (
		<SidebarProvider>
			<AppSidebar />
			<main>
				{/* カスタムトリガー */}
				<CustomTrigger />
				{children}
			</main>
		</SidebarProvider>
	);
}

21 SidebarRail

サイドバー内のレールをレンダリングするために使用されます。 このレールは、サイドバーを切り替えるために使用することができます。

SidebarTriggerがサイドバーの外に配置されるのに対して、
SidebarRailはサイドバー自体の中に配置されるのが大きな違いです。

<Sidebar>
  <SidebarHeader />
  <SidebarContent>
    <SidebarGroup />
  </SidebarContent>
  <SidebarFooter />
  111
  <SidebarRail />
  222
</Sidebar>

Railとは
サイドバーの表示/非表示を切り替えるボタンを表示するための領域を確保することです。

通常はここに、サイドバーを開閉するためのボタンなどを配置します。

SidebarRail 自体は単にレールを表示するためのコンポーネントなので、具体的なボタンなどの要素は自分で用意する必要があります。


サイトバートリガーとサイドバーレール

なぜ単に閉じる機能だけでなく、SidebarTriggerSidebarRailといった、サイドバーの開閉を操作するための複数の方法が用意されているのか?

これは主に、ユーザーエクスペリエンス(UX)とデザインの柔軟性のためです。それぞれのコンポーネントは、異なる状況やデザイン要件に対応できるように設計されています。

  • SidebarTrigger

これは、サイドバーの外側に配置されることを想定しています。
例えば、ヘッダー部分にハンバーガーメニューアイコンとして配置したり、メインコンテンツの上部にボタンとして配置したりします。
これにより、サイドバーが閉じている状態でも、ユーザーは明確に「サイドバーを開ける場所」を認識できます。
特に、モバイルファーストなデザインや、コンテンツを広く表示したい場合に適しています。

  • SidebarRail

これは、サイドバーの内側、通常は端に配置されます。
サイドバーが閉じている状態では、細い線やアイコンとして表示され、ユーザーはそれをクリックまたはドラッグすることでサイドバーを開くことができます。
これは、サイドバーが常に部分的に表示されているようなデザイン(例えば、ナビゲーションアイコンだけが表示されている状態)でよく使われます。
デスクトップアプリケーションのような、常に何らかのナビゲーション要素を表示しておきたい場合に適しています。

つまり、
完全に隠れたサイドバーを開く場合はSidebarTrigger
部分的に表示されたサイドバーを展開/格納する場合はSidebarRail
という使い分けが考えられます。

例を挙げると、
スマートフォンのウェブサイト:画面が狭いため、通常はサイドバーを隠しておき、ハンバーガーメニュー(SidebarTrigger)をクリックして表示するのが一般的です。
デスクトップのウェブアプリケーション:画面が広いため、常にサイドバーの一部(アイコンなど)を表示しておき、それをクリックして展開する(SidebarRail)のが一般的です。

このように、SidebarTriggerSidebarRailは、それぞれ異なるデザインパターンやユーザーの操作方法に対応するために用意されています。
どちらを使うかは、ウェブサイトやアプリケーションのデザインや要件によって決まります。
また、両方を同時に使うことも可能です。例えば、モバイルではSidebarTriggerを使い、デスクトップではSidebarRailを使う、といった実装もできます。
要するに、これらのコンポーネントは、開発者がより柔軟にサイドバーの挙動を制御し、最適なユーザーエクスペリエンスを提供できるようにするためのものです。


Data Fetching

React Server Components

以下は、React Server Componentsを使用してプロジェクトのリストをレンダリングするSidebarMenuコンポーネントの例です。

Skeleton to show loading state.

function NavProjectsSkeleton() {
  return (
    <SidebarMenu>
      {Array.from({ length: 5 }).map((_, index) => (
        <SidebarMenuItem key={index}>
          <SidebarMenuSkeleton showIcon />
        </SidebarMenuItem>
      ))}
    </SidebarMenu>
  )
}

22 Server component fetching data.

async function NavProjects() {
  const projects = await fetchProjects()

  return (
    <SidebarMenu>
      {projects.map((project) => (
        <SidebarMenuItem key={project.name}>
          <SidebarMenuButton asChild>
            <a href={project.url}>
              <project.icon />
              <span>{project.name}</span>
            </a>
          </SidebarMenuButton>
        </SidebarMenuItem>
      ))}
    </SidebarMenu>
  )
}

23 Usage with React Suspense.

function AppSidebar() {
  return (
    <Sidebar>
      <SidebarContent>
        <SidebarGroup>
          <SidebarGroupLabel>Projects</SidebarGroupLabel>
          <SidebarGroupContent>
            <React.Suspense fallback={<NavProjectsSkeleton />}>
              <NavProjects />
            </React.Suspense>
          </SidebarGroupContent>
        </SidebarGroup>
      </SidebarContent>
    </Sidebar>
  )
}

SWR and React Query

SWRやreact-queryでも同じアプローチが使えます。

function NavProjects() {
  const { data, isLoading } = useSWR("/api/projects", fetcher)

  if (isLoading) {
    return (
      <SidebarMenu>
        {Array.from({ length: 5 }).map((_, index) => (
          <SidebarMenuItem key={index}>
            <SidebarMenuSkeleton showIcon />
          </SidebarMenuItem>
        ))}
      </SidebarMenu>
    )
  }

  if (!data) {
    return ...
  }

  return (
    <SidebarMenu>
      {data.map((project) => (
        <SidebarMenuItem key={project.name}>
          <SidebarMenuButton asChild>
            <a href={project.url}>
              <project.icon />
              <span>{project.name}</span>
            </a>
          </SidebarMenuButton>
        </SidebarMenuItem>
      ))}
    </SidebarMenu>
  )
}

function NavProjects() {
  const { data, isLoading } = useQuery()

  if (isLoading) {
    return (
      <SidebarMenu>
        {Array.from({ length: 5 }).map((_, index) => (
          <SidebarMenuItem key={index}>
            <SidebarMenuSkeleton showIcon />
          </SidebarMenuItem>
        ))}
      </SidebarMenu>
    )
  }

  if (!data) {
    return ...
  }

  return (
    <SidebarMenu>
      {data.map((project) => (
        <SidebarMenuItem key={project.name}>
          <SidebarMenuButton asChild>
            <a href={project.url}>
              <project.icon />
              <span>{project.name}</span>
            </a>
          </SidebarMenuButton>
        </SidebarMenuItem>
      ))}
    </SidebarMenu>
  )
}


Controlled Sidebar

サイドバーを制御するには、openとonOpenChangeプロップを使用します。

このコードは、サイドバーを制御する方法を示しています。openonOpenChange というプロップを使って、サイドバーの開閉状態を管理します。

export function AppSidebar() {
  const [open, setOpen] = React.useState(false);

  return (
    <SidebarProvider open={open} onOpenChange={setOpen}>
      <Sidebar />
    </SidebarProvider>
  );
}

React.useState(false) を使って、サイドバーが開いているかどうかを管理する open という状態を作ります。
初期値は false です。

SidebarProvider コンポーネントに openonOpenChange プロップを渡します。

  • open はサイドバーが開いているかどうかを示します。
  • onOpenChange はサイドバーの開閉状態が変わったときに呼ばれる関数です。

これにより、サイドバーの開閉状態を簡単に管理できます。

export function AppSidebar() {
  const [open, setOpen] = React.useState(false)

  return (
    <SidebarProvider open={open} onOpenChange={setOpen}>
      <Sidebar />
    </SidebarProvider>
  )
}

Theming

サイドバーのテーマ設定には次の CSS 変数を使用します。

@layer base {
  :root {
    --sidebar-background: 0 0% 98%;
    --sidebar-foreground: 240 5.3% 26.1%;
    --sidebar-primary: 240 5.9% 10%;
    --sidebar-primary-foreground: 0 0% 98%;
    --sidebar-accent: 240 4.8% 95.9%;
    --sidebar-accent-foreground: 240 5.9% 10%;
    --sidebar-border: 220 13% 91%;
    --sidebar-ring: 217.2 91.2% 59.8%;
  }

  .dark {
    --sidebar-background: 240 5.9% 10%;
    --sidebar-foreground: 240 4.8% 95.9%;
    --sidebar-primary: 0 0% 98%;
    --sidebar-primary-foreground: 240 5.9% 10%;
    --sidebar-accent: 240 3.7% 15.9%;
    --sidebar-accent-foreground: 240 4.8% 95.9%;
    --sidebar-border: 240 3.7% 15.9%;
    --sidebar-ring: 217.2 91.2% 59.8%;
  }
}

意図的にサイドバーとアプリケーションの残りの部分に異なる変数を使い、アプリケーションの残りの部分とは異なるスタイルのサイドバーを簡単に持てるようにしています。 メインアプリケーションとは異なる暗い色合いのサイドバーを考えてみてください。

Styling

ここでは、異なる状態に基づいたサイドバーのスタイルのヒントをいくつか紹介します。

サイドバーの折りたたみ可能な状態に基づいて要素をスタイリングします。 以下は、サイドバーがアイコンモードのとき、SidebarGroupを非表示にします。

<Sidebar collapsible="icon">
  <SidebarContent>
    <SidebarGroup className="group-data-[collapsible=icon]:hidden" />
  </SidebarContent>
</Sidebar>

メニューボタンのアクティブ状態に応じてメニューアクションをスタイリングする。 以下のようにすると、メニューボタンがアクティブなときにメニューアクションが表示されるようになります。

<SidebarMenuItem>
  <SidebarMenuButton />
  <SidebarMenuAction className="peer-data-[active=true]/menu-button:opacity-100" />
</SidebarMenuItem>


Changelog

最新ログ
2024-10-30 Cookie handling in setOpen



1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?