Vue の class・style バインディングとは、:class と :style に JavaScript の値(オブジェクト・配列・文字列)を渡して、状態に応じてクラス名やインラインスタイルを切り替えるしくみです。 「選択中の行だけ色を変える」「エラーのときだけ赤くする」といった見た目の切り替えを、文字列を自分で組み立てずに書けます。

この記事では、Vue 3.5.43 の <script setup> でコードを動かし、実際に出力された HTML をそのまま載せています。 :class のオブジェクト・配列・三項演算子・computed・配列内オブジェクトの5通りを「書き方 → 出力 HTML」の早見表にまとめ、子コンポーネントへの class の引き継ぎ(ルートが複数あるときの $attrs.class)、:style の単位の付け忘れ・配列・自動プレフィックス、CSS の中で使う v-bind()、Tailwind CSS で文字列を連結して作ったクラスが出力 CSS に含まれない現象の再現と対処まで扱います。

確認環境: Linux(x64)/Node.js 22.22.2/Vue 3.5.43/Vite 8.3.2+@vitejs/plugin-vue 6.0.9/Tailwind CSS 4.3.3(@tailwindcss/vite 4.3.3)/Vitest 5.0.3+jsdom 30.1.1+@vue/test-utils 2.5.1/Chromium 141(Playwright 1.56.0)。class の出力 HTML は jsdom、style・scoped・Tailwind の結果は Chromium で確かめています。出力中のパスは /home/taro/ に置き換えています。

class・styleバインディングの早見表

:class の書き方 → 出力HTML

isActive = true、hasError = false、size = 'lg' のときの出力です。

書き方 テンプレート 出力された HTML
オブジェクト :class="{ active: isActive, 'text-danger': hasError }" <p class="active">
配列 :class="['card', 'card-' + size]" <p class="card card-lg">
三項演算子 :class="isActive ? 'on' : 'off'" <p class="on">
computed :class="buttonClass"(オブジェクトを返す computed) <p class="btn btn-active">
配列内オブジェクト :class="['card', { active: isActive, disabled: hasError }]" <p class="card active">
静的な class と併用 class="card" :class="{ active: isActive }" <p class="card active">
配列の中で && :class="[isActive && 'on', hasError && 'error']" <p class="on">
1つのキーに2クラス :class="{ 'is-open has-shadow': isActive }" <p class="is-open has-shadow">
キーを式で作る :class="{ ['size-' + size]: true }" <p class="size-lg">

:style の書き方 → 出力HTML(Chromium)

color = 'crimson'、size = 20、width = 120 のときの出力です。

書き方 テンプレート 出力された style 属性
オブジェクト(キャメルケース) :style="{ color: color, fontSize: size + 'px' }" color: crimson; font-size: 20px;
オブジェクト(ケバブケース) :style="{ 'font-size': size + 'px', 'background-color': 'lightyellow' }" font-size: 20px; background-color: lightyellow;
単位を付けていない :style="{ fontSize: size, width: width }" (style 属性が付かない)
単位が要らないプロパティ :style="{ lineHeight: 1.5, opacity: 0.8, zIndex: 10 }" line-height: 1.5; opacity: 0.8; z-index: 10;
配列 :style="[base, accent]" padding: 4px; color: crimson; font-weight: bold;
プレフィックスが必要なプロパティ :style="{ textStroke: '1px crimson' }" -webkit-text-stroke: 1px crimson;
複数の値 :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }" display: flex;

どれを使うかの判断表

やりたいこと 向いている書き方
クラスを付ける/外すを条件で切り替える :class のオブジェクト
「A か B のどちらか」を付ける 三項演算子
いつも付けるクラス+条件つきクラス 静的な class と :class の併用、または配列内オブジェクト
条件が3つ以上・式が長い computed
値そのもの(幅・位置・色コード)が状態で決まる :style、または CSS の v-bind()

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Vueのclass・styleバインディング|:classの配列・オブジェクト」

:classの5通りを動かして出力HTMLを確認

早見表のもとになったコードの全文です。ボタンを押すと isActive と hasError が反転します。

<!-- ClassDemo.vue -->
<script setup>
import { ref, computed } from 'vue'

const isActive = ref(true)
const hasError = ref(false)
const size = ref('lg')

// 条件が多いときは computed にまとめる
const buttonClass = computed(() => ({
  btn: true,
  'btn-active': isActive.value,
  'btn-error': hasError.value,
}))

