📝

tailwindcssでAdmin向けの管理画面レイアウトを作る

2025/01/19に公開

https://www.youtube.com/watch?v=W2Hl8rtRZ2M
を参考にAdmin向けの管理画面レイアウトを作った。

レイアウトのみの完成形は以下。

セットアップ

ホットリロードを利用したいためtailwindcliではなくviteで構築をした。

https://zenn.dev/skakimoto/articles/2025-01-14-tailwindcss-from-zero-to-production#postcssを利用してtailwindcssをインストール-https%3A%2F%2Ftailwindcss.com%2Fdocs%2Finstallation%2Fusing-postcss

fontsを設定する

inter fontの埋め込みコードを取得する。

https://fonts.google.com/selection/embed

themeにfontFamilyを追加することで、font-interクラスが利用できる。

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["index.html"],
  theme: {
    extend: {
+      fontFamily: {
+        'inter': ['Inter', 'sans-serif'],
+      },
    },
  },
  plugins: [],
}

https://tailwindcss.com/docs/font-family#customizing-your-theme

サイドバーの作成

レイアウトの指定

fixedクラスを利用して、サイドバーを画面上部にレイアウト固定する

https://tailwindcss.com/docs/position#fixed-positioning-elements

<body class="text-gray-800 font-inter">
+  <!-- Sidebar -->
+  <div class="fixed left-0 top-0 w-64 h-full bg-gray-900">
+  </div>
</div>

ロゴの指定

ロゴデザイン用の検証にはplaceholdを利用する。

https://placehold.co/

object-coverはアスペクト比を維持したまま、コンテンツボックス全体を埋める。

https://developer.mozilla.org/ja/docs/Web/CSS/object-fit

  <!-- Sidebar -->
  <div class="fixed left-0 top-0 w-64 h-full bg-gray-900 p-4">
+    <div class="flex items-center">
+      <img src="https://placehold.co/40x40" alt="" class="w-10 h-10 rounded object-cover">
+      <span class="text-lg font-bold text-gray-200">Logo</span>
    </div>
  </div>

aタグに変更し、aタグの下にborderを配置する

  <!-- Sidebar -->
  <div class="fixed left-0 top-0 w-64 h-full bg-gray-900 p-4">
+    <a href="#" class="flex items-center pb-4 border-b border-gray-800">
      <img src="https://placehold.co/32x32" alt="" class="w-8 h-8 rounded object-cover">
      <span class="text-lg font-bold text-white ml-3">Logo</span>
+    </a>
  </div>

サイドメニューの作成

メニューアイコンには、remixiconを利用する。

https://remixicon.com/

  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link href="https://cdn.jsdelivr.net/npm/remixicon@4.5.0/fonts/remixicon.css" rel="stylesheet"/>

メニューアイコンをグループでまとめてアイコンを適用する。groupクラスにより、親の要素クラスの状態に基づいて、cssを適用する。

https://tailwindcss.com/docs/hover-focus-and-other-states#arbitrary-groups

  <!-- Sidebar -->
  <div class="fixed left-0 top-0 w-64 h-full bg-gray-900 p-4">
    <a href="#" class="flex items-center pb-4 border-b border-gray-800">
      <img src="https://placehold.co/32x32" alt="" class="w-8 h-8 rounded object-cover">
      <span class="text-lg font-bold text-white ml-3">Logo</span>
    </a>
+    <ul class="mt-4">
+      <li class="mb-1 group active">
+        <a href="#" class="flex items-center py-2 px-4 text-gray-300 hover:bg-gray-950 rounded-md group-[.active]:text-white">
+          <i class="ri-home-2-line mr-3 text-lg"></i>
+          <span class="text-sm">Dashboard</span>
+        </a>
+      </li>
+       ...
+    </ul>
  </div>

ネストメニューアイコンを作る

Ordersにネストメニューアイコンを作る

+      <li class="mb-1 group">
+        <a href="#" class="flex items-center py-2 px-4 text-gray-300 hover:bg-gray-950 rounded-md group-[.active]:text-white">
+          <i class="ri-instance-line mr-3 text-lg"></i>
+          <span class="text-sm">Orders</span>
+        </a>
+        <ul class="pl-7 mt-2">
+          <li class="mb-4">
+            <a href="#" class="text-gray-300 text-sm flex items-center before:content-[''] before:w-1 before:h-1 before:bg-gray-300 before:rounded-full before:mr-3">Active order</a>
+          </li>

矢印アイコンを設定する。

      <li class="mb-1 group">
        <a href="#" class="flex items-center py-2 px-4 text-gray-300 hover:bg-gray-950 rounded-md group-[.active]:text-white">
          <i class="ri-instance-line mr-3 text-lg"></i>
          <span class="text-sm">Orders</span>
+          <i class="ri-arrow-right-s-line ml-auto group-[.active]:rotate-90"></i>
        </a>

