Vueでアプリケーションを構築するとユーザまたは管理者用のダッシュボードが必要になる場合があります。有料・無料を問わずネット上にはVueを利用して作成されたダッシュボードが公開されていますが本文書ではViteでVue3のプロジェクトを作成し、Composition API(script setup)を利用してスクラッチからダッシュボードのレイアウトを作成してきます。Tailwind CSSを利用してレスポンシブデザインに対応しているだけではなくダークモードへの切り替え機能の実装も行っています。ダッシュボードのレイアウトの作成のチュートリアルなのでページ上で表示する内容については現時点では触れていません。今後内容は増やしていく予定です。

ダッシュボードのレイアウトを作成を通して以下の知識を習得することができます。

  • Viteを使ったVueプロジェクトの作成方法
  • VueのComposition APIのscript setupでのコードの記述方法
  • TailwindCSSによりレスポンシブデザインとダークモード切り替え
  • ドロップダウンメニュー、アコーディオンメニューの実装方法
  • 表示・非表示の変化に対する各種アニメーション設定
  • script setupでのライフサイクルフック(onMounted, onUnmounted)の利用方法
  • Vue Routerの設定

最終的に作成するダッシュボードレイアウトは下記の通りです。レスポンシブデザインとダークモードに対応しています。

ライトモードのダッシュボード
ライトモードのダッシュボード
ダークモードのダッシュボード
ダークモードのダッシュボード
モバイル時のダッシュボード
モバイル時のダッシュボード

Viteによるプロジェクトの作成

Viteを利用してVueのプロジェクトを作成するる場合はnpm init viteコマンドを利用します。本文書ではプロジェクト名にvue-dashboardという任意の名前をつけています。各自好きな名前をつけてください。実行するとフレームワークの選択を行うことができるのでvueを選択してください。TypeScriptを選択することができますが本文書ではTypeScriptを利用していません。


 % npm init vite@latest vue-dashboard
Need to install the following packages:
  create-vite@latest
Ok to proceed? (y) y
? Select a framework: › - Use arrow-keys. Return to submit.
    vanilla
❯   vue
    react
    preact
    lit
    svelte

プロジェクトの作成が完了すると作成したプロジェクトフォルダに移動してnpm install, npm run devコマンドを実行します。


 % cd vue-dashboard
 % npm install
 % npm run dev

> vue-dashboard@0.0.0 dev
> vite

Pre-bundling dependencies:
  vue
(this will be run only when your dependencies or config have changed)

  vite v2.6.13 dev server running at:

  > Local: http://localhost:3000/
  > Network: use `--host` to expose

  ready in 461ms.

localhost:3000にブラウザからアクセスすると以下の画面が表示されます。ViteによるVueプロジェクトの作成は完了です。

viteの初期ページ
viteの初期ページ

ブラウザ上に表示されている内容はsrcフォルダのApp.vueファイルに記述されています。ファイルの中身を確認すると大半の人が見慣れているSFC(Single File Components)とは異なりscript, template, styleの順番で記述されています。またscriptタグの中にはsetupと追加されています。


<script setup>
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <HelloWorld msg="Hello Vue 3 + Vite" />
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

script setupの下に記述されているコメントのhttps://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setupにアクセスするとscript setupを利用した場合のコードの記述方法が記載されています。script setupはSignle File Components(SFC)の中でComposition APIを使ってコードを記述する際に推奨される方法でComposition APIをより簡潔にコードを記述することができます。この時点でscript setupの記述方法がわからなくても本文書を通して記述方法を理解することができるようになると思うので安心してください。

Vue2までの記述方法はOptions APIと呼ばれます。Vue3からはOptions APIだけではなくComposition APIを利用して記述することができます。さらにComposition APIではscript setupで簡潔にコードを記述することができます。Options APIとComposition APIは記述方法が大きくことなりますがCompostion APIとsetup scriptを使って記述方法は大きな違いはありません。
fukidashi

Visual Studio Codeの拡張機能 Volar

Visual Studio Codeを利用している場合は拡張機能のVolar(Vue Language Features)をインストールしておきます。

Tailwind CSSのインストール

CSSの設定にはTailwind CSSを利用します。Tailwind CSSのドキュメントを参考にVite環境でのTailwind CSSのインストールを行います。


 % npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

パッケージのインストール後にTailwind CSSの設定ファイルを作成するために以下のコマンドを実行します。実行後にtailwind.config.jsとpostcss.config.jsファイルが作成されます。


 % npx tailwindcss init -p

Created Tailwind CSS config file: tailwind.config.js
Created PostCSS config file: postcss.config.js

tailwind.config.jsファイルを開いてpurgeオプションの設定を行います。ダークモードの設定はtailwind.config.jsファイルで行うことができます。設定は後ほどdarkModeオプションを利用して行います。