function toggle() {
  isActive.value = !isActive.value
  hasError.value = !hasError.value
}
</script>

<template>
  <button @click="toggle">切り替え</button>
  <p :class="{ active: isActive, 'text-danger': hasError }">オブジェクト</p>
  <p :class="['card', 'card-' + size]">配列</p>
  <p :class="isActive ? 'on' : 'off'">三項演算子</p>
  <p :class="buttonClass">computed</p>
  <p :class="['card', { active: isActive, disabled: hasError }]">配列内オブジェクト</p>
  <p class="card" :class="{ active: isActive }">静的な class と併用</p>
  <p :class="[isActive && 'on', hasError && 'error']">配列の中で &amp;&amp;</p>
  <p :class="[isActive ? 'on' : 'off', 'card']">配列の中で三項演算子</p>
  <p :class="{ 'is-open has-shadow': isActive }">1つのキーに2クラス</p>
  <p :class="{ ['size-' + size]: true }">キーを式で作る</p>
  <p :class="{ active: hasError }">条件が false のとき</p>
</template>

初期表示(isActive = true、hasError = false)の出力です。

<p class="active">オブジェクト</p>
<p class="card card-lg">配列</p>
<p class="on">三項演算子</p>
<p class="btn btn-active">computed</p>
<p class="card active">配列内オブジェクト</p>
<p class="card active">静的な class と併用</p>
<p class="on">配列の中で &amp;&amp;</p>
<p class="on card">配列の中で三項演算子</p>
<p class="is-open has-shadow">1つのキーに2クラス</p>
<p class="size-lg">キーを式で作る</p>
<p class="">条件が false のとき</p>

「切り替え」を押したあと(isActive = false、hasError = true)の出力です。

<p class="text-danger">オブジェクト</p>
<p class="card card-lg">配列</p>
<p class="off">三項演算子</p>
<p class="btn btn-error">computed</p>
<p class="card disabled">配列内オブジェクト</p>
<p class="card">静的な class と併用</p>
<p class="error">配列の中で &amp;&amp;</p>
<p class="off card">配列の中で三項演算子</p>
<p class="">1つのキーに2クラス</p>
<p class="size-lg">キーを式で作る</p>
<p class="active">条件が false のとき</p>

1. オブジェクト:キーがクラス名、値が条件

<p :class="{ active: isActive, 'text-danger': hasError }">オブジェクト</p>

「値が真のキーだけがクラス名として出力される」のがオブジェクトの書き方です。isActive が true のときは active だけ、反転後は text-danger だけが付きました。ハイフンを含むクラス名は JavaScript のキーとしてそのまま書けないので、'text-danger' のように引用符で囲みます。引用符なしで { text-danger: hasError } と書いてビルドすると、次のエラーで止まりました。

[plugin vite:vue] /home/taro/app/src/Hyphen.vue:7:14
SyntaxError: Error parsing JavaScript expression: Unexpected token, expected "," (1:7)

2. 配列:複数のクラスを並べる

<p :class="['card', 'card-' + size]">配列</p>

配列の要素がそのまま並んで出力されます。変数に入っているクラス名や、式で作ったクラス名を並べたいときに向いています。配列の中に false・null・undefined があった場合は飛ばされるので、isActive && 'on' のような書き方もできます(反転後は on が消えて error だけになりました)。

3. 三項演算子:どちらか一方を付ける

<p :class="isActive ? 'on' : 'off'">三項演算子</p>

「A か B のどちらか」を必ず付けるときは、三項演算子がいちばん短く書けます。ほかのクラスと並べたいときは、配列の中に入れます([isActive ? 'on' : 'off', 'card'])。

4. computed:条件をスクリプト側にまとめる

const buttonClass = computed(() => ({
  btn: true,
  'btn-active': isActive.value,
  'btn-error': hasError.value,
}))

条件が増えてテンプレートが長くなってきたら、オブジェクトを返す computed にまとめます。テンプレートは :class="buttonClass" だけになり、条件の中身に名前が付くので読みやすくなります。同じ組み合わせを複数の要素で使い回せるのも利点です。

5. 配列内オブジェクト:固定クラスと条件つきクラスを一緒に

<p :class="['card', { active: isActive, disabled: hasError }]">配列内オブジェクト</p>

