Vuetify — セットアップとテーマ
テクノロジ系 / L19 vuetify — Material Design コンポーネントライブラリ
Vuetify — セットアップとテーマ
Vuetify とは
- Vue 3 向けの Material Design コンポーネントライブラリ
v-btn/v-text-field/v-cardなど、見た目が整った部品がすぐ使える- 自分で CSS を1から書かなくても、ボタン・フォーム・カードなどの基本 UI が揃う
この教材の前提知識
- この教材は、component-design(コンポーネント設計)・vue-basic などを
既に学習済みであることを前提とする
- Vue のコンポーネント、Composition API、
<script setup>構文、
Vue Router(router-view)の基本は、ここでは説明なしに使う
composable(useTheme()のような再利用可能な状態ロジック)という
用語も、既知のものとしてそのまま使う
Vuetify を導入する
Vue プロジェクトに Vuetify を組み込むには、大きく3つの手順が必要になる。
npm installでパッケージを追加する- ルートコンポーネント (
App.vue) を<v-app>で囲む main.tsでプラグインとして登録する(後述)
npm install vuetify
なぜ npm 経由で導入するのか
npm install vuetify
- npm 経由にするのは、Vite などのビルドツールと組み合わせて
importで
必要な部分だけ読み込むため
<script setup>からuseTheme()のような composable を呼ぶ今回の構成は、
この import ベースの導入が前提になる
ルートコンポーネントを <v-app> で囲む
App.vue
<template>
<v-app>
<router-view />
</v-app>
</template>- Vuetify のコンポーネントは、レイアウトやテーマの基盤を提供する
<v-app>の
内側で使われることを前提に作られている
<v-app>で囲むのを忘れると、v-btnなどを置いても配色やレイアウトが
正しく反映されないことがある
- なぜ専用タグが必要なのか: Vuetify は
v-app-barやv-navigation-drawer
の高さ・幅を計算し、v-main の余白に自動反映するレイアウト機構を持つ。
この計算の起点となる「箱」が <v-app> であり、ただの見た目のラッパーではない
ここまでで確認できること
npm install vuetify → App.vue を <v-app> で囲む → 後述の
main.ts の設定、まで済ませて開発サーバーを起動すると:
- ブラウザのコンソールに Vuetify 関連のエラーが出ない
- 画面に
<v-btn>送信</v-btn>のようなタグを1つ置くだけで、
角丸・影・色のついた Material Design 風のボタンが表示される
もしボタンが素の <button> のような見た目のままなら、vuetify/styles の
読み込み忘れや app.use(vuetify) の呼び忘れを疑うとよい(詳細は後述)。
セットアップ (main.ts) — 読み込み
import { createApp } from 'vue'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import 'vuetify/styles'
import App from './App.vue'- Vuetify 本体・全コンポーネント・全ディレクティブ・スタイルをまとめて読み込む
なぜ全コンポーネント/ディレクティブを読み込むのか
- Vuetify には
v-btn以外にも数十種類のコンポーネントがあるが、ここでは
使う予定のないものも含めて全部 import している
- 利点: コンポーネントを使うたびに
import { VBtn } from 'vuetify/components'
と個別に書く必要がなく、<v-xxx> タグをそのまま書けば動く。学習中に
「このタグだけ import し忘れた」という事故が起きない
全部読み込みの代償と本番での切り替え
- 代償: 実際には使わないコンポーネントの分だけ、本番ビルドのバンドルサイズが増える
- 本番運用では
vite-plugin-vuetify等で使用コンポーネントだけを自動 import
する構成に切り替えるのが一般的
- まずは全部読み込みで動かし方を覚えるのが先。最適化は後回しでよい
セットアップ (main.ts) — 登録
const vuetify = createVuetify({ components, directives })
createApp(App).use(vuetify).mount('#app')app.use(vuetify) した時点で、アプリ全体のどのコンポーネントからも
v-btn などのタグや useTheme() などの composable が使えるようになる。
なぜ createVuetify() + app.use() という形なのか
const vuetify = createVuetify({ components, directives })
createApp(App).use(vuetify).mount('#app')createVuetify()は、テーマ設定やコンポーネント一覧をまとめた
「プラグイン」オブジェクトを作る関数
app.use(vuetify)はこのプラグインを Vue の provide/inject の仕組みで
アプリ全体に配る。これにより、どんなに深い階層のコンポーネントからでも
useTheme() を呼ぶだけで同じテーマ状態にアクセスできる
- もしテーマ情報がただのグローバル変数(例:
export let theme = 'light')
だったら、provide/inject 越しではなく「別ファイルの変数を直接書き換える」
形になり、値が変わったことを検知して画面を再描画する仕組み
(リアクティビティ)が働かない
基本コンポーネントを使う
<template> <v-btn color="primary">送信</v-btn> <v-btn color="error" variant="outlined">削除</v-btn> </template>
colorprop で配色を指定(自分でclass="btn-primary"等を書かなくてよい)variantprop で見た目の種類(flat/outlined/text等)を切り替え- なぜ prop 指定なのか:
color="primary"は「primary = 主役級の操作」という
意味を指定しているだけで、実際の色は次に出てくる theme 設定側が持つ。
CSS クラスを自分で書いてしまうと、後でテーマの色を変えたときにそのボタンだけ
古い色のまま取り残される
テーマ設定 (createVuetify の theme オプション)
const vuetify = createVuetify({
components,
directives,
theme: {
defaultTheme: 'light',
},
})- Vuetify にはあらかじめ
light/darkの2つのテーマが用意されている defaultThemeで起動時のテーマを指定できる- なぜプラグイン作成時にまとめて設定するのか:
v-btn color="primary"のように
アプリ中のあらゆるコンポーネントが同じ色名(primary 等)を参照する。
その色の実体をここ1箇所で決めておくことで、ブランドカラーを変えたいときの
変更箇所が1箇所で済む
useTheme() でテーマの状態を読み取る
<script setup>
import { useTheme } from 'vuetify'
const theme = useTheme()
</script>useTheme()を呼ぶと、現在のテーマ情報を持つオブジェクトが得られるtheme.global.name.value… 現在のテーマ名('light'/'dark')を
読み取れる
なぜグローバル変数でなく useTheme() なのか
useTheme()が返すtheme.global.nameはリアクティブな値(ref)。
これを読んでいるコンポーネントは、値が変わると自動で再描画される
- もし
export let currentTheme = 'light'のようなただのモジュール変数
にしていたら、値を書き換えても Vue はそれを検知できず、画面は
自分で再描画のコードを書くまで古い表示のまま残る
useTheme()はapp.use(vuetify)時に配られた状態を受け取るだけの
「取得口」。component-design で学んだ他の useXxx() composable と
同じ形なので、Vue のコンポーネントならどこからでも同じ書き方で呼べる
テーマを切り替える
<style scoped>
section code, section pre code { font-variant-ligatures: none; }
</style>
<script setup>
function toggleTheme() {
theme.change(theme.global.name.value === 'light' ? 'dark' : 'light')
}
</script>theme.change('dark')… テーマを切り替える(推奨される書き換え方法)- 現在のテーマ名を見て、逆のテーマ名を渡すことでトグル動作にできる
- なぜ
theme.global.name.value = 'dark'と直接書き換えずchange()を
経由するのか: change() という名前の関数を挟むことで「テーマを変える」
という意図がコードから読み取れる。Vuetify 側の内部実装が将来変わっても、
change() という窓口さえ変わらなければ呼び出し側のコードは書き換えずに済む
テーマはアプリ全体で共有される
graph LR A["app.use(vuetify)<br/>(main.ts で1回)"] --> B[コンポーネントA<br/>useTheme] A --> C[コンポーネントB<br/>useTheme] A --> D[孫コンポーネント<br/>useTheme] B -.同じ theme を参照.- C C -.同じ theme を参照.- D
useTheme()はアプリ内のどの階層のコンポーネントで呼んでも同じ状態を参照する- component-design の ch04 (provide/inject) と同じ仕組みが Vuetify 内部で使われている
まとめ
createVuetify({ components, directives })→app.use()でプラグイン導入v-btnなどのタグはそのまま使える。color/variantprop で見た目を変えるuseTheme()でテーマの読み取り・切り替えができる- テーマの状態はアプリ全体で共有される(provide/inject ベース)
考えてみよう
既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。
Lv1: 基本 — v-btn の color / variant
問題
以下の仕様を満たすボタンを実装せよ。
primary色のボタン「送信」error色でoutlinedのボタン「削除」success色でtextのボタン「完了」
合格基準
- 3つのボタンが色違いで表示される
variantを変えたボタンの見た目が異なる(枠だけ / 文字だけ)
Lv2: 応用 — useTheme でテーマ切り替え
問題
useTheme() を使い、クリックするたびにアプリ全体のテーマを
light ⇄ dark で切り替えるボタンを実装せよ。
- ボタンのラベルに現在のテーマ名(
lightまたはdark)を表示すること - クリックのたびに light ⇄ dark が交互に切り替わること
- テーマの切り替えには
change()を使うこと(プロパティへの直接代入はしない)
合格基準
- 初期状態は light と表示される
- クリックすると dark に変わり、もう一度押すと light に戻る
Lv3: 発展 — 複数コンポーネントで theme を共有
問題
以下の仕様を満たすコンポーネント構成を実装せよ。
- 親コンポーネントに
ThemeButton(Lv2 のテーマ切り替えボタン)と、
現在のテーマ名だけを表示する ThemeLabel(別コンポーネント)を並べて配置する
ThemeButtonとThemeLabelは、それぞれ自分自身でuseTheme()を呼び出す- 親コンポーネントから子へ props を渡したり、子から emit したりしてはいけない
ThemeButtonでテーマを切り替えたときに、ThemeLabelの表示も自動的に追従すること
合格基準
- 別コンポーネントなのに、片方の操作がもう片方の表示に反映される
- なぜ props/emit なしで反映されるのか、自分の言葉で説明できる
Lv4: 発展 — createVuetify でカスタムカラーテーマを定義する
問題
以下の仕様を満たすボタンを実装せよ。
createVuetify()のテーマ設定に、好きな名前でカスタムカラー
(組み込みにない独自の配色)を1つ定義する
v-btnのcolorprop にそのカスタムカラー名を指定し、ボタンに反映させる
合格基準
- ボタンの背景色に、定義したカスタムカラーが適用されている
primaryやerrorなど組み込みの色名ではなく、独自に定義した色名を使っている
提出
submission.md に4問の回答を。
ヒント
まずは第1段階だけを見て考えてみる。それでも分からなければ第2段階、
それでも分からなければ最後の手段として第3段階を見る。
第1段階: 考え方
- Vuetify の「テーマ」(配色・ライト/ダークの設定)は、コンポーネントの外(
main.ts)で
一度だけ定義されている。それを画面側の <script setup> から読み書きするには、
Vuetify が提供している「テーマにアクセスするための composable」を呼び出す必要がある。
- その composable の戻り値を、テンプレートやクリック処理から使えるように
変数へ代入しておく。
- 「切り替える」処理は、今の状態を見て逆の状態を選ぶ、という単純な分岐でしかない。
今の状態を読み取る方法と、状態を変更する方法さえ分かれば、toggleTheme() の中身は
自分で組み立てられる。
第2段階: 使う API 名
- composable の名前は
useTheme()。vuetifyパッケージからインポートする。 - 現在のテーマ名は、戻り値の
global.name.valueというプロパティで読み取れる。 - テーマを切り替えるときは、戻り値が持つ
change()というメソッドを使う
(プロパティへの直接代入は非推奨)。
toggleTheme()は「今の名前が'light'かどうかを見て、'dark'か'light'かを
change() に渡す」という三項演算子 1 行で書ける。
第3段階: コード断片
- Vuetify のテーマ(配色設定)を JS から読み書きするには
useTheme()を呼び出す。import { useTheme } from 'vuetify'。 useTheme()の戻り値をthemeという変数に代入すればよい。- 現在のテーマ名は
theme.global.name.value('light'または'dark')で読み取れる。 - テーマを切り替えるときは
theme.global.name.value = ...と直接代入せず、theme.change('dark')のようにchange()メソッドを使う(直接代入は非推奨警告が出る)。 toggleTheme()の中身はこう書ける:theme.change(theme.global.name.value === 'light' ? 'dark' : 'light')
解説
Vuetify の「テーマ」はアプリ全体で共有される設定
Vuetify を導入すると、色・ダークモードなどの見た目の設定は createVuetify({ theme: {...} }) で
アプリ起動時に一度だけ定義し、あとはどのコンポーネントからでも useTheme() で参照・変更できる。
import { useTheme } from 'vuetify'
const theme = useTheme()
console.log(theme.global.name.value) // 'light' | 'dark'<script setup>
import { useTheme } from 'vuetify'
const theme = useTheme()
function toggleTheme() {
theme.change(theme.global.name.value === 'light' ? 'dark' : 'light')
}
</script>
<template>
<v-btn @click="toggleTheme">
テーマ: {{ theme.global.name.value }}
</v-btn>
</template>theme.global.nameはアプリ全体で 1 つだけ存在する グローバルな状態。
どのコンポーネントで useTheme() を呼んでも同じ値を参照する(Pinia の store に近い性質)。
- 値を直接
theme.global.name.value = 'dark'と書き換えることもできるが、Vuetify は
theme.change('dark') を推奨している(将来的な内部処理の変更に対応するため)。
なぜコンポーネントごとに v-theme-provider を書かなくてよいか
Vue の provide/inject(component-design の ch04 で学んだ仕組み)を Vuetify がプラグイン内部で
使っており、app.use(vuetify) した時点でアプリ全体にテーマ情報が provide 済みになっている。
だからこそ、深い階層のどのコンポーネントからでも useTheme() するだけで同じテーマにアクセスできる。
「ライブラリが用意する composable(useXxx())は、内部で provide/inject や store を
使っていることが多い」と知っておくと、他のライブラリを読むときにも見通しが良くなる。
ヒント
まずは第1段階だけを見て考えてみる。それでも分からなければ第2段階、
それでも分からなければ最後の手段として第3段階を見る。
第1段階: 考え方
- Vuetify にはあらかじめ
primary/error/successなどの色名が
用意されているが、それとは別に、自分だけの色名(カスタムカラー)を
追加で定義することもできる。
- カスタムカラーは
createVuetify()を呼び出すときの設定(テーマ設定)の中に、
「好きな名前」と「16進カラーコード」のペアとして書く。
- 定義した色名は、組み込みの
primaryなどと全く同じように、v-btnの
color prop にそのまま指定して使える。
第2段階: 使う API 名
createVuetify({ theme: { themes: { light: { colors: { ... } } } } })という
構造の中の colors オブジェクトに、キー名: '#16進カラーコード' の形で
カスタムカラーを追加する。
- 例えば
brandという名前で色を定義したら、v-btnにはcolor="brand"と書く。
第3段階: コード断片
import { createVuetify } from 'vuetify'
const vuetify = createVuetify({
theme: {
themes: {
light: {
colors: {
brand: '#8E24AA', // 好きな16進カラーコードでOK
},
},
},
},
})<template> <v-btn color="brand">ブランドボタン</v-btn> </template>
解説
Vuetify は組み込みの色以外に「自分だけの色」を追加定義できる
primary や error のような色名は、Vuetify が最初から用意している「テーマの色」の
一部でしかない。createVuetify() の theme.themes.<テーマ名>.colors に
自分の好きなキー名を追加すれば、そのままアプリ独自の色として使えるようになる。
import { createVuetify } from 'vuetify'
const vuetify = createVuetify({
theme: {
themes: {
light: {
colors: {
brand: '#8E24AA', // 独自に追加したカスタムカラー
},
},
},
},
})<template> <v-btn color="brand">ブランドボタン</v-btn> </template>
colorprop は「組み込みの色名」と「自分で定義したカスタム色名」を区別しない。
theme.themes.light.colors に登録されている名前であれば、そのまま使える。
- キー名は自由に付けられる(
brandである必要はない。プロジェクトの
ブランドカラーなどに合わせて名前を決めるとよい)。
内部でどう反映されているか
Vuetify は createVuetify() に渡されたテーマ設定から、色ごとに
--v-theme-<色名> という CSS カスタムプロパティ(CSS 変数)を生成し、
<style id="vuetify-theme-stylesheet"> として <head> に差し込む。
v-btn などの color prop は、その値に応じて bg-<色名> というクラスを
コンポーネントに付与するだけで、実際の色は background-color: rgb(var(--v-theme-<色名>))
という形でこの CSS 変数から解決される。
graph LR
A["createVuetify({ theme: { colors: { brand: '#8E24AA' } } })"] --> B["--v-theme-brand が\nCSS変数として生成される"]
B --> C["v-btn color='brand' が\nbg-brand クラスを付与"]
C --> D["background-color: rgb(var(--v-theme-brand))\nとして描画される"]つまり「色名を書いただけ」と「テーマにその色を定義した」は別物で、
colors に登録していない名前を color prop に渡しても、対応する CSS 変数が
存在しないため色は反映されない。
この演習でのファイル構成について
通常のアプリでは createVuetify() は main.ts で1回だけ呼び出し、
各コンポーネントは app.use(vuetify) 済みの状態からその恩恵を受けるだけでよい
(ch01 の「セットアップ」スライドの通り)。
この演習では、テストで動作を確認しやすくするため、createVuetify() の呼び出しを
answer.vue / starter.vue の中に書いているが、書いているテーマ設定の内容自体は
main.ts に書く内容と何も変わらない。