Next.jsでNotionライクなリサイズ可能なサイドバーを実装しよう
IT技術
はじめに
こんにちは! ふとNotionのようなリサイズ可能なサイドバーの実装方法が気になったので、調べたことをコードを交えてステップバイステップで実装し共有しようと思います。
ステップ1 基本構造の作成
今回は以下のようなUIのダッシュボードをSidebar / Navbar / MainContentコンポーネントを元に作成したいと思います。まずはこれら各コンポーネントの基本的なコードを実装していきます。
src/app/page.tsx
1import MainContent from "@/components/MainContent";
2import Navbar from "@/components/Navbar";
3import Sidebar from "@/components/Sidebar";
4
5export default function Home() {
6 return (
7 <div className="h-screen flex flex-col">
8 <div className="flex-1 flex overflow-hidden">
9 <Sidebar />
10 <div className="flex-1 flex flex-col">
11 <Navbar />
12 <MainContent />
13 </div>
14 </div>
15 </div>
16 );
17}
src/components/Sidebar/index.tsx
1import React from 'react';
2
3const Sidebar = () => {
4 return (
5 <aside className="h-full overflow-y-auto p-4 font-semibold">
6 Sidebar
7 </aside>
8 );
9};
10
11export default Sidebar;
src/components/Navbar/index.tsx
1import React from 'react';
2
3const Navbar = () => {
4 return (
5 <nav className="w-full p-4 font-semibold">
6 Navbar
7 </nav>
8 );
9};
10
11export default Navbar;
src/components/MainContent/index.tsx
1import React from 'react';
2
3const MainContent = () => {
4 return (
5 <div className="flex-1 overflow-y-auto bg-zinc-100 p-4 font-semibold">
6 Main Content
7 </div>
8 );
9};
10
11export default MainContent;
これらの基本的なコンポーネント実装により以下のようなUIになるかと思います。
ステップ2 サイドバーをリサイズ可能にするためのロジックを追加
このステップでは実際にリサイズ可能なサイドバーを実装します。
classNameを動的に変更するためにclsx
とtailwind-merge
を追加します。
1npm i clsx tailwind-merge
src/lib/utils.ts
1import { type ClassValue, clsx } from 'clsx';
2import { twMerge } from 'tailwind-merge';
3
4export function cn(...inputs: ClassValue[]) {
5 return twMerge(clsx(inputs));
6}
ロジックをカスタムフックとして実装し、コードの複雑さを解消します。
src/hooks/useSidebarResize.ts
1'use client';
2
3import { useCallback, useRef, useState } from 'react';
4
5export const useSidebarResize = () => {
6 const MIN_SIDEBAR_WIDTH = 200;
7 const MAX_SIDEBAR_WIDTH = 480;
8 const isResizingRef = useRef(false);
9
10 const [sidebarWidth, setSidebarWidth] = useState(MIN_SIDEBAR_WIDTH);
11
12 const [isDragging, setIsDragging] = useState(false);
13
14 const handleMouseMove = useCallback((event: MouseEvent) => {
15 if (!isResizingRef.current) return;
16 //マウスの現在位置を取得
17 let newWidth = event.clientX;
18
19 if (newWidth < MIN_SIDEBAR_WIDTH) newWidth = MIN_SIDEBAR_WIDTH;
20 if (newWidth > MAX_SIDEBAR_WIDTH) newWidth = MAX_SIDEBAR_WIDTH;
21
22 setSidebarWidth(newWidth);
23 }, []);
24
25 // ドラッグ操作を開始したときに呼び出される
26 const handleMouseDown = (
27 event: React.MouseEvent,
28 ) => {
29 event.preventDefault();
30 // リサイズを開始
31 isResizingRef.current = true;
32 // ドラッグ状態を開始
33 setIsDragging(true);
34 document.addEventListener('mousemove', handleMouseMove);
35 document.addEventListener('mouseup', handleMouseUp);
36 };
37
38 // ドラッグ操作を終了したときに呼び出される
39 const handleMouseUp = () => {
40 // リサイズを終了
41 isResizingRef.current = false;
42 // ドラッグ状態を終了
43 setIsDragging(false);
44 document.removeEventListener('mousemove', handleMouseMove);
45 document.removeEventListener('mouseup', handleMouseUp);
46 };
47
48 return { sidebarWidth, isDragging, handleMouseDown };
49};
src/components/Sidebar/index.tsx
1import { useSidebarResize } from '@/hooks/useSidebarResize';
2import React from 'react';
3import { cn } from '@/lib/utils';
4
5const Sidebar = () => {
6 const { sidebarWidth, isDragging, handleMouseDown } = useSidebarResize();
7
8 return (
9 <aside
10 style={{
11 width: sidebarWidth,
12 transition: isDragging ? 'none' : 'width 0.3s ease-in-out',
13 }}
14 className={cn(
15 'h-full overflow-y-auto relative flex flex-col z-[100] transition-all duration-300 ease-in-out p-4 font-semibold'
16 )}
17 >
18 Sidebar
19 <div
20 onMouseDown={handleMouseDown}
21 className="cursor-ew-resize absolute h-full w-1 right-0 top-0"
22 />
23 </aside>
24 );
25};
26
27export default Sidebar;
上記のコードにより、サイドバーをドラッグすることでwidthを制限した範囲内でリサイズできるようになります!
ステップ3 サイドバーを開閉するためのロジックを追加する
このステップではドラッグによるリサイズに加え、サイドバーにアイコンを追加し、トグルでサイドバーを開閉できるようにします。
以下のコードでreact-iconsを追加してください。
1npm i react-icons
src/app/page.tsx
1'use client'
2
3import MainContent from "@/components/MainContent";
4import Navbar from "@/components/Navbar";
5import Sidebar from "@/components/Sidebar";
6import { useState } from "react";
7
8export default function Home() {
9 // サイドバーが折りたたまれているかどうかの状態管理を追加
10 const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
11 // 状態管理をトグルする関数の追加
12 const handleToggleSidebar = () => {
13 setIsSidebarCollapsed(!isSidebarCollapsed);
14 };
15
16 return (
17 <div className="h-screen flex flex-col">
18 <div className="flex-1 flex overflow-hidden">
19 <Sidebar
20 isCollapsed={isSidebarCollapsed}
21 onToggle={handleToggleSidebar}
22 />
23 <div className="flex-1 flex flex-col">
24 <Navbar
25 isCollapsed={isSidebarCollapsed}
26 onToggleSidebar={handleToggleSidebar}
27 />
28 <MainContent />
29 </div>
30 </div>
31 </div>
32 );
33}
src/components/Sidebar/index.tsx
1import { useSidebarResize } from '@/hooks/useSidebarResize';
2import React from 'react';
3import { cn } from '@/lib/utils';
4import { MdKeyboardDoubleArrowLeft } from "react-icons/md";
5
6// 受け取る引数の型宣言
7type SidebarProps = {
8 isCollapsed: boolean;
9 onToggle: () => void;
10};
11
12const Sidebar:React.FC<SidebarProps> = ({
13 isCollapsed,
14 onToggle,
15}) => {
16 const { sidebarWidth, isDragging, handleMouseDown } = useSidebarResize(isCollapsed);
17
18 return (
19 <aside
20 style={{
21 // isCollapsedがtrueの場合はwidthを0、それ以外はsidebarWidthにする
22 width: isCollapsed ? 0 : sidebarWidth,
23 transition: isDragging ? 'none' : 'width 0.1s ease-in-out',
24 }}
25 className={cn(
26 'h-full overflow-y-auto relative flex flex-col z-[100] font-semibold p-4',
27 isCollapsed ? 'w-0 p-0' : 'transition-all duration-300 ease-in-out',
28 )}
29 >
30 <div
31 onClick={onToggle}
32 role="button"
33 className={cn(
34 'h-6 w-6 rounded-sm hover:bg-neutral-200 absolute top-4 right-4',
35 isCollapsed ? 'hidden' : 'block',
36 )}
37 >
38 <MdKeyboardDoubleArrowLeft className="h-6 w-6" />
39 </div>
40 Sidebar
41 <div
42 onMouseDown={handleMouseDown}
43 className="cursor-ew-resize absolute h-full w-1 right-0 top-0"
44 />
45 </aside>
46 );
47};
48
49export default Sidebar;
src/hooks/useSidebarResize.tsに変更を加えます。
- 引数にisCollapsedを持たせる
1export const useSidebarResize = (isCollapsed: boolean) => {
- サイドバーのwidthをisCollapsedの状態によって動的に変更
1const [sidebarWidth, setSidebarWidth] = useState(
2 isCollapsed ? 0 : MIN_SIDEBAR_WIDTH,
3);
src/components/Navbar/index.tsx
1import React from 'react';
2import { MdMenu } from "react-icons/md";
3
4//受け取る引数の型宣言
5type NavbarProps = {
6 isCollapsed: boolean;
7 onToggleSidebar: () => void;
8};
9
10const Navbar: React.FC<NavbarProps> = ({
11 isCollapsed,
12 onToggleSidebar,
13}) => {
14 return (
15 <nav className="p-4 w-full font-semibold flex">
16 {/* isCollapsedがtrueの場合に表示 (= サイドバーが折りたたまれている時) */}
17 {isCollapsed && (
18 <MdMenu
19 onClick={onToggleSidebar}
20 role="button"
21 className="h-6 w-6 cursor-pointer mr-4"
22 />
23 )}
24 Navbar
25 </nav>
26 );
27};
28
29export default Navbar;
これら上記の実装により、サイドバーのwidthをドラッグで変えられるだけでなく、アイコンをクリックすることでサイドバーを開閉できるようになり、Notionで実装されているようなリサイズ可能なサイドバーを実装できました。
最後に
いかがだったでしょうか。Notionのようなリサイズできるサイドバーを実装できたかと思います。他にも気になる実装方法があればまたブログを通して共有していこうと思います。最後まで読んでいただき、ありがとうございました。
参考サイト:Fullstack Notion Clone: Next.js 13, React, Convex, Tailwind | Full Course 2023
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
2024年4月から入社いたしました。24歳の道産子、櫻田と申します。 日々学び、いち早く戦力になれるよう精進してまいりますのでどうぞよろしくお願いいたします。趣味はNBA観戦と音楽を聴くことです。