配列の要素としてオブジェクトを入れられます。「いつも付けるクラス」と「条件で付け外しするクラス」を1か所に書けるので、部品のクラス指定をまとめたいときに便利です。

静的な class と :class は合わさる

<p class="card" :class="{ active: isActive }">静的な class と併用</p>

ふつうの class 属性と :class は同じ要素に並べて書けて、出力では1つの class 属性にまとまります(class="card active")。いつも付けるクラスは静的な class に、切り替えるクラスだけ :class に書くと、役割がはっきりします。

条件がすべて false のときは class="" が残る

最後の行は、条件が false のときに class="" という空の属性が残ることを示しています。見た目には影響しませんが、テストで HTML を文字列比較するときや、[class] という属性セレクターを使っているときは覚えておくと役立ちます。

一覧の行ごとにクラスを切り替える例

v-for の中で、行のデータによってクラスを変える例です。値に応じたクラス名は対応表(オブジェクト)で引き、真偽で切り替えるクラスはオブジェクトで書く、という組み合わせにしています。

<!-- TaskList.vue -->
<script setup>
import { ref } from 'vue'

const tasks = ref([
  { id: 1, title: '資料を作る', done: true, priority: 'high' },
  { id: 2, title: '備品を注文する', done: false, priority: 'low' },
  { id: 3, title: '会議室を予約する', done: false, priority: 'high' },
])
const selectedId = ref(2)

// 値ごとのクラス名は対応表にしておく
const priorityClass = { high: 'badge-red', low: 'badge-gray' }
</script>

<template>
  <ul>
    <li
      v-for="task in tasks"
      :key="task.id"
      class="task"
      :class="[priorityClass[task.priority], { done: task.done, selected: task.id === selectedId }]"
      @click="selectedId = task.id"
    >
      {{ task.title }}
    </li>
  </ul>
</template>
<ul>
  <li class="task badge-red done">資料を作る</li>
  <li class="task badge-gray selected">備品を注文する</li>
  <li class="task badge-red">会議室を予約する</li>
</ul>
<!-- 3行目をクリックしたあと -->
<ul>
  <li class="task badge-red done">資料を作る</li>
  <li class="task badge-gray">備品を注文する</li>
  <li class="task badge-red selected">会議室を予約する</li>
</ul>

selected が2行目から3行目に移っています。

子コンポーネントに付けたclassの行き先(フォールスルー)

コンポーネントのタグに書いた class は、props として宣言していなくても子に届きます。これをフォールスルー属性と呼びます。どの要素に付くかは、子のルート要素の数で変わります。親のテンプレートは次のとおりです(active は true)。

<!-- FallParent.vue -->
<script setup>
import { ref } from 'vue'
import ChildSingle from './ChildSingle.vue'
import ChildMulti from './ChildMulti.vue'
import ChildMultiAttrs from './ChildMultiAttrs.vue'
import ChildInner from './ChildInner.vue'
const active = ref(true)
</script>

<template>
  <section data-case="single"><ChildSingle class="mt-4" :class="{ active }" /></section>
  <section data-case="multi"><ChildMulti class="mt-4" :class="{ active }" /></section>
  <section data-case="multi-attrs"><ChildMultiAttrs class="mt-4" :class="{ active }" /></section>
  <section data-case="inner"><ChildInner class="mt-4" :class="{ active }" /></section>
</template>

ルートが1つ:ルート要素のクラスに合わさる

<!-- ChildSingle.vue -->
<template>
  <div class="box">ルートが1つ</div>
</template>
<div class="box mt-4 active">ルートが1つ</div>

子がもともと持っている box に、親が渡した mt-4 と active が足されています。 上書きではなく合わさるので、部品側の見た目を保ったまま、置く側で余白などを足せます。

ルートが複数:どこにも付かず、警告が出る

<!-- ChildMulti.vue -->
<template>
  <header>見出し</header>
  <main>本文</main>
</template>
<header>見出し</header><main>本文</main>
[Vue warn]: Extraneous non-props attributes (class) were passed to component but could not be automatically inherited because component renders fragment or text or teleport root nodes. 
  at <ChildMulti class="mt-4 active" > 

ルート要素が2つあると、Vue はどちらに付ければよいか決められません。クラスはどの要素にも付かず、開発中は上の警告が出ます。

直し方:$attrs.class で付ける要素を指定する

<!-- ChildMultiAttrs.vue -->
<template>
  <header>見出し</header>
  <main :class="$attrs.class">本文</main>