jsによるサイドバーのドロップダウン制御

https://youtu.be/W2Hl8rtRZ2M?si=rYlxlOGgou8k-T-X&t=984

sidebarのdropdownをjsで制御する。

// Sidebar
document.querySelectorAll(".sidebar-dropdown-toggle").forEach((item) => {
  item.addEventListener("click", (e) => {
    e.preventDefault();
    const parent = item.closest(".group");
    if (parent.classList.contains("selected")) {
      // 閉じる
      parent.classList.remove("selected");
    } else {
      // 開く
      document.querySelectorAll(".sidebar-dropdown-toggle").forEach((i) => {
        i.closest(".group").classList.remove("selected");
      });
      parent.classList.add("selected");
    }
  });
});

サイドバーラップアップ

sidebarのtoggleに関しては、class addによる制御ではなくdata-属性の利用もできる。
classによるtoggleは存在しないクラス名を制御のため追加することになるので、アプリケーションの状態についてはdata-属性を利用するほうがいいかと思われる。
tailwind3.2で導入されたdata-attributes-variantsを使えば、cssではなく状態をdata-*に記載することができる。
https://tailwindcss.com/blog/tailwindcss-v3-2#data-attribute-variants

https://railsdesigner.com/data-attributes-magic/ を参考に実行してみた。

      <li class="mb-1 group">
        <a href="#" class="flex items-center py-2 px-4 text-gray-300 hover:bg-gray-950 hover:text-gray-100 rounded-md group-[.active]:bg-gray-800 group-[.active]:text-white group-data-[state-opened]:bg-gray-950 group-data-[state-opened]:text-gray-100 sidebar-dropdown-toggle">
          <i class="ri-instance-line mr-3 text-lg"></i>
          <span class="text-sm">Orders</span>
          <i class="ri-arrow-right-s-line ml-auto group-data-[state-opened]:rotate-90"></i>
        </a>
        <ul class="pl-7 mt-2 hidden group-data-[state-opened]:block">
          <li class="mb-4">
            <a href="#" class="text-gray-300 text-sm flex items-center hover:text-gray-100 before:contents-[''] before:w-1 before:h-1 before:rounded-full before:bg-gray-300 before:mr-3">Active order</a>
          </li> 
          <li class="mb-4">
            <a href="#" class="text-gray-300 text-sm flex items-center hover:text-gray-100 before:contents-[''] before:w-1 before:h-1 before:rounded-full before:bg-gray-300 before:mr-3">Completed order</a>
          </li> 
          <li class="mb-4">
            <a href="#" class="text-gray-300 text-sm flex items-center hover:text-gray-100 before:contents-[''] before:w-1 before:h-1 before:rounded-full before:bg-gray-300 before:mr-3">Canceled order</a>
          </li> 
        </ul>
      </li>
// Sidebar
document.querySelectorAll(".sidebar-dropdown-toggle").forEach((item) => {
  item.addEventListener("click", (e) => {
    e.preventDefault();
    const parent = item.closest(".group");
    if (parent.hasAttribute("data-state-opened")) {
        // 閉じる
        parent.removeAttribute("data-state-opened");
    } else {
        // 開く
        document.querySelectorAll(".sidebar-dropdown-toggle").forEach((i) => {
            i.closest(".group").removeAttribute("data-state-opened");
        });
        parent.toggleAttribute("data-state-opened");
    }
  });
});

Mainコンテンツ

https://youtu.be/W2Hl8rtRZ2M?si=1NNPpCpLdrdq1AY9&t=1364

Mainコンテンツ枠を作る

+  <main class="w-[calc(100%-256px)] ml-64">
+    <div class="py-2 px-4 hg-white flex items-center">
+      Hi
+    </div>
+  </main>

shadow-black/5で透明を5%に指定する。

https://tailwindcss.com/docs/box-shadow-color#setting-the-box-shadow-color

パンくずの指定

  <!-- Main -->
  <main class="w-[calc(100%-256px)] ml-64 bg-gray-50 min-h-screen">
+    <div class="py-2 px-4 hg-white flex items-center shadow-md shadow-black/5">
+      <button type="button" class="text-lg text-gray-600">
+        <i class="ri-menu-line"></i>
+      </button>
+      <ul class="flex items-center ml-4">
+        <li class="mr-2">
+          <a href="#" class="text-gray-400 text-sm hover:text-gray-600">Dashboard</a>
+        </li>
+        <li class="text-gray-600 mr-2 font-medium">/</li>
+        <li class="text-gray-600 mr-2 font-medium">Analytics</li>
+      </ul>
    </div>
  </main>

Searchメニューアイコンを作る

ml-autoで、利用可能なスペースをすべて左マージンとして利用するため、Searchメニューアイコンは右寄せになる。

https://coliss.com/articles/build-websites/operation/css/all-about-margin-auto-in-css.html

