CSS を書いていると、「またこの3行を書いている」と感じる場面が出てきます。ボタンの共通スタイル、メディアクエリの書き出し、テキストの省略表示などです。Sass の @mixin は、そうしたスタイルの塊に名前を付けて、必要な場所で @include と書くだけで呼び出せるようにする機能です。この記事では、基本の書き方から引数の渡し方、@content を使ったメディアクエリ用 mixin、@extend との違い、そして「Undefined mixin」と怒られたときの確認どころまでを、コンパイル後の CSS と並べて解説します。記法は SCSS を前提にしています。
目次
@mixin を使うと何が嬉しいのか
同じスタイルを複数のセレクタに書くと、あとで変更するときにすべてを探して直すことになります。1か所だけ直し忘れて見た目がずれる、というのはよくある失敗です。mixin はスタイルの塊を「部品」として1か所にまとめ、使う側は名前で呼び出すだけにします。直したいときは定義を書き換えるだけで、呼び出しているすべての場所に反映されます。
変数($color: #333; のようなもの)が「1つの値」に名前を付ける機能だとすれば、mixin は「複数の宣言のまとまり」に名前を付ける機能だと考えると分かりやすいです。しかも引数を受け取れるので、色やサイズだけを差し替えた似たスタイルを、1つの定義から作り分けられます。
基本の書き方|@mixin で定義して @include で呼び出す
定義は @mixin 名前 { ... }、呼び出しは @include 名前; です。まずは引数なしの、いちばんシンプルな形を見てみます。
// 中央寄せのスタイルに名前を付けて定義する
@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.card {
@include flex-center; // 呼び出す
height: 200px;
}
.modal {
@include flex-center;
position: fixed;
inset: 0;
}
これをコンパイルすると、@include と書いた位置に定義の中身がそのまま展開されます。
.card {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.modal {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
inset: 0;
}
注目してほしいのは、@mixin flex-center の定義そのものは CSS に出力されないことです。mixin は呼び出されて初めて中身が展開されます。逆に言えば、呼び出した回数だけ同じ宣言が複製されます。この「複製される」という性質は、あとで @extend と比べるときの重要なポイントになります。
引数を渡して使い回す
mixin は関数のように引数を受け取れます。定義側は名前のあとに丸かっこで引数を並べ、呼び出し側も丸かっこで値を渡します。
@mixin box($size, $color) {
width: $size;
height: $size;
background-color: $color;
}
.icon-sm {
@include box(24px, #0d6efd);
}
.icon-lg {
@include box(48px, #dc3545);
}
.icon-sm {
width: 24px;
height: 24px;
background-color: #0d6efd;
}
.icon-lg {
width: 48px;
height: 48px;
background-color: #dc3545;
}
デフォルト値を決めておく
引数に $color: #333 のようにコロンで値を書いておくと、それがデフォルト値になります。呼び出し側で省略すればデフォルト値が、渡せばその値が使われます。よく使う設定をデフォルトにしておくと、ほとんどの呼び出しが短くなります。
@mixin button($bg, $color: #fff, $radius: 4px) {
display: inline-block;
padding: 12px 24px;
border: none;
border-radius: $radius;
background-color: $bg;
color: $color;
cursor: pointer;
}
.btn-primary {
@include button(#0d6efd); // $color と $radius は既定のまま
}
.btn-pill {
@include button(#198754, #fff, 999px);
}
デフォルト値がある引数は、必ず引数リストの後ろにまとめて書きます。デフォルト値のある引数のうしろに、デフォルト値のない引数を置くことはできません。
キーワード引数で必要なものだけ渡す
上の .btn-pill では、$color を変えたいわけではないのに、順番の都合で #fff を書き直しています。こういうときは $引数名: 値 の形(キーワード引数)で呼び出すと、変えたい引数だけを指定できます。
.btn-pill {
@include button(#198754, $radius: 999px); // $color は飛ばせる
}
// すべてキーワードで書けば順番も自由になる
.btn-ghost {
@include button($color: #0d6efd, $bg: transparent);
}
可変長引数($args…)でまとめて受け取る
引数名のうしろにピリオド3つを付けると、渡された値をいくつでもまとめて受け取れます。box-shadow や transition のように、カンマ区切りで値をいくつも並べられるプロパティと相性が良い書き方です。
@mixin shadow($shadows...) {
box-shadow: $shadows;
}
.card {
@include shadow(0 1px 2px rgba(0, 0, 0, 0.1), 0 4px 12px rgba(0, 0, 0, 0.08));
}
.card {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 4px 12px rgba(0, 0, 0, 0.08);
}
同じピリオド3つは呼び出し側でも使えて、@include shadow($my-shadows...); と書くとリストの中身を引数として展開して渡せます。別の mixin にそのまま引数を横流ししたいときに便利です。
ここまでに出てきた記法を整理しておきます。
| 記法 | 意味 |
|---|---|
@mixin 名前 { ... } | スタイルの塊に名前を付けて定義する。これ自体は CSS に出力されない |
@include 名前; | 定義した mixin を呼び出し、その位置に中身を展開する |
@mixin 名前($a, $b) | 引数を受け取る。呼び出しは @include 名前(値1, 値2); |
$b: 値(定義側) | 引数のデフォルト値。省略して呼び出せるようになる |
$b: 値(呼び出し側) | キーワード引数。指定したい引数だけを名前付きで渡す |
$args... | 可変長引数。渡された値をまとめて1つの引数で受け取る |
@content | @include に渡されたブロックを差し込む位置を示す |
@include 名前 { ... } | スタイルのブロックごと mixin に渡す |
@content でブロックごと渡す|メディアクエリ用 mixin
mixin の中に @content と書いておくと、呼び出し側が波かっこで渡したスタイルのブロックが、その位置に差し込まれます。この仕組みがいちばん活きるのがメディアクエリです。
@mixin mq($min-width) {
@media (min-width: $min-width) {
@content; // ここに呼び出し側のブロックが入る
}
}
.card {
padding: 16px;
font-size: 14px;
@include mq(768px) {
padding: 32px;
font-size: 16px;
}
}
.card {
padding: 16px;
font-size: 14px;
}
@media (min-width: 768px) {
.card {
padding: 32px;
font-size: 16px;
}
}
Sass が .card というセレクタをメディアクエリの内側に組み立て直してくれるので、書く側は「このセレクタの、この画面幅のときの見た目」をひとかたまりで書けます。スマートフォン用とパソコン用のスタイルがファイルの離れた場所に散らばらない、というのが大きな利点です。
実務ではブレークポイントの数値を直接書かず、名前で呼べるようにしておくと管理しやすくなります。マップ(キーと値の組)にまとめておき、map.get() で取り出す形が定番です。
@use 'sass:map';
$breakpoints: (
'sm': 576px,
'md': 768px,
'lg': 1024px,
);
@mixin mq($name) {
@media (min-width: map.get($breakpoints, $name)) {
@content;
}
}
これで @include mq('md') { ... } のように名前で呼べます。ブレークポイントを変えたくなっても、直す場所はこのマップ1か所だけです。
実践例|テキストを指定行数で省略する mixin
一覧ページの説明文を「2行を超えたら…で省略する」というスタイルは、毎回4行ほどの決まり文句を書くことになります。行数を引数にした mixin にしておくと、呼び出し1行で済みます。
@mixin line-clamp($lines: 2) {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: $lines;
overflow: hidden;
}
.card__excerpt {
@include line-clamp; // 既定の2行
}
.news__title {
@include line-clamp(1);
}
@extend との違いは「出力される CSS の形」
スタイルを使い回す手段としては @extend もあります。どちらを使っても見た目の結果は同じにできますが、出力される CSS の形がまったく違います。
mixin は呼び出した場所に宣言を複製します。呼び出しが10か所あれば、同じ宣言が10回書き出されます。一方 @extend は宣言を複製せず、セレクタのほうをカンマでまとめます。次の例を比べてみてください。
// プレースホルダーセレクタ(それ自体は出力されない)
%btn-base {
padding: 12px 24px;
border-radius: 4px;
}
.btn-primary {
@extend %btn-base;
background-color: #0d6efd;
}
.btn-secondary {
@extend %btn-base;
background-color: #6c757d;
}
.btn-primary, .btn-secondary {
padding: 12px 24px;
border-radius: 4px;
}
.btn-primary {
background-color: #0d6efd;
}
.btn-secondary {
background-color: #6c757d;
}
ファイルサイズだけを見れば @extend のほうが小さくなります。ただし @extend は引数を取れず、@content のようにブロックを渡すこともできません。さらに、まとめられたセレクタは元の記述場所に出力されるため、CSS の並び順が想像と変わって詳細度や上書きの前提が崩れることがあります。メディアクエリをまたいだ @extend はエラーになる、という制約もあります。
迷ったら mixin を選ぶのが安全です。値を変えて使い回したいとき、メディアクエリを含むとき、ブロックを渡したいときは mixin しか選べません。@extend は「引数のいらない共通の土台を、同じ種類の要素どうしで共有する」という限られた場面で使うと考えておくとよいでしょう。なお @extend を使うときは、クラスではなく上の例のようなプレースホルダーセレクタ(%名前)を継承するのが基本です。
別ファイルにまとめた mixin を @use で読み込む
mixin は専用のファイルにまとめておき、必要なファイルから読み込むのが一般的です。ファイル名の先頭にアンダースコアを付けた _mixins.scss(パーシャル)にしておくと、そのファイル単体では CSS が出力されません。
@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}
@mixin mq($min-width) {
@media (min-width: $min-width) {
@content;
}
}
@use 'mixins' as m; // 先頭のアンダースコアと拡張子は書かない
.card {
@include m.flex-center; // 名前空間 m を付けて呼び出す
@include m.mq(768px) {
padding: 32px;
}
}
@use で読み込んだ mixin には、名前空間(モジュール名)を付けて呼び出します。as m のように別名を付ければ m.flex-center、as を省略した @use 'mixins'; ならファイル名がそのまま名前空間になり mixins.flex-center です。どのファイル由来の mixin なのかが呼び出し側で分かるので、名前の衝突も起きません。@use と、古い @import との詳しい違いは別記事で扱っているので、ここでは使い方の紹介にとどめます。
「Undefined mixin」と言われるとき
mixin を呼び出したのにコンパイルが Error: Undefined mixin. で止まる、というのは Sass を書き始めた頃によく出会うエラーです。「その名前の mixin が見つからない」という意味で、原因はだいたい次の4つに絞られます。
読み込みの @use を書き忘れている
いちばん多いのがこれです。_mixins.scss に定義があっても、呼び出す側のファイルで @use 'mixins'; を書かなければ、その mixin は存在しないのと同じ扱いになります。@use は読み込んだファイル自身にしか効かないため、「A が B を読み込み、B が C を読み込んでいるから A でも C の mixin が使える」ということにはなりません。使うファイルそれぞれで @use を書きます。
あわせて、@use を書く位置にも決まりがあります。@use はファイルの先頭に、ほかのスタイルルールより前に書く必要があります(変数の宣言と @charset は例外です)。セレクタの途中や下のほうに書くとエラーになります。
名前空間を付け忘れている
@use 'mixins'; と書いたうえで @include flex-center; と呼ぶと、やはり Undefined mixin になります。@use で読み込んだものは mixins.flex-center のように名前空間付きで呼ぶ決まりだからです。@import を使っていたコードを @use に書き換えたときに、呼び出し側の修正が漏れて起きるパターンがよくあります。as m で短い別名を付けておくと書く量が減り、付け忘れにも気づきやすくなります。
定義より前で呼び出している
Sass はファイルを上から順に処理します。同じファイル内で @include を @mixin の定義より上に書くと、その時点ではまだ定義されていないため見つかりません。mixin や変数の定義はファイルの上部(あるいは専用のパーシャル)にまとめておくのが安全です。
名前が違う、またはスコープの外にある
単純な打ち間違いのほか、セレクタの内側で定義した mixin を外側から呼んでいるケースもあります。セレクタのブロックの中で定義した mixin は、そのブロックの中でしか使えません。どこからでも使いたい mixin は、セレクタの外(ファイルのトップレベル)で定義してください。
なお、引数の数が合っていない場合は Undefined mixin ではなく Missing argument のような別のメッセージが出ます。
まとめ
@mixin はスタイルの塊に名前を付ける機能で、@include で呼び出すとその位置に中身が展開されます。引数を受け取れるので、色やサイズだけが違う似たスタイルを1つの定義から作り分けられ、デフォルト値やキーワード引数、可変長引数($args...)を使えば呼び出し側の記述はさらに短くなります。@content を使えばスタイルのブロックごと渡せるため、メディアクエリを1つの mixin にまとめて、セレクタごとにレスポンシブの指定を書けるようになります。宣言を複製する mixin に対して、@extend はセレクタをまとめる、という出力の違いを押さえておけば選び分けに迷いません。別ファイルの mixin は @use 'mixins' as m; で読み込み、m.名前 の形で呼び出します。Undefined mixin が出たら、@use の有無、名前空間、定義と呼び出しの順序を順に確認してみてください。