</template>
<header>見出し</header><main class="mt-4 active">本文</main>

$attrs.class には、親が渡したクラスがまとめて入っています。付けたい要素に :class="$attrs.class" と書くと、警告が消えてその要素にクラスが付きます。 Vue 公式ガイドにも、複数のルート要素を持つコンポーネントではこの方法でクラスを受け取る要素を指定すると書かれています。

ルート以外の要素に付けたいとき:inheritAttrs: false

ルートが1つでも、「外側の label ではなく中の input に付けたい」ことがあります。inheritAttrs: false でルートへの自動付与を止めて、付けたい要素に $attrs.class を書きます。

<!-- ChildInner.vue -->
<script setup>
defineOptions({ inheritAttrs: false }) // ルートへの自動付与を止める
</script>

<template>
  <label class="field">
    名前
    <input class="input" :class="$attrs.class" />
  </label>
</template>
<label class="field"> 名前 <input class="input mt-4 active"></label>

スクリプト側で受け取った class の中身

<script setup> の中では useAttrs() で同じ値を読めます。親が配列とオブジェクトを混ぜて渡した場合の中身を出力しました。

<!-- AttrsChild.vue -->
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
console.log('attrs.class =', JSON.stringify(attrs.class))
</script>

<template>
  <div>子</div>
</template>
(親で class に ['mt-4', { active: true, hidden: false }] を渡した場合)
attrs.class = "mt-4 active"

子に届く時点で、空白区切りの1つの文字列に整えられています。 子の側で配列かオブジェクトかを気にする必要はありません。

子の形 親が付けた class の行き先 子の側で書くこと
ルート要素が1つ ルート要素(既存のクラスに足される) 何も書かなくてよい
ルート要素が複数 どこにも付かない(警告) 付けたい要素に :class="$attrs.class"
ルート以外に付けたい 指定した要素 inheritAttrs: false と :class="$attrs.class"

:styleの使い方と、単位・配列・自動プレフィックス

:style のいろいろな書き方を1つのコンポーネントにまとめ、Chromium で開いて各要素の style 属性を読み取りました。

<!-- StyleDemo.vue -->
<script setup>
import { ref } from 'vue'

const color = ref('crimson')
const size = ref(20)
const width = ref(120)

const base = { padding: '4px', color: 'gray' }
const accent = { color: 'crimson', fontWeight: 'bold' }
</script>

<template>
  <p :style="{ color: color, fontSize: size + 'px' }">キャメルケース</p>
  <p :style="{ 'font-size': size + 'px', 'background-color': 'lightyellow' }">ケバブケース</p>
  <p :style="{ fontSize: size, width: width }">単位を付けていない</p>
  <p :style="{ fontSize: `${size}px`, width: `${width}px` }">単位を付けた</p>
  <p :style="{ lineHeight: 1.5, opacity: 0.8, zIndex: 10, flexGrow: 1 }">単位が要らないプロパティ</p>
  <p :style="[base, accent]">配列</p>
  <p style="margin: 0" :style="{ color }">静的な style と併用</p>
  <p :style="`color: ${color}; font-size: ${size}px`">文字列</p>
  <p :style="{ '--accent': color, color: 'var(--accent)' }">CSS 変数</p>
  <p :style="{ color: null, fontSize: undefined, margin: '' }">null・undefined・空文字</p>
  <p :style="{ textStroke: '1px crimson', boxOrient: 'vertical', userSelect: 'none' }">自動プレフィックス</p>
  <p :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }">複数の値</p>
  <p :style="{ color: 'crimson;' }">末尾にセミコロン</p>
</template>
1. キャメルケース → style="color: crimson; font-size: 20px;"
2. ケバブケース → style="font-size: 20px; background-color: lightyellow;"
3. 単位を付けていない → style="(属性なし)"
   計算後: font-size=16px width=1264px
4. 単位を付けた → style="font-size: 20px; width: 120px;"
   計算後: font-size=20px width=120px
5. 単位が要らないプロパティ → style="line-height: 1.5; opacity: 0.8; z-index: 10; flex-grow: 1;"
6. 配列 → style="padding: 4px; color: crimson; font-weight: bold;"
7. 静的な style と併用 → style="margin: 0px; color: crimson;"
8. 文字列 → style="color: crimson; font-size: 20px;"
9. CSS 変数 → style="--accent: crimson; color: var(--accent);"
10. null・undefined・空文字 → style="(属性なし)"
11. 自動プレフィックス → style="-webkit-text-stroke: 1px crimson; -webkit-box-orient: vertical; user-select: none;"
12. 複数の値 → style="display: flex;"
13. 末尾にセミコロン → style="(属性なし)"