+      <!-- ヘッダーの右側 -->
+      <ul class="ml-auto flex items-center">
+        <li>
+          <button type="button" class="text-gray-400 w-8 h-8 rounded flex items-center justify-center hover:bg-gray-50 hover:text-gray-600">
+            <i class="ri-search-line"></i>
+          </button>
+        </li>
+        <li>
+          <button type="button" class="text-gray-400 w-8 h-8 rounded flex items-center justify-center hover:bg-gray-50 hover:text-gray-600">
+            <i class="ri-notification-3-line"></i>
+          </button>
+        </li>
+        <li>
+          <button type="button">
+            <img src="https://placehold.co/32x32" alt="" class="w-8 h-8 rounded block object-cover align-middle">
+          </button>
+        </li>
+      </ul>

https://youtu.be/W2Hl8rtRZ2M?si=SBPtqq2wKshROcSr&t=1803

           <button type="button" class="text-gray-400 w-8 h-8 rounded flex items-center justify-center hover:bg-gray-50 hover:text-gray-600">
             <i class="ri-search-line"></i>
           </button>
+          <div class="dropdown-menu absolute max-w-xs w-full bg-white rounded-md border border-gray-100">
+            <form action="" class="p-4">
+              <div class="relative w-full">
+                <input type="text" class="py-2 pr-4 pl-10 bg-gray-50 w-full outline-none border border-gray-100 rounded-md text-sm focus:border-blue-500" palceholder="Search...">
+                <i class="ri-search-line absolute top-1/2 left-4 -translate-y-1/2 text-gray-400"></i>
+              </div>
+            </form>
+          </div>
         </li>
       <!-- ヘッダーの右側 -->
       <ul class="ml-auto flex items-center">
-        <li>
-          <button type="button" class="text-gray-400 w-8 h-8 rounded flex items-center justify-center hover:bg-gray-50 hover:text-gray-600">
+        <li class="mr-1 dropdown">
+          <button type="button" class="dropdown-toggle text-gray-400 w-8 h-8 rounded flex items-center justify-center hover:bg-gray-50 hover:text-gray-600">
             <i class="ri-search-line"></i>
           </button>
-          <div class="dropdown-menu absolute max-w-xs w-full bg-white rounded-md border border-gray-100">
-            <form action="" class="p-4">
+          <div class="dropdown-menu hidden absolute max-w-xs w-full bg-white rounded-md border border-gray-100">
+            <form action="" class="p-4 border-b border-gray-100">
               <div class="relative w-full">
                 <input type="text" class="py-2 pr-4 pl-10 bg-gray-50 w-full outline-none border border-gray-100 rounded-md text-sm focus:border-blue-500" palceholder="Search...">
                 <i class="ri-search-line absolute top-1/2 left-4 -translate-y-1/2 text-gray-400"></i>
             </form>
           </div>
         </li>
-        <li>
+        <li class="mr-1">
           <button type="button" class="text-gray-400 w-8 h-8 rounded flex items-center justify-center hover:bg-gray-50 hover:text-gray-600">
             <i class="ri-notification-3-line"></i>
           </button>
       </ul>
     </div>
   </main>
+  <script src="https://unpkg.com/@popperjs/core@2"></script>
   <script src="/src/js/script.js"></script>
 </body>
 </html>

お知らせ枠の作成

https://youtu.be/W2Hl8rtRZ2M?si=QzGP1G6qS2Y-UEWG&t=2745

truncateクラスでテキストオーバーフロー時の挙動を設定する

https://tailwindcss.com/docs/text-overflow#truncate

             </form>
+            <div class="mt-3 mb-2">
+              <div class="text-[13px] font-medium text-gray-400 ml-4 mb-2">Recent</div>
+              <ul class="max-h-64 overflow-y-auto">
+                <li>
+                  <a href="#" class="py-2 px-4 flex items-center hover:bg-gray-50 group">
+                    <img src="https://placehold.co/32x32" alt="" class="w-8 h-8 rounded block object-cover align-middle">
+                    <div class="ml-2">
+                      <div class="text-[13px] text-gray-600 font-medium truncate group-hover:text-blue-500">Create landing page</div>
+                      <div class="text-[12px] text-gray-600">$345</div>
+                    </div>
+                  </a>
+                </li>
+                <li>

ラップアップ

管理画面レイアウトをtailwindcssを作って構築。サイドバーの構築やヘッダーの右寄せのやり方がわかった。ただ状態制御がcssを使って制御して、好みではなかったので途中離脱した。
他に管理画面レイアウトの作り方はshadcnを参考によりシンプルに実装できる気がする。

https://github.com/satnaing/shadcn-admin

https://shadcn-admin.netlify.app/

また、shadcnでもコンポーネントではなくブロックも公開されるようになっている。

https://ui.shadcn.com/blocks

Discussion