module.exports = {
  purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

srcフォルダにindex.cssを作成してtailwindのディレクティブを追加します。


@tailwind base;
@tailwind components;
@tailwind utilities;

作成したindex.cssファイルはsrcフォルダのmain.jsファイルでimportします。


import { createApp } from 'vue';
import App from './App.vue';
import './index.css';

createApp(App).mount('#app');

設定後App.vueファイルでTailwind CSSのutility classが適用できるか確認します。


<script setup></script>

<template>
  <div class="font-semibold">ダッシュボード</div>
</template>

太文字のダッシュボードがブラウザ上に表示されたらTailwind CSSの設定は問題なく完了しています。

TailwindCSSの設定確認
TailwindCSSの設定確認

ダッシュボードのレイアウトの中でアイコンを利用するため、アイコン用にheroiconsのライブラリのインストールを行っておきます。


 % npm install @heroicons/vue

レイアウトの設定

ダッシュボード用のレイアウトを作成するためにsrcフォルダにlayoutsフォルダを作成し、Default.vueファイルを作成します。

App.vueファイルでは作成したDefault.vueファイルをimportします。ダッシュボードの文字列をSlotを利用してDefaultコンポーネントに渡しています。


<script setup>
import Default from './layouts/Default.vue';
</script>

<template>
  <Default>ダッシュボード</Default>
</template>

下記の図のように主にPCのような広い画面ではサイドバーが表示されメニューアイコンをクリックするとサイドバーが表示・非表示になるような設定を行なっていきます。

サイドバーが開いた状態
サイドバーが開いた状態

メニューアイコンをクリックするとサイドバーが非表示になります。再度メニューアイコンをクリックするとサイドバーが表示されます。

サイドバーが閉じた状態
サイドバーが閉じた状態

サイドバーの表示・非表示の設定を行っていくためサイドバーとメインの領域を作成します。この時サイドバーはpositionをfixedに設定して固定しメインの領域はpadding-leftにサイドバーの広さと同じ値を設定しています。


<script setup>
</script>
<template>
  <div class="relative">
    <div class="fixed top-0 w-64 h-screen bg-white z-20">サイドバー</div>
    <div class="bg-gray-100 h-screen overflow-hidden pl-64"></div>
  </div>
</template>

右側の薄いグレーの領域をメイン領域と呼んでいます。

サイドバーとメイン領域
サイドバーとメイン領域

サイドバーを開閉するためのメイン領域にメニューバーを追加し、その中にメニューアイコンを配置します。アイコンはインストールを行ったheroiconsのライブラリからimportします。メイン領域にはslotも追加します。


<script setup>
import { MenuIcon } from '@heroicons/vue/outline';
</script>
<template>
  <div class="relative">
    <div class="fixed top-0 w-64 h-screen bg-white z-20">サイドバー</div>
    <div class="bg-gray-100 h-screen overflow-hidden pl-64">
      <div class="bg-white rounded shadow m-4 p-4">
        <MenuIcon class="h-6 w-6 text-gray-600 cursor-pointer" />
      </div>
      <div>
        <slot />
      </div>
    </div>
  </div>
</template>

メニューバーをダッシュボードの文字列の上に表示されている背景が白の領域です。中にはメニューアイコンがはいっています。

メニューを追加
メニューを追加

メニューアイコンが表示されたのでメニューアイコンをクリックするとサイドバーが開閉できるように設定を行います。開閉を制御するshow変数を追加します。show変数はrefを利用して定義します。


<script setup>
import { ref } from 'vue';
import { MenuIcon } from '@heroicons/vue/outline';

const show = ref(true);
</script>

script setupでは、show変数はそのままtemplateタグの中で利用することができます。MenuIconにclickイベントを追加します。アイコンをクリックするとshowの値がtrueの場合はfalse, falseの場合はtrueに変わります。


<MenuIcon
  class="h-6 w-6 text-gray-600 cursor-pointer"
  @click="show = !show"
/>

メイン領域ではpadding-leftの設定でサイドバーの領域分を確保していましたがクリックを押すとpadding-leftの適用を解除します。


<template>
  <div class="relative">
    <div class="fixed top-0 w-64 h-screen bg-white z-20">サイドバー</div>
    <div
      class="bg-gray-100 h-screen overflow-hidden"
      :class="{ 'pl-64': show }"
    >
      <div class="bg-white rounded shadow m-4 p-4">
        <MenuIcon
          class="h-6 w-6 text-gray-600 cursor-pointer"
          @click="show = !show"
        />
      </div>
      <div>
        <slot />
      </div>
    </div>
  </div>
</template>

クリックするとpl-64が非適用になるためメイン領域がサイドバーの下に入り込みます。

サイドバーの下にメインが入り込む
サイドバーの下にメインが入り込む

サイドバーも非表示になるようにshowの値とtransform translateXを利用してサイドバーの広さ分左側に移動させます。


<div
  class="fixed top-0 w-64 h-screen bg-white z-20 transform"
  :class="{ '-translate-x-full': !show }"
>
  サイドバー
</div>

クリックするとサイドバーが左側に消えてメイン領域が画面いっぱいに広がります。

サイドバーが非表示に
サイドバーが非表示に

PCのように画面が横長の場合はここまでの設定で問題がありませんがモバイルのように横幅が狭い場合は以下のようになりサイドバーによってメイン領域の中にコンテンツを収めるのが難しくなります。

モバイルのような幅が狭い時のサイドバー
モバイルのような幅が狭い時のサイドバー

ブラウザの幅が狭い場合はメイン領域をサイドバーに下に入り込むように設定を行います。Tailwind CSSのbreakpointsを利用してxl(1280px)より小さい場合はメイン領域をサイドバーの下に入り込むように設定します。

実現するためにpl-64の前にxl:を追加します。1280px以上の場合のみshowの値によってpl-64が適用され、1280pxより小さい場合はpl-64がshowの値がどちらであれ適用されることはありません。


<div
  class="bg-gray-100 h-screen overflow-hidden"
  :class="{ 'xl:pl-64': show }"
>

設定通りブラウザの幅が狭い場合にはサイドバーの下にメインの領域が入り込みます。

サイドバーの下に入り込む
サイドバーの下に入り込む

しかしこの状態ではメニューアイコンがサイドバーの下に隠れてしまったためメニューアイコンがクリックできずサイドバーを非表示にすることができません。メニューアイコンの代わりとなるdiv要素をサイドバー要素の下に新たに追加します。このdiv要素はpositionをfiexedで画面全体に広がるようにinset-0(top:0px,righ:0px,left:0px,bottom:0px)を設定し背景書にはopacity-50(50%)を設定しサイドバーのz-indexよりも小さい10を設定しています。showがtrueの場合のみ表示されxl(1280px)以上では非表示にしています。


<div
  class="fixed top-0 w-64 h-screen bg-white z-20 transform"
  :class="{ '-translate-x-full': !show }"
>
  サイドバー
</div>
<div
  class="fixed xl:hidden inset-0 bg-gray-900 opacity-50 z-10"
  @click="show = !show"
  v-show="show"
></div>

サイドバーが表示されている時には半透明の幕が表示されます。サイドバーとメイン領域の間に半透明の幕が表示されていることになります。半透明の幕が表示されている間メインの領域にアクセスすることはできません。

開閉用のdiv要素を追加
開閉用のdiv要素を追加

半透明の幕をクリックするとサイドバーが消え、半透明の幕も非表示になりブラウザ上にはメイン領域のみ表示されます。

アニメーションの設定

メニューボタンまたは半透明の幕をクリックするとサイドバーの開閉がすぐに切り替わるのでアニメーションの追加を行います。サイドバーのdivとメインのdivにduration-300を設定します。


<div
  class="fixed top-0 w-64 h-screen bg-white z-20 transform duration-300"
  :class="{ '-translate-x-full': !show }"
>
  サイドバー
</div>

<div
  class="bg-gray-100 h-screen overflow-hidden duration-300"
  :class="{ 'xl:pl-64': show }"
>

設定後サイドメニューを表示する場合はは左からゆっくりと表示され、非表示にする場合は右側にゆっくりと消えていきます。

アクセス時のサイドバーの表示/非表示設定

デフォルトではshowはtrueに設定されているのでブラウザの幅が広い場合も狭い場合もサイドバーが表示された状態で表示されます。特に幅の狭いモバイルの場合は最初からサイドバーが開いている必要はありません。

アクセス時のブラウザの横幅に合わせてサイドバーを表示するかしないかを設定します。

innerWidth変数を追加し、windowオプジェクトから横幅の情報を取得します。innerWidthの値によってshowの値をfalseにするかtrueにするか決めています。1280という値はbreakpointのxlの1280pxに合わせています。


const innerWidth = ref(window.innerWidth);
const show = ref(innerWidth.value >= 1280 ? true : false);

設定後はブラウザの幅が1280px以上の場合はサイドバーが表示された状態で表示され、1280pxより小さい場合はサイドバーが非表示の状態で表示させるようになります。

リサイズ時のサイドバーの表示/非表示設定

ブラウザの横幅をリサイズした際に1280px以上であれば自動でサイドバーが表示され1280px以上のサイズからリサイズして1280pxより小さい横幅になった場合にサイドバーを非表示にするといったことも可能です。

script setupタグの中にcheckWindowSize関数を追加します。


const checkWindowSize = () => {
  if (window.innerWidth >= 1280) {
    if (show.value === false && innerWidth.value < 1280) show.value = true;
  } else {
    if (show.value === true) show.value = false;
  }
  innerWidth.value = window.innerWidth;
};

ブラウザの横幅の変更はresizeイベントを利用して検知します。resizeイベントをイベントリスナーに追加します。イベントリスナーはVueのライフサイクルフックのonMountedで追加し、onUnmountedで削除しています。checkWindowSizeイベントの発生を減らすためにlodashのdebounceを利用しています。


import { ref, onMounted, onUnmounted } from 'vue';
import { MenuIcon } from '@heroicons/vue/outline';
import { debounce } from 'lodash';

const innerWidth = ref(window.innerWidth);
const show = ref(innerWidth.value >= 1280 ? true : false);

const checkWindowSize = () => {
  if (window.innerWidth >= 1280) {
    if (show.value === false && innerWidth.value < 1280) show.value = true;
  } else {
    if (show.value === true) show.value = false;
  }
  innerWidth.value = window.innerWidth;
};

onMounted(() => {
  window.addEventListener('resize', debounce(checkWindowSize, 100));
});
onUnmounted(() => {
  window.removeEventListener('resize', checkWindowSize);
});

設定後はリサイズによってサイドバーの表示・非表示が制御できるようになります。

ダークモードの設定

Tailwind CSSのDark Modeの機能を利用してダークモードの設定を行います。ダークモードの設定はTailwind CSSの設定ファイルであるtailwind.config.jsファイルにあるdarkModeオプションを利用します。デフォルトではfalseになっておりダークモードは利用できません。darkModeオプションではmediaまたはclassを設定することができます。mediaはOS(Operating System)のダークモードの設定から情報を取得してダークモードへ変更することができます。OSのダークモードの設定についてはmacOSであればシステム環境設定→一般から設定することができます。

macOS ダークモード設定
macOS ダークモード設定

classの場合は手動でダークモードへの切り替えを行うことができます。本文書では両方の設定で動作確認を行います。

mediaによるダークモード設定

macOSの場合はシステム環境設定→一般から外観モードを”ダーク”に設定しておきます。

tailwind.config.jsのdarkModeオプションをmediaに変更します。


module.exports = {
  purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  darkMode: 'media', // or 'media' or 'class'
  //略

後はダークモードに設定したいclassにdark:をつけていきます。サイドバーの背景色をダークモードの場合にbg-gray-800にしたい場合はdark:を先頭についてdark:bg-gray-800と設定します。


<div
  class="fixed top-0 w-64 h-screen bg-white dark:bg-gray-800 z-20 transform duration-300"
  :class="{ '-translate-x-full': !show }"
>
  サイドバー
</div>

ブラウザで確認するとサイドバーの領域のみ背景色がダークカラーになっていることが確認できます。mediaを設定した場合はこれだけの設定でダークモードに変更することができます。OSから情報を取得する処理はTailwind CSSが行ってくれているので何か特別の設定を行う必要がありません。

サイドバーのみダークモード設定
サイドバーのみダークモード設定

システム環境設定→一般から外観モードを”ライト”の設定に戻すとdark:で設定した値が適用されないことを確認してください。

OSのダークモードを解除
OSのダークモードを解除

再度OSの外観モードをダークに設定して他の要素にもdark:の設定を行なっていきます。各要素の背景色とテキストの文字にダークモード設定を行います。


<template>
  <div class="relative">
    <div
      class="
        fixed
        top-0
        w-64
        h-screen
        bg-white
        dark:bg-gray-800
        z-20
        transform
        duration-300
        dark:text-gray-300
      "
      :class="{ '-translate-x-full': !show }"
    >
      サイドバー
    </div>
    <div
      class="fixed xl:hidden inset-0 bg-gray-900 z-10 opacity-50"
      @click="show = !show"
      v-show="show"
    ></div>
    <div
      class="bg-gray-100 dark:bg-gray-900 h-screen overflow-hidden duration-300"
      :class="{ 'xl:pl-64': show }"
    >
      <div class="bg-white dark:bg-gray-800 rounded shadow m-4 p-4">
        <MenuIcon
          class="h-6 w-6 text-gray-600 dark:text-gray-300 cursor-pointer"
          @click="show = !show"
        />
      </div>
      <div class="dark:text-gray-300">
        <slot />
      </div>
    </div>
  </div>
</template>

表示されているページ全体がダークモードに対応することができるようになります。

ダークモードの適用
ダークモードの適用

classによるダークモード設定

tailwind.config.jsのdarkModeオプションを'class'に設定します。


module.exports = {
  purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  darkMode: 'class', // or 'media' or 'class'
  //略

default.vueはdarkModeオプションを'media'に設定した場合に作成した内容をそのまま利用します。'class'の場合はhtmlタグにclass="dark"を追加することでダークモードに変更させることができます。

index.htmlファイルを開いてhtmlタグにclass="dark"を追加してください。追加後保存するとダークモードへと変更されます。


<!DOCTYPE html>
<html lang="en" class="dark">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
ダークモードの適用
ダークモードの適用

ダークモードに変更することができましたがこのままでは手動で切り替えることはできません。一度htmlタグに追加したclass="dark"を削除します。

切り替えを行うために変数themeを追加します。themeは'light'と'dark'の値を持ちデフォルトは'light'に設定しておきます。


<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
//略
const theme = ref('light');
//略
</script>

ダークモードを切り替えるためのアイコンを追加します。アイコンは上部のメニューバーに追加します。themeの値によってダークモードの場合はSunIcon(太陽)のアイコン、ライトモードの場合はMoonIcon(月)のアイコンが表示されます。flexを利用してメニューアイコンは左側、ダークモードの切り替えアイコンは右側に表示させています。


//略
import { MenuIcon, MoonIcon, SunIcon } from '@heroicons/vue/outline';
//略
<div
  class="flex items-center justify-between bg-white dark:bg-gray-800 rounded shadow m-4 p-4"
>
  <MenuIcon
    class="h-6 w-6 text-gray-600 dark:text-gray-300 cursor-pointer"
    @click="show = !show"
  />
  <div>
    <MoonIcon
      class="w-7 h-7 text-gray-600 cursor-pointer"
      v-if="theme === 'light'"
    />
    <SunIcon class="w-7 h-7 text-gray-300 cursor-pointer" v-else />
  </div>
</div>
ライトモードでMoonIcon表示
ライトモードでMoonIcon表示

太陽のアイコン、月のアイコンをクリックしたらモードが変わるようにそれぞれにclickイベントを設定します。clickイベントにはchangeMode関数を設定し、引数を取れるように設定しておきます。


<MoonIcon
  class="w-7 h-7 text-gray-600 cursor-pointer"
  @click="changeMode('dark')"
  v-if="theme === 'light'"
/>
<SunIcon
  class="w-7 h-7 text-gray-300 cursor-pointer"
  @click="changeMode('light')"
  v-else
/>

script setupタグの中にchageMode関数を追加します。refで定義した値を更新したい場合はtheme.valueで行います。


const theme = ref('light');

const changeMode = (mode) => {
  theme.value = mode;
};

アイコンをクリックするたびに太陽のアイコンと月のアイコンが交互に表示されることを確認してください。動作が確認できたらdocument.documentElementでhtml要素を取得してclass="dark"を追加、削除させることでダークモードの切り替えを行います。


const changeMode = (mode) => {
  theme.value = mode;
  theme.value === 'light'
    ? document.documentElement.classList.remove('dark')
    : document.documentElement.classList.add('dark');
};

月のアイコンをクリックしたらダークモードに変更されることを確認します。ダークモードに変更後は太陽のアイコンになっていることが確認できます。

手動でのモードの切り替え
手動でのモードの切り替え

再度太陽のアイコンをクリックすると元の状態に戻ることを確認してください。これでdarkModeオプションの'class'によるダークモードの切り替えを実装することができました。

一度モードを切り替えたら次回アクセス時もそのモードを維持できるようにブラウザのlocalStoragethemeを利用してモードの値を保存します。

changeMode関数の中にlocalstorage.theme = modeを追加します。


const changeMode = (mode) => {
  theme.value = mode;
  theme.value === 'light'
    ? document.documentElement.classList.remove('dark')
    : document.documentElement.classList.add('dark');
  localStorage.theme = mode;
};

ローカルストレージに保存されている値はデベロッパーツールのアプリケーションから確認することができます。

ローカルストレージの確認
ローカルストレージの確認

アクセス後にローカルストレージを確認するコードを追加します。


//略
const theme = ref('light');

if (localStorage.theme === 'dark') {
  document.documentElement.classList.add('dark');
  theme.value = 'dark';
} else {
  document.documentElement.classList.remove('dark');
  theme.value = 'light';
}

const changeMode = ( mode ) => {
//略

コードを追加後にダークモードでブラウザを閉じ、再度アクセスするとダークモードで表示されることが確認できます。ここまでの設定でダークモードを設定できるようになりました。

ドロップダウンメニュー

一般的なダッシュボードでは画面右上にアイコンがあり、そのアイコンをクリックするとメニューが表示されます。本文書では上部のメニューバーに人物画像を設定して画像をクリックするとドロップダウンメニューが表示されるように設定を行います。人物画像はassetsフォルダにavatar.jpgで保存しています。画像ファイルは各自準備してください。

componentsフォルダにDropdownMenu.vueファイルを作成します。


<script setup></script>

<template>
  <div>
    <img
      src="../assets/avatar.jpg"
      class="rounded-full w-10 h-10 cursor-pointer"
    />
  </div>
</template>

作成したDropdownMenu.vueファイルをDefault.vueファイルでimportします。


import DropdownMenu from '../components/DropdownMenu.vue';

importしたDropdownMenuコンポーネントをダークモード切り替えアイコンの横に追加します。flexを利用してダークモード切り替えアイコンと横並びにして間にはスペース(space-x-4)を設定しています。


<div class="flex items-center space-x-4">
  <MoonIcon
    class="w-7 h-7 text-gray-600 cursor-pointer"
    @click="changeMode('dark')"
    v-if="theme === 'light'"
  />
  <SunIcon
    class="w-7 h-7 text-gray-300 cursor-pointer"
    @click="changeMode('light')"
    v-else
  />
  <DropdownMenu />
</div>

設定後メニューバーの右端に画像が表示されます。

ドロップダウンメニューのための画像表示
ドロップダウンメニューのための画像表示

画像をクリックすると表示されるドロップダウンメニューを作成します。画像の親要素のdivにpositionのrelativeを設定し、ドロップダウンメニューはabsolute設定で画像の親要素を基準にtop-16, right-0を設定しています。ログアウトとプロファイルのリンクを設定していますがこれらのページは存在しないのでアプリケーションに合わせてメニューのリストを変更する必要があります。


<script setup>
import { UserIcon, LogoutIcon } from '@heroicons/vue/outline';
</script>

<template>
  <div class="relative">
    <img
      src="../assets/avatar.jpg"
      class="rounded-full w-10 h-10 cursor-pointer"
    />
    <div
      class="absolute top-16 right-0 z-10 w-40 py-2 bg-white rounded-sm shadow"
    >
      <ul>
        <li class="text-gray-700 hover:bg-blue-100 hover:text-blue-600 p-2">
          <a href="/#" class="flex items-center space-x-2">
            <UserIcon class="w-5 h-5" />
            <span class="text-sm font-bold">プロファイル</span></a
          >
        </li>
        <li class="text-gray-700 hover:bg-blue-100 hover:text-blue-600 p-2">
          <a href="/#" class="flex items-center space-x-2">
            <LogoutIcon class="w-5 h-5" />
            <span class="text-sm font-bold">ログアウト</span></a
          >
        </li>
      </ul>
    </div>
  </div>
</template>

ブラウザで確認すると画像の下にメニューが表示されます。

メニュー表示
メニュー表示

メニューの上にマウスがのせるとhoverによって文字色と背景色が変わるように設定しています。

メニューにhoverを設定
メニューにhoverを設定

画像をクリックすることでメニューの表示・非表示を行えるようにimg要素にclickイベントを設定して関数toggleを設定します。


<img
  src="../assets/avatar.jpg"
  class="rounded-full w-10 h-10 cursor-pointer"
  @click="toggle"
/>

変数showを追加し、showの値によってメニューの表示・非表示を切り替えます。showの値の切り替えにはtoggle関数を利用します。toggle関数ではshowの値がfalseの場合はtrueに、trueの場合はfalseに更新します。


import { ref } from 'vue';
import { UserIcon, LogoutIcon } from '@heroicons/vue/outline';

const show = ref(false);

const toggle = () => {
  show.value = !show.value;
};

v-showディレクティブをドロップダウンメニューの要素に設定します。


<div
  class="absolute top-16 right-0 z-10 w-40 py-2 bg-white rounded-sm shadow"
  v-show="show" //追加
>
  <ul>
    <li class="text-gray-700 hover:bg-blue-100 hover:text-blue-600 p-2">
      <a href="/#" class="flex items-center space-x-2">
        <UserIcon class="w-5 h-5" />
        <span class="text-sm font-bold">プロファイル</span></a
      >
    </li>
    <li class="text-gray-700 hover:bg-blue-100 hover:text-blue-600 p-2">
      <a href="/#" class="flex items-center space-x-2">
        <LogoutIcon class="w-5 h-5" />
        <span class="text-sm font-bold">ログアウト</span></a
      >
    </li>
  </ul>
</div>

ドロップダウンメニューの設定は完了し画像をクリックするとメニューの表示・非表示を切り替えることができるようになりました。

アニメーションの設定

ドロップダウンメニューを追加しましたが画像をクリックすると一瞬でメニューが表示され再度クリックするとメニューが非表示になります。この表示・非表示の切り替え時にアニメーションの設定を行います。

ドロップダウンメニューではVueのtransitionを利用してアニメーションの設定を行います。Vueのtransitionを利用したい場合はv-if, v-showディレクティブを持つ要素に対してtranstionタグで包みます。

アニメーションの変化はstyleタグにcssを記述して行う方法だけではなくclass属性も利用することができます。ドロップダウンメニューではclass属性を利用します。表示・非表示のみの切り替えなのでopacityを利用してアニメーション設定を行います。

Vueのtransitionの設定ではEnterとLeaveにおける状態の理解が必要となります。

transitionの図
transitionの図

v-enter-fromが初期の状態で今回のドロップダウンメニューであれば非表示の状態(show=false)です。非表示なのでopacityは0に設定します。v-enter-toは非表示から表示に切り替わった後の状態で表示されているのでopacityは1になります。v-enter-activeでは非表示から表示までの表示中の遷移の状態を設定します。遷移にかかる時間の設定durationやeasingを設定します。easingはアニメーションの速度の調整に利用します。

v-leave-fromは表示されている状態を表しておりopacityは1です。今回の例では表示された状態からクリックを押すと非表示になります。非表示の状態を表すのがv-leave-toでopacityは0に設定します。v-leave-activeは表示から非表示中の遷移の状態を設定します。

Tailwind CSSのclassを利用してtransitionを設定します。設定を行うと先ほどまでとは異なりゆっくりと表示・非表示に繰り返されるようになります。


<transition
  enter-active-class="transition-opacity"
  enter-from-class="opacity-0"
  leave-active-class="transition-opacity"
  leave-to-class="opacity-0"
>
  <div
    class="
      absolute
      top-16
      right-0
      z-10
      w-40
      py-2
      bg-white
      rounded-sm
      shadow
    "
    v-show="show"
  >
    <ul>
//略
    </ul>
  </div>
</transition>
表示された状態はopacityは1なのでenter-to-class, leave-from-classを設定する必要はありません。transition-opacityではopacityの変化でアニメーションが行われます。
fukidashi

さらにゆっくりと表示・非表示を繰り返すためにdurationを設定することができます。duration-1000を設定すると1秒をかけて非表示から表示、表示から非表示に切り替わります。


<transition
  enter-active-class="transition-opacity duration-1000"
  enter-from-class="opacity-0"
  leave-active-class="transition-opacity duration-1000"
  leave-to-class="opacity-0"
>

durationの設定値による違いが確認できたら本文書ではduration-300に設定します。

opacity以外に縦方向の移動をわずかに加えたい場合は以下のようにtransform、translateを利用します。enter-active-classではopacityだけの変化にアニメーションを加えるためtransition-opacityを設定していましがtransformの変化にもアニメーションを加えるためtransitionに変更します。


<transition
  enter-active-class="transition duration-300"
  enter-from-class="transform opacity-0 -translate-y-2 "
  leave-active-class="transition duration-300"
  leave-to-class="transform opacity-0 -translate-y-2"
>

Vueのtransitionでは表示・非表示のアニメーションに加えてclassを追加することでいろいろな変化を加えることができます。

ダークモードの設定

メニューの要素にはダークモードの設定を行っていないので月のアイコンをクリックするダークモードに切り替わってもメニューだけはそのままのカラーになります。

ダークモードが適用されていないメニュー
ダークモードが適用されていないメニュー

ドロップダウンメニューにもダークモードを設定します。メニューの中の要素にdark:を追加します。hover:bg-blue-100を利用していますがほかのclassの設定方法と同様にdark:を追加しdark:hover:bg-blue-100でダークモードを設定することができます。


<div
  class="
    absolute
    top-16
    right-0
    z-10
    w-40
    py-2
    bg-white
    rounded-sm
    shadow
    dark:bg-gray-800
  "
  v-show="show"
>
  <ul>
    <li
      class="
        text-gray-700
        dark:text-gray-300
        hover:bg-blue-100
        dark:hover:bg-gray-700
        hover:text-blue-600
        dark:hover:text-blue-600
        p-2
      "
    >
      <a href="/#" class="flex items-center space-x-2">
        <UserIcon class="w-5 h-5" />
        <span class="text-sm font-bold">プロファイル</span></a
      >
    </li>
    <li
      class="
        text-gray-700
        dark:text-gray-300
        hover:bg-blue-100
        dark:hover:bg-gray-700
        hover:text-blue-600
        dark:hover:text-blue-600
        p-2
      "
    >
      <a href="/#" class="flex items-center space-x-2">
        <LogoutIcon class="w-5 h-5" />
        <span class="text-sm font-bold">ログアウト</span></a
      >
    </li>
  </ul>
</div>

メニューの要素に対してダークモードが設定されました。

メニューにダークモード設定
メニューにダークモード設定

画像クリック以外での表示・非表示設定

メニューの表示・非表示を切り替えるためには必ず画像をクリックする必要があります。表示されているメニューの外側をクリックしても表示から非表示に切り替わるように設定を行います。イベントリスナーを登録・削除するのでVueのライフサイクルフックのonMountedとonUnmountedを利用します。

DropdownMenuコンポーネント全体の要素の情報を保存するために変数rootを追加します。


import { ref } from 'vue';
import { UserIcon, LogoutIcon } from '@heroicons/vue/outline';

const show = ref(false);
const root = ref(null);

const toggle = () => {
  show.value = !show.value;
};

要素を取得するために要素に対して直接アクセスできるtemplate refsを利用します。


<template>
  <div class="relative" ref="root">
    <img

メニュー要素の外側をクリックしたかどうか判断するためにクリックした要素がrefを設定したdiv要素の内側にあるかどうかチェックする関数clickOutsideを追加します。


const clickOutside = (e) => {
    if (!root.value.contains(e.target) && show.value) {
        show.value = false;
    }
};

clickOutside関数はイベントリスナーに登録します。イベントリスナーへの登録はライフサイクルフックを利用します。コンポーネントのマウント時にclickOutside関数を追加し、アンマウント時に削除しています。


<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { UserIcon, LogoutIcon } from '@heroicons/vue/outline';

const show = ref(false);
const root = ref(null);

const toggle = () => {
  show.value = !show.value;
};

const clickOutside = (e) => {
  if (!root.value.contains(e.target) && show.value) {
    show.value = false;
  }
};

onMounted(() => document.addEventListener('click', clickOutside));
onUnmounted(() => document.removeEventListener('click', clickOutside));
</script>

clickOutside関数追加後に表示したメニューの外側をクリックするとメニューが非表示になることを確認してください。

root.valueには<div class="relative">...</div>が入っています。e.targetはクリックした場所によって異なりダッシュボードの文字上でクリックすると<div class="dark:text-gray-300">...</div>が入っていることが確認できます。
fukidashi

これでドロップダウンメニューの実装は完了です。

サイドバーの設定

ここまでサイドバーには何も設定を行っていませんでしたが、サイドバーにロゴとメニューリストを追加します。表示するメニューリストにはアコーディオンメニューで実装します。

ロゴの設定

アプリケーションのロゴを設定する場所を作成するためにcomponentsフォルダにSidebar.vueファイルを作成します。


<script setup></script>
<template>
  <div class="p-4">
    <div class="font-bold text-lg text-blue-600">LOGO</div>
  </div>
</template>

作成したSidebar.vueはDefault.vueファイルでimportします。


import Sidebar from '../components/Sidebar.vue';

文字列のサイドバーからimportしたDefaultコンポーネントに変更します。


<template>
  <div class="relative">
    <div
      class="
        fixed
        top-0
        w-64
        h-screen
        bg-white
        dark:bg-gray-800
        z-20
        transform
        duration-300
        dark:text-gray-300
      "
      :class="{ '-translate-x-full': !show }"
    >
      <Sidebar />
    </div>

ブラウザで確認すると左上に青文字でLOGOという文字が表示されます。ここでは文字列でLOGOにしていますが文字列またはSVG, 画像などでロゴを設定してください。

ロゴの表示
ロゴの表示

メニューリストの設定

サイドバーに表示させるメニューリストを設定するためcomponentsフォルダにList.vueファイルを作成します。List.vueファイルの中にはメニューリストの情報を保持するためにreactiveを利用してlistsを定義します。

v-forディレクティブでlistsを展開してnameを表示させます。lists変数の中ではsublistsによってメニューリストは階層化されていますが最初は無視します。


<script setup>
import { reactive } from 'vue';

const lists = reactive([
  {
    name: 'ダッシュボード',
    icon: 'TemplateIcon',
    link: '/',
  },
  {
    name: 'EC',
    icon: 'ShoppingCartIcon',
    link: '/#',
    sublists: [
      {
        name: '商品一覧',
        link: '/#',
      },
      {
        name: '注文一覧',
        link: '/#',
      },
    ],
  },
]);
</script>
<template>
  <ul class="text-gray-700">
    <li class="mb-1" v-for="list in lists" :key="list.name">
      <a
        :href="list.link"
        class="block p-2 rounded-sm hover:text-white hover:bg-blue-400"
      >
        <span>{{ list.name }}</span>
      </a>
    </li>
  </ul>
</template>

作成したList.vueファイルをSidebar.vueファイルでimportします。


<script setup>
import List from './List.vue';
</script>
<template>
  <div class="p-4">
    <div class="font-bold text-lg text-blue-600">LOGO</div>
    <div class="mt-8">
      <List />
    </div>
  </div>
</template>

ブラウザで確認するとサイドバーにメニューリストが表示されます。

サイドバーにメニュー表示
サイドバーにメニュー表示

メニューリストの一つにマウスを乗せるとhoverの設定により背景色と文字列が変わります。

hoverによりclassの適用
hoverによりclassの適用

Dynamic Componentsによるアイコン設定

メニューリストの左側にlistsに含まれているiconを表示させるための設定を行います。listsに含まれるiconの値を動的に設定するためにDynamic Componentsを利用します。Dynamic Componentsはcomponentタグを利用します。is属性に表示させたいコンポーネント名を指定します。


<component :is="component_name"></component>

componentタグを追加してis属性にアイコンのコンポーネント名が含まれるlist.iconを設定します。list.nameと横並びになるように親要素にflexを追加しています。Iconのimportも必須なので忘れないで行ってください。


import { TemplateIcon, ShoppingCartIcon } from '@heroicons/vue/outline';
//略
<template>
  <ul class="text-gray-700">
    <li class="mb-1" v-for="list in lists" :key="list.name">
      <a
        :href="list.link"
        class="
          flex
          items-center
          block
          p-2
          rounded-sm
          hover:text-white hover:bg-blue-400
        "
      >
        <component :is="list.icon" class="w-6 h-6 mr-2"></component>
        <span>{{ list.name }}</span>
      </a>
    </li>
  </ul>
</template>

ブラウザで確認するとアイコンの領域は確保されていますがアイコンは表示されません。デベロッパーツールでどのような状態になっているか確認するとtemplateicon、shoppingcarticonタグが表示されていることは確認することができます。しかしこの状態ではアイコンは表示されません。

dynamic componetsの設定
dynamic componetsの設定

script setupではdynamic componentsを利用するためにはアイコン名の対応が記述されているオブジェクトを事前に設定しておく必要があります。


const icons = {
  TemplateIcon: TemplateIcon,
  ShoppingCartIcon: ShoppingCartIcon,
};

このオブジェクトを利用してコードを更新します。


<component :is="icons[list.icon]" class="w-6 h-6 mr-2"></component>

設定後ブラウザで確認するとアイコンが表示されます。デベロッパーツール上では先ほどのようにtempateicon, shppingcarticonタグではくsvgタグを確認することができます。

dynamic componetsを利用してアイコン表示
dynamic componetsを利用してアイコン表示

リストの階層化

階層化されたサブリストを展開できるようにコードを更新します。サブリスト(sublists)を持っているリストと持っていないリストをv-ifディレクティブで分岐を行っています。サブリストを持っている場合はさらにv-forでsublistsの展開を行っています。


<template>
  <ul class="text-gray-700">
    <li class="mb-1" v-for="list in lists" :key="list.name">
      <a
        v-if="!list.sublists"
        :href="list.link"
        class="
          flex
          items-center
          block
          p-2
          rounded-sm
          hover:text-white hover:bg-blue-400
        "
      >
        <component :is="icons[list.icon]" class="w-6 h-6 mr-2"></component>
        <span>{{ list.name }}</span>
      </a>
      <div
        v-else
        class="
          flex
          items-center
          p-2
          cursor-pointer
          rounded-sm
          hover:bg-blue-400 hover:text-white
        "
      >
        <component :is="icons[list.icon]" class="w-6 h-6 mr-2"></component>
        <span>{{ list.name }}</span>
      </div>
      <ul class="mt-1">
        <li class="mb-1" v-for="list in list.sublists" :key="list.name">
          <a
            :href="list.link"
            class="block p-2 rounded-sm hover:bg-blue-400 hover:text-white"
          >
            <span class="pl-8">{{ list.name }}</span>
          </a>
        </li>
      </ul>
    </li>
  </ul>
</template>

ブラウザで確認するとECはサブリストを持っているのでその下に展開した商品一覧と注文一覧が表示されます。

sublistsを展開
sublistsを展開

アコーディオンメニューの設定

sublistsを開閉できるようにsublistsを持つリストにshowプロパティを追加します。デフォルトではfalseに設定します。listsにreactivityを持たせるためにreactiveで設定した意味がここにあります。


const lists = reactive([
  {
    name: 'ダッシュボード',
    icon: 'TemplateIcon',
    link: '/',
  },
  {
    name: 'EC',
    icon: 'ShoppingCartIcon',
    link: '/#',
    show: false,//追加
    sublists: [
      {
        name: '商品一覧',
        link: '/#',
      },
      {
        name: '注文一覧',
        link: '/#',
      },
    ],
  },
]);

showがfalseの場合は非表示になるようにsublistsの要素にv-showを設定します。


<ul class="mt-1" v-show="list.show">
  <li class="mb-1" v-for="list in list.sublists" :key="list.name">
    <a
      :href="list.link"
      class="block p-2 rounded-sm hover:bg-blue-400 hover:text-white"
    >
      <span class="pl-8">{{ list.name }}</span>
    </a>
  </li>
</ul>

v-showを設定するとshowはデフォルトでfalseに設定されているのでsublistsは非表示に状態になります。

sublistはv-showにより非表示
sublistはv-showにより非表示

リストをクリックするとsublistsが表示されるようにtoggle関数を追加します。どのリストがクリックされたかを識別するために引数にリストの名前を設定します。


<div
  v-else
  class="
    flex
    items-center
    p-2
    cursor-pointer
    rounded-sm
    hover:bg-blue-400 hover:text-white
  "
  @click="toggle(list.name)"
>
  <component :is="icons[list.icon]" class="w-6 h-6 mr-2"></component>
  <span>{{ list.name }}</span>
</div>

script setupタグの中にtoggle関数を追加します。findメソッドを利用して引数のnameを持つリストを取得してそのリストのshowの値を更新しています。


const toggle = (name) => {
  const list = lists.find((list) => list.name === name);
  list.show = !list.show;
};

設定後リストをクリックすることで表示・非表示を繰り返すことが可能になりました。

メニューリストのアニメーション設定

表示されている”ダッシュボード”、”EC”のリストを見てもどちらがsublistsを持っているかわかりません。sublistsを持っているか識別できるようにアイコンを設定します。


<div
  v-else
  class="
    flex
    items-center
    justify-between
    p-2
    cursor-pointer
    rounded-sm
    hover:bg-blue-400 hover:text-white
  "
  @click="toggle(list.name)"
>
  <div class="flex items-center">
    <component
      v-bind:is="icons[list.icon]"
      class="w-6 h-6 mr-2"
    ></component>
    <span>{{ list.name }}</span>
  </div>
  <ChevronDownIcon class="w-4 h-4" />
</div>

ChervonDownIconのimportも忘れずに行います。


import {
  TemplateIcon,
  ShoppingCartIcon,
  ChevronDownIcon,
} from '@heroicons/vue/outline';

ECのリストのみ下矢印が表示されるようになります。

Chervon Iconの下矢印表示
Chervon Iconの下矢印表示

表示と非表示を矢印の向きで区別できるようにclassを設定しリストのshowの値とtransformのrotateを利用します。アニメーションの設定も一緒に行います。


<ChevronDownIcon
  class="w-4 h-4 transform duration-300"
  :class="!list.show ? 'rotate-0' : '-rotate-180'"
/>

リストをクリックするとアニメーションにより矢印が右回転で180度回転して下向きから上向きに表示されます。

sublistsが表示されたら矢印は下方向
sublistsが表示されたら矢印は上方向

アコーディオンメニューのアニメーション設定

サブリストの表示・非表示が一瞬で切り替わるためドロップダウンメニューの場合と同様にvueのtransitionを利用してアニメーションを設定します。

sublistsのul要素をtransitionタグで囲みます。ul要素のclassにoverflow-hiddenを追加しています。overflow-hiddenを追加しない場合、下にリストがある場合にアニメーションの動作時に下のリストと文字が被るなど正しい動作になりません。


<transition>
  <ul class="mt-1 overflow-hidden" v-show="list.show">
    <li class="mb-1" v-for="list in list.sublists" :key="list.name">
      <a
        :href="list.link"
        class="block p-2 rounded-sm hover:bg-blue-400 hover:text-white"
      >
        <span class="pl-8">{{ list.name }}</span>
      </a>
    </li>
  </ul>
</transition>

styleタグの中でclassの設定を行います。しかし下記の設定ではアニメーションとして動作しません。


<style scoped>
.v-enter-from,
.v-leave-to {
  height: 0;
}
.v-enter-active,
.v-leave-active {
  transition: height 0.3s;
}
</style>

アニメーションを動作させるためにはサブリストが開いた後の高さが必要となりますがサブリストのリスト数によって高さは変わるので1つのheightの固定値は指定することができません。1つの固定値を設定した場合にはアニメーションが動作するのか動作するためにheight:100pxを設定します。

height:autoに設定してもアニメーションは動作しません。
fukidashi

<style scoped>
.v-enter-from,
.v-leave-to {
  height: 0;
}
.v-enter-active,
.v-leave-active {
  transition: height 0.3s;
}
.v-enter-to,
.v-leave-from {
  height: 100px;
}
</style>

height:100pxを設定するとアニメーションが動作しゆっくりと開閉を行います。heightを固定値の100pxに設定しているのでもう一つsublistsにカテゴリー一覧を追加してみましょう。また下にリストがあっても正常に動作するのか確認するためにダッシュボードのリストを下にも追加します。


const lists = reactive([
  {
    name: 'ダッシュボード',
    icon: 'TemplateIcon',
    link: '/',
  },
  {
    name: 'EC',
    icon: 'ShoppingCartIcon',
    link: '/#',
    show: false,
    sublists: [
      {
        name: '商品一覧',
        link: '/#',
      },
      {
        name: '注文一覧',
        link: '/#',
      },
      {
        name: 'カテゴリー一覧',
        link: '/#',
      },
    ],
  },
  {
    name: 'ダッシュボード',
    icon: 'TemplateIcon',
    link: '/',
  },
]);

sublistsを追加するとカテゴリー一覧の表示のアニメーションの動作が正常に動作しません。カテゴリー一覧が表示される場所が100pxを超えているためです。固定値を設定するとアニメーションは動作するがさまざまな高さを持つ場合には対応できないことがわかりました。

リストの数に依存せずアニメーションを動作させるためにはtransitionの動作時にheightを取得する必要があります。Vueのtransitionはclassだけではなく関数を利用することもできるためその機能を利用します。

styleタグの中には.v-enter-active, .v-leave-activeのみ残して他は削除します。


<style scoped>
.v-enter-active,
.v-leave-active {
  transition: height 0.3s;
}
</style>

transitionタグへの関数の設定方法についてはVueのドキュメントではtransitionのJavaScript Hookに記載されています。transitionタグにenter、leaveを追加します。


<transition @enter="enter" @leave="leave">
  <ul class="mt-1 overflow-hidden" v-show="list.show">
    <li class="mb-1" v-for="list in list.sublists" :key="list.name">
      <a
        :href="list.link"
        class="block p-2 rounded-sm hover:bg-blue-400 hover:text-white"
      >
        <span class="pl-8">{{ list.name }}</span>
      </a>
    </li>
  </ul>
</transition>

script setupの中に指定したenter、leave関数を追加します。


const enter = (element) => {
    element.style.height = "auto";
    const height = getComputedStyle(element).height;
    element.style.height = 0;
    getComputedStyle(element);
    setTimeout(() => {
        element.style.height = height;
    });
};

const leave = (element) => {
    element.style.height = getComputedStyle(element).height;
    getComputedStyle(element);
    setTimeout(() => {
        element.style.height = 0;
    });
};

2つの関数を追加後、heightが関数内で取得されるためsublistsのリスト数によらずsublistsの開閉がアニメーションによってスムーズに行われることがわかります。

アニメーションが設定されたアコーディオンメニューを実装することができました。

ダークモード設定

メニューリストもダークモードに設定すると文字が見えなくなってしまうのでdark:の設定を行います。

ダークモードの場合のサイドメニュー
ダークモードの場合のサイドメニュー

テキストの文字の色をダークモードの場合はtext-gray-300に設定します。


<template>
  <ul class="text-gray-700 dark:text-gray-300">

サイドバーもダークモードに対応できるようになりました。

ダークモード対応後のサイドバー
ダークモード対応後のサイドバー

Vue Routerの設定

ダッシュボードのレイアウトの作成が完了したのでリンクを使ってページが移動できるようにVue Routerをインストールします。Vue Routerを利用することでシングルページアプリケーションを構築することができます。

Vue Routerのインストールはnpmコマンドで行います。


% npm install vue-router@4

インストール完了後、srcフォルダにrouterフォルダを作成しその中にindex.jsファイルを作成します。index.jsファイルの中でルーティングの設定を行っていきます。

ドロップダウンメニューのプロファイルページ、サイドバーのダッシュボード、注文一覧、商品一覧のルーティングを追加します。各ルーティングに対応するコンポーネントはsrcフォルダの下に作成するpagesフォルダの中に保存します。Vue Routerの設定が完了するとindex.jsファイル内のルーティングの設定を元に/profileにアクセスがあるとProfileコンポーネントの内容が表示されることになります。


import { createRouter, createWebHistory } from 'vue-router';
import Dashboard from '../pages/Dashboard.vue';
import Profile from '../pages/Profile.vue';
import Order from '../pages/Order.vue';
import Product from '../pages/Product.vue';

const routes = [
  {
    path: '/',
    name: 'Dashboard',
    component: Dashboard,
  },
  {
    path: '/profile',
    name: 'Profile',
    component: Profile,
  },
  {
    path: '/order',
    name: 'Order',
    component: Order,
  },
  {
    path: '/product',
    name: 'Product',
    component: Product,
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;

pagesフォルダの下にDashboar.vue, Profile.vue, Order.vue, Product.vueファイルを作成します。Dashboard.vueファイルの内容は下記の通りです。そのほかのファイルも下記を参考にダッシュモードの文字のみ変更を行います。


<script setup></script>
<template>
  <div>ダッシュボード</div>
</template>
本文書ではダッシュボードのレイアウトの作成のチュートリアルなので各ページの内容については対応するコンポーネント内に各自が追加していくことになります。
fukidashi

作成したrouterの設定をVueに登録するためmain.jsファイルを更新します。


import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import './index.css';

createApp(App).use(router).mount('#app');

/(ルート)にアクセスした場合にDashboardコンポーネントの内容を表示させるためにApp.vueファイルにrounter-viewタグを追加します。router-viewの場所にルーティングで設定したコンポーネントの内容が表示されることになります。


<script setup>
import Default from './layouts/Default.vue';
</script>

<template>
  <Default><router-view></router-view></Default>
</template>

ここまでの設定でブラウザを開いてlocalhost:3000にアクセスしてダッシュボードの文字列が表示され、localhost:3000/profileにアクセスしてプロファイルの文字列が表示されればVue Routerの設定は問題なく行われています。/order, /productにアクセスすると対応するコンポーネントに記述した内容が表示されるかも確認を行ってください。

プロファイルページの表示
プロファイルページの表示

リンクの設定

ドロップダウンメニューのプロファイルをクリックすると/profileに移動できるように設定を行います。これまではリンクの設定ではaタグを利用してリンク先はすべて/#としていました。

Vue Routerではリンクにはrouter-linkタグを利用します。

aタグを利用したままでもページの移動を行うことができます。しかしページ移動時にページ全体が再読み込みされるためスムーズなページの移動はできません。
fukidashi

DropDownMenu.vueファイルを開いたaタグをrouter-linkに変更し、hrefをtoに変更し値を/profileに設定します。


<router-link to="/profile" class="flex items-center space-x-2">
  <UserIcon class="w-5 h-5" />
  <span class="text-sm font-bold">プロファイル</span></router-link
>

設定後ダッシュボードのページから画像をクリックしてドロップダウンメニューからプロファイルを選択してください。ページ全体の再読み込みが行われることなくメイン領域の内容がダッシュボードからプロファイルに更新されることが確認できます。

サイドバーのメニューのリストのリンク先を設定するためにList.vueファイルを開いてlistsのlinkの値を設定します。


const lists = reactive([
  {
    name: 'ダッシュボード',
    icon: 'TemplateIcon',
    link: '/',
  },
  {
    name: 'EC',
    icon: 'ShoppingCartIcon',
    link: '/#',
    show: false,
    sublists: [
      {
        name: '商品一覧',
        link: '/product',
      },
      {
        name: '注文一覧',
        link: '/order',
      },
    ],
  },
]);

templateタグ内のaタグをrouter-linkタグに変更します。


<router-link
  v-if="!list.sublists"
  :to="list.link"
  class="
    flex
    items-center
    block
    p-2
    rounded-sm
    hover:text-white hover:bg-blue-400
  "
>
  <component :is="icons[list.icon]" class="w-6 h-6 mr-2"></component>
  <span>{{ list.name }}</span>
</router-link>

設定後はサイドメニューのリストを選択してもページのリロードなしにページ移動を行うことができます。

現在表示しているページの情報

現在表示しているページの情報を取得したい場合はuseRouteを利用することができます。list.vueファイルでvue-routerからuseRouteをimportします。


import { reactive } from "vue";
import { useRoute } from 'vue-router';

どのような情報が含まれているか確認したい場合はconsole.log(useRoute())で確認できます。現在のアクセスしているパスの情報はuserRoute().fullPathで確認できます。ページを移動する度にfullPathを取得するためにはcomputedプロパティを利用します。script setupではcomputedプロパティを利用するためにimportが必要になります。


import { reactive, computed } from 'vue';
import { useRoute } from 'vue-router';
//略
const currentRoute = computed(() => {
  return useRoute().fullPath;
});

表示しているページがどのページなのかサイドバーのメニューリストでわかるようにuseRoute().fullPathを利用します。currentRouteの値とlist.linkの値が一致した場合にbg-blue-600とtext-whiteを適用します。


<router-link
  v-if="!list.sublists"
  :to="list.link"
  class="
    flex
    items-center
    block
    p-2
    rounded-sm
    hover:text-white hover:bg-blue-400
  "
  :class="{
    'bg-blue-600 text-white': currentRoute === list.link,
  }"
>

商品一覧ページが表示されている場合は下記のように表示されます。

現在のページのメニューにclassを適用
現在のページのメニューにclassを適用

ここまでの作業でダッシュボードのレイアウトの作成は完成でダッシュボード上に表示させる内容についてはrouter/index.jsで指定したルーティングに対応するコンポーネントの中に記述することになります。

ダッシュボードのレイアウト作成後はユーザ認証機能の実装を行います。