キーはキャメルケースでもケバブケースでもよい

1番と2番のとおり、fontSize と 'font-size' はどちらも font-size として出力されました。Vue 公式ガイドではキャメルケースが推奨されています。キャメルケースなら引用符が要らないので、JavaScript のオブジェクトとして書きやすくなります。

単位の付け忘れ:数値だけ渡すと適用されない

3番が、つまずきやすい点です。fontSize: size(20)や width: width(120)のように数値だけを渡すと、ブラウザーが値として受け付けず、style 属性そのものが付きませんでした。 計算後のスタイルは font-size=16px(ブラウザーの既定値)、width=1264px(親の幅いっぱい)で、指定が効いていないことがわかります。エラーや警告は出ません。

4番のように単位まで含めた文字列にすると適用されます。

<!-- 単位まで含めて渡す -->
<p :style="{ fontSize: size + 'px', width: width + 'px' }">...</p>

一方、5番の lineHeight・opacity・zIndex・flexGrow は、CSS でも単位なしの数値を書くプロパティなので、数値のままで適用されました。

プロパティの種類 例 数値だけ渡したとき
長さが必要 width・height・fontSize・margin・top 適用されない。'px' などを付ける
単位なしの数値を書く lineHeight・opacity・zIndex・flexGrow そのまま適用される

配列:あとのオブジェクトが優先される

6番は [base, accent] という配列です。base の color: 'gray' と accent の color: 'crimson' が重なっていますが、出力は color: crimson でした。複数のスタイルオブジェクトが順に合わさり、同じプロパティはあとのものが使われます。 「共通のスタイル+状態ごとの上書き」を分けて持てます。

自動プレフィックス:必要なときだけ付く

11番では、textStroke・boxOrient・userSelect の3つを渡しました。出力は次のとおりです。

  • textStroke → -webkit-text-stroke
  • boxOrient → -webkit-box-orient
  • userSelect → user-select(そのまま)

Vue 公式ガイドによると、Vue は実行時にブラウザーがどのプロパティに対応しているかを調べ、必要なプレフィックスを付けます。Chromium 141 では、プレフィックスなしで使える user-select はそのまま、-webkit- 付きでのみ使える2つにはプレフィックスが付きました。

複数の値:対応している最後の値が使われる

12番は、1つのプロパティに値の配列を渡す書き方です。Vue 公式ガイドには「配列に含まれる値のうち、ブラウザーでサポートされる最後の値のみがレンダリングに使われる」とあり、Chromium 141 では display: flex になりました。

値の末尾にセミコロンを書くと適用されない

13番のように値の文字列の末尾に ; を入れると、開発中のコンソールに次の警告が出て、スタイルは適用されませんでした。

[Vue warn]: Unexpected semicolon at the end of 'color' style value: 'crimson;' 
  at <StyleDemo>

CSS ファイルから値をコピーしたときに入りやすいので、オブジェクトの値にはセミコロンを付けないと覚えておきます。

null・undefined・空文字は出力されない

10番のとおり、値が null・undefined・空文字のプロパティは出力されません。「条件に合うときだけスタイルを付ける」は、{ color: hasError ? 'crimson' : null } のように書けます。この書き方で、hasError が false の間は style 属性なし、true にすると style="color: crimson;" になることを確かめました。

CSSの中で使うv-bind()

:style はインラインスタイルを直接書きますが、単一ファイルコンポーネントの <style> の中で v-bind() を使うと、CSS のルールの中にコンポーネントの状態を埋め込めます。

<!-- VBind.vue -->
<script setup>
import { ref } from 'vue'

const color = ref('crimson')
const theme = ref({ size: 18 })
</script>

<template>
  <div class="box">
    <p class="text">v-bind() の文字</p>
    <button @click="color = 'teal'">色を変える</button>
  </div>
</template>

<style scoped>
.text {
  color: v-bind(color);
  font-size: v-bind('theme.size + "px"');
}
</style>

vite build で出力された CSS と、Chromium で開いたときの HTML です。

.text[data-v-e3bb8730]{color:var(--v2138d9a3);font-size:var(--v4d717db2)}
<div data-v-e3bb8730="" class="box" style="--v2138d9a3: crimson; --v4d717db2: 18px;"><p data-v-e3bb8730="" class="text">v-bind() の文字</p><button data-v-e3bb8730="">色を変える</button></div>
  計算後: color=rgb(220, 20, 60) font-size=18px
--- 「色を変える」をクリック
<div data-v-e3bb8730="" class="box" style="--v2138d9a3: teal; --v4d717db2: 18px;">(中略)</div>
  計算後: color=rgb(0, 128, 128) font-size=18px

CSS の v-bind(color) は var(--v2138d9a3) という CSS カスタムプロパティに置き換わり、その値がコンポーネントのルート要素の style 属性に入っています。 ボタンを押すと、ルート要素のカスタムプロパティの値だけが teal に変わり、文字の色が変わりました。Vue 公式 API リファレンス「SFC CSS 機能」に書かれているとおりの動きです。

書き方のポイントは2つです。

  • 変数名だけなら v-bind(color)。
  • 式を書くときは引用符で囲む(v-bind('theme.size + "px"'))。ここでも、長さには単位を付けます。

:class・:style・v-bind() の使い分け

方法 向いている場面 特徴
:class 「選択中」「エラー」など、状態の種類が決まっている 見た目の定義を CSS 側にまとめられる。基本はこれ
:style 値そのものが連続的に変わる(幅・位置・進み具合の割合) その要素にだけインラインで付く
CSS の v-bind() 疑似要素・:hover・子孫セレクターなど、CSS のルールの中で値を使いたい カスタムプロパティ経由でルール全体に効く

scopedスタイルと動的クラスでつまずく点

<style scoped> を付けると、そのコンポーネントの要素にだけ効く CSS になります。:class で付けたクラスも自分の要素には効きますが、子コンポーネントの内側や v-html で入れた HTML には届きません。 Chromium で確かめました。

<!-- ScopedChild.vue -->
<template>
  <div class="child">
    子のルート
    <span class="active inner">子の内側</span>
  </div>
</template>
<!-- ScopedParent.vue -->
<script setup>
import { ref } from 'vue'
import ScopedChild from './ScopedChild.vue'

const active = ref(true)
const html = '<span class="active">v-html の文字</span>'
</script>

<template>
  <div class="scoped-demo">
    <p class="own" :class="{ active }">自分の要素</p>
    <ScopedChild :class="{ active }" />
    <div class="rich" v-html="html"></div>
  </div>
</template>

<style scoped>
.active {
  background-color: gold;
}
</style>
自分の要素: background-color=rgb(255, 215, 0)
子のルート: background-color=rgb(255, 215, 0)
子の内側: background-color=rgba(0, 0, 0, 0)
v-html の文字: background-color=rgba(0, 0, 0, 0)
要素 .active が効いたか 理由
自分のテンプレートの要素 効いた 親の目印(data-v-... 属性)が付く
子コンポーネントのルート要素 効いた 子のルートには親の目印も付く
子コンポーネントの内側の要素 効かなかった 親の目印が付かない
v-html で入れた要素 効かなかった 同上

出力された CSS は .active[data-v-a9e54c67] という形で、data-v-a9e54c67 属性を持つ要素だけが対象です。子の内側や v-html の要素にはこの属性が付かないため、クラス名が合っていても効きません。

直し方::deep() で内側まで届かせる

セレクターを :deep() で包むと、目印の位置が外側の要素に移り、内側の要素にも届きます。

<style scoped>
.deep-demo :deep(.active) {
  background-color: gold;
}
</style>
自分の要素: background-color=rgb(255, 215, 0)
子のルート: background-color=rgb(255, 215, 0)
子の内側: background-color=rgb(255, 215, 0)
v-html の文字: background-color=rgb(255, 215, 0)

出力された CSS は .deep-demo[data-v-d771dea4] .active で、「このコンポーネントの .deep-demo の中にある .active」という意味になります。

CSS Modulesのクラス名を :class に渡す

<style module> を使うと、クラス名が $style というオブジェクトで渡されます。:class の配列やオブジェクトとそのまま組み合わせられます。

<!-- ModuleDemo.vue -->
<script setup>
import { ref } from 'vue'
const isActive = ref(true)
</script>

<template>
  <p :class="[$style.title, { [$style.active]: isActive }]">CSS Modules</p>
</template>

<style module>
.title {
  font-weight: bold;
}
.active {
  color: crimson;
}
</style>
<p class="_title_616b4e _active_616b4e">CSS Modules</p>

クラス名が _title_616b4e のように書き換えられ、ほかのコンポーネントと名前が重ならなくなります。オブジェクトのキーに使うときは [$style.active] と角かっこで囲みます。

Tailwindで文字列連結したクラスが効かないとき

Tailwind CSS を使っていて「クラスは HTML に付いているのに色が付かない」ときは、クラス名を文字列の連結で組み立てていないかを確認します。Tailwind CSS 4.3.3 と Vite で再現しました。

再現:props からクラス名を組み立てる

/* src/style.css */
@import "tailwindcss";
<!-- src/App.vue -->
<script setup>
import Badge from './Badge.vue'
</script>

<template>
  <Badge color="red">在庫なし</Badge>
  <Badge color="blue">入荷待ち</Badge>
  <Badge color="green">在庫あり</Badge>
</template>
<!-- src/Badge.vue -->
<script setup>
defineProps({ color: String })
</script>

<template>
  <!-- クラス名を文字列の連結で組み立てている -->
  <span class="px-2 text-white" :class="`bg-${color}-500`"><slot /></span>
</template>

ビルドした CSS に各クラスの定義が含まれているか(1 = ある、0 = ない)と、Chromium で開いたときの結果です。

.px-2: 1
.text-white: 1
.bg-red-500: 0
.bg-blue-500: 0
.bg-green-500: 0
5488 bytes
<span class="px-2 text-white bg-red-500">在庫なし</span>
   計算後: background-color=rgba(0, 0, 0, 0) padding-left=8px
<span class="px-2 text-white bg-blue-500">入荷待ち</span>
   計算後: background-color=rgba(0, 0, 0, 0) padding-left=8px
<span class="px-2 text-white bg-green-500">在庫あり</span>
   計算後: background-color=rgba(0, 0, 0, 0) padding-left=8px

HTML には bg-red-500 が付いているのに、出力 CSS に .bg-red-500 の定義がなく、背景色は透明のままです。 静的に書いた px-2 は定義があり、padding-left=8px が効いています。

Tailwind CSS 公式ドキュメント「Detecting classes in source files」には、Tailwind はソースファイルをプレーンテキストとして読むので、文字列の連結や埋め込みは理解できないと書かれています。ソースのどこにも bg-red-500 という文字の並びが出てこないため、その定義が作られません。

対処1:クラス名を全文で書いた対応表にする

<!-- src/Badge.vue(対応表にした版) -->
<script setup>
import { computed } from 'vue'

const props = defineProps({ color: String })

// クラス名は省略せずに全文で書き、props からは「どれを選ぶか」だけを決める
const colorClasses = {
  red: 'bg-red-500',
  blue: 'bg-blue-500',
  green: 'bg-green-500',
}
const colorClass = computed(() => colorClasses[props.color])
</script>

<template>
  <span class="px-2 text-white" :class="colorClass"><slot /></span>
</template>
.px-2: 1
.text-white: 1
.bg-red-500: 1
.bg-blue-500: 1
.bg-green-500: 1
5835 bytes
<span class="px-2 text-white bg-red-500">在庫なし</span>
   計算後: background-color=oklch(0.637 0.237 25.331) padding-left=8px
<span class="px-2 text-white bg-blue-500">入荷待ち</span>
   計算後: background-color=oklch(0.623 0.214 259.815) padding-left=8px
<span class="px-2 text-white bg-green-500">在庫あり</span>
   計算後: background-color=oklch(0.723 0.219 149.579) padding-left=8px

3つとも定義が出力され、背景色が付きました。公式ドキュメントがすすめているのもこの形(props を静的なクラス名に対応づける)です。:class のオブジェクトや三項演算子でも、isError ? 'text-red-600' : 'text-green-600' のようにクラス名を全文で書いていれば出力されます(この三項演算子の形でビルドし、2つとも定義が含まれることを確かめました)。

対処2:@source inline() で出力するクラスを指定する

クラス名がデータベースや外部の設定から来るなど、ソースに全文を書けない場合は、CSS 側で「このクラスは必ず出力する」と指定できます。Badge.vue は連結のままにして、style.css だけを変えました。

/* src/style.css */
@import "tailwindcss";
@source inline("bg-{red,blue,green}-500");
.px-2: 1
.text-white: 1
.bg-red-500: 1
.bg-blue-500: 1
.bg-green-500: 1
5835 bytes

波かっこの中は展開されるので、1行で3色ぶんを指定できます。Chromium での背景色も対処1と同じ値になりました。

対処 向いている場面 気をつけること
対応表(全文で書く) 色や大きさの種類が決まっている部品 種類を増やすときは対応表に1行足す
@source inline() クラス名が外部のデータで決まる 指定したぶんだけ CSS が増えるので、必要な範囲に絞る
:style や CSS 変数 色コードなど、値そのものが自由に変わる クラスではなく値として渡す

つまずきやすい点の早見表

症状 理由 直し方
ハイフン入りのクラス名でエラーになる オブジェクトのキーとしてそのまま書けない 'text-danger' と引用符で囲む
子コンポーネントに付けた class が効かない 子のルート要素が複数ある 子で :class="$attrs.class" を書く
:style の幅や文字サイズが効かない 単位のない数値を渡している width + 'px' のように単位を付ける
:style の値が効かず警告が出る 値の末尾にセミコロンがある セミコロンを取る
scoped のクラスが子の内側に効かない 親の目印が付かない要素 :deep() を使う
Tailwind のクラスが HTML にあるのに効かない クラス名を連結で作っている 全文で書いた対応表にする
CSS の v-bind() で式が動かない 引用符で囲んでいない v-bind('a + "px"')

よくある質問

Q. :class はオブジェクトと配列のどちらで書くのがよいですか? A. 条件で付け外しするクラスはオブジェクト、いつも付けるクラスや変数に入ったクラスを並べるなら配列です。両方あるときは、配列の中にオブジェクトを入れる書き方(['card', { active: isActive }])か、静的な class と :class の併用が読みやすくなります。条件が3つ以上になったら computed にまとめるのがおすすめです。

Q. class と :class を同じ要素に書くと、どちらが優先されますか? A. どちらかが優先されるのではなく、合わさって1つの class 属性になります。class="card" :class="{ active: isActive }" は class="card active" と出力されました。

Q. :style に数値を渡したのにスタイルが変わりません。 A. width や fontSize のように長さが必要なプロパティは、120 のような数値だけでは適用されず、style 属性も付きませんでした。width + 'px' のように単位まで含めた文字列にしてください。lineHeight・opacity・zIndex などは数値のままで適用されます。

Q. 子コンポーネントのどの要素に class が付くかを選べますか? A. 選べます。子で defineOptions({ inheritAttrs: false }) を指定し、付けたい要素に :class="$attrs.class" を書きます。ルート要素が複数あるコンポーネントでは、inheritAttrs の指定がなくても $attrs.class を書いた要素に付きます。

まとめ

  • :class は「オブジェクト=条件で付け外し」「配列=並べる」「三項演算子=どちらか」。配列の中にオブジェクトを入れると、固定クラスと条件つきクラスを一緒に書けます。
  • 静的な class と :class は合わさる。条件が増えたら computed にまとめると、テンプレートが短くなります。
  • 子コンポーネントに付けた class は、ルートが1つならルートに足される。ルートが複数なら $attrs.class で付ける要素を指定します。
  • :style で長さを渡すときは単位まで含める。数値だけの width や fontSize は適用されません。配列はあとのオブジェクトが優先、プレフィックスは必要なときだけ自動で付きます。
  • CSS の v-bind() はカスタムプロパティに置き換わり、ルート要素の style に値が入る。式は引用符で囲みます。
  • scoped のクラスは子の内側と v-html には届かないので、必要なら :deep() を使います。
  • Tailwind のクラス名は全文で書く。連結で作ったクラスは出力 CSS に含まれないため、対応表にするか @source inline() で指定します。

参考・出典

  • Vue.js 公式ガイド「クラスとスタイルのバインディング」(ja.vuejs.org/guide/essentials/class-and-style)
  • Vue.js 公式 API リファレンス「SFC CSS 機能」(ja.vuejs.org/api/sfc-css-features)
  • Tailwind CSS 公式ドキュメント「Detecting classes in source files」(tailwindcss.com/docs/detecting-classes-in-source-files)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Vueのclass・styleバインディング|:classの配列・オブジェクト」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?