1. ホーム
  2. Sass

【Sass】@function の使い方|値を計算して返す自作の関数を定義する方法

Share

Sass を書いていると、「px を rem に直した値を入れたい」「決めておいた余白の刻みから値を取り出したい」といった、値そのものを計算したい場面が出てきます。そういうときに使うのが @function です。自分で関数を定義しておけば、font-size: rem(24px); のようにプロパティの値として呼び出せるようになります。この記事では @mixin との違いから始めて、基本の書き方、引数の渡し方、使い回せる関数の実例、組み込み関数モジュールの読み込み方、エラーが出たときの確認どころまでを、コンパイル後の CSS と並べて解説します。記法は SCSS を前提にしています。

@mixin との違いは「宣言を出力する」か「値を返す」か

Sass には @mixin と @function という、どちらも名前を付けて使い回すための機能があります。この2つは似ているようで、役割がはっきり分かれています。

@mixin が作るのはスタイルのかたまりです。@include で呼び出すと、その位置に display: flex; のような宣言が丸ごと展開されます。一方 @function が作るのは値ひとつです。呼び出してもスタイルは何も出力されず、計算結果の数値や色が返ってくるだけなので、プロパティの値を書く場所にそのまま置いて使います。

style.scss
@use 'sass:math';

// mixin はスタイルのかたまりを出力する
@mixin square($size) {
  width: $size;
  height: $size;
}

// function は値をひとつ返す
@function half($size) {
  @return math.div($size, 2);
}

.box {
  @include square(200px);   // 宣言そのものが増える
  margin-left: half(200px); // プロパティの値として使う
}
style.css
.box {
  width: 200px;
  height: 200px;
  margin-left: 100px;
}

@include square(200px); は2行の宣言に置き換わり、half(200px) は 100px という値に置き換わっています。この違いを表にすると次のようになります。

@mixin@function
作るものスタイルのかたまり(宣言の集まり)値ひとつ
呼び出し方@include 名前(引数);名前(引数) をプロパティの値として書く
書ける場所セレクタの中など、宣言を書ける場所値を書ける場所ならどこでも(変数の代入も可)
結果の返し方中に書いた宣言がそのまま展開される@return で明示的に返す
向いている用途メディアクエリ、ボタンの共通スタイルなど単位換算、マップからの取り出し、数値の丸めなど

迷ったときは「返したいのは宣言か、値か」で判断します。padding: ...; ごと出力したいなら mixin、padding に入れる数値だけがほしいなら function です。mixin の中で function を呼ぶのもよくある組み合わせで、両者は競合するものではありません。

基本の書き方|@function で定義して @return で値を返す

定義は @function 名前($引数) { ... }、値を返すのは @return 値; です。まずは受け取った数値を2倍にして返すだけの、いちばん短い例を見てみます。

style.scss
@function double($number) {
  @return $number * 2;
}

.box {
  width: double(120px);
}
style.css
.box {
  width: 240px;
}

ポイントは3つあります。ひとつめは、関数の定義そのものは CSS に出力されないことです。呼び出されたところで計算が行われ、結果の値だけが残ります。ふたつめは、値を返すには必ず @return を書くことです。最後に評価した式が自動的に返る、といった仕組みはありません。みっつめは、@return は関数の中でしか使えないことです。セレクタの中やファイルのトップレベルに書くと Error: This at-rule is not allowed here. になります。

また、@return が実行された時点で関数の処理は終わります。@if と @else で条件ごとに別の値を返す、という書き方も自然にできます。

引数の渡し方を使い分ける

引数のまわりの記法は @mixin とまったく同じです。デフォルト値、キーワード引数、可変長引数の3つを覚えておくと、呼び出し側の記述をかなり短くできます。

デフォルト値で省略できるようにする

引数名のうしろにコロンで値を書いておくと、それがデフォルト値になります。呼び出し側で省略すればデフォルト値が使われ、渡せばその値が使われます。次の rem() は、基準サイズを普段は 16px として、必要なときだけ変えられるようにした例です。

style.scss
@use 'sass:math';

@function rem($px, $base: 16px) {
  @return math.div($px, $base) * 1rem;
}

.title {
  font-size: rem(24px);      // $base は 16px のまま
}

.lead {
  font-size: rem(24px, 20px); // 基準を 20px に変える
}
style.css
.title {
  font-size: 1.5rem;
}

.lead {
  font-size: 1.2rem;
}

デフォルト値を持つ引数は、引数リストの後ろにまとめて書きます。デフォルト値のある引数のうしろに、デフォルト値のない引数を置くことはできません。

キーワード引数で指定したい引数だけ渡す

呼び出し側で $引数名: 値 の形で書くと、順番を気にせず、変えたい引数だけを名前付きで渡せます。引数が多い関数ほど、何を渡しているのかが読み取りやすくなります。

style.scss
.note {
  font-size: rem($px: 24px);
}

// すべてキーワードで書けば順番も自由
.sub {
  font-size: rem($base: 20px, $px: 24px);
}

存在しない引数名を書くと Error: No parameter named $b. というエラーになるので、打ち間違いにはすぐ気づけます。

可変長引数($args…)でいくつでも受け取る

引数名のうしろにピリオドを3つ付けると、渡された値をまとめてひとつのリストとして受け取れます。受け取ったあとは @each で順に処理できます。次は渡された数値をすべて足して返す関数です。

style.scss
@function sum($numbers...) {
  $total: 0;

  @each $number in $numbers {
    $total: $total + $number;
  }

  @return $total;
}

.stack {
  padding-top: sum(8px, 16px, 4px);
}
style.css
.stack {
  padding-top: 28px;
}

同じピリオド3つは呼び出し側でも使えて、sum($my-sizes...) と書けばリストの中身を引数として展開して渡せます。

実践例1|px を rem に変換する rem() 関数

@function の使いどころとしていちばん定番なのが、デザインカンプの px 指定を rem に直す関数です。ここで押さえておきたいのが、Dart Sass では / による除算が非推奨だという点です。$px / 16px と書くと DEPRECATION WARNING [slash-div] が出て、将来のバージョンでは削除されます。割り算には sass:math モジュールの math.div() を使い、ファイルの先頭で @use 'sass:math'; と読み込んでおきます。ここでは関数をパーシャル(先頭にアンダースコアを付けた、それ単体では CSS を出力しないファイル)にまとめます。

_functions.scss
@use 'sass:math';

$base-font-size: 16px;

// px を rem に変換する
@function rem($px, $base: $base-font-size) {
  @return math.div($px, $base) * 1rem;
}
style.scss
@use 'functions' as fn; // 先頭のアンダースコアと拡張子は書かない

.card {
  padding: fn.rem(24px);
  font-size: fn.rem(14px);
}
style.css
.card {
  padding: 1.5rem;
  font-size: 0.875rem;
}

math.div(24px, 16px) は px どうしの割り算なので単位が打ち消し合い、1.5 という単位のない数値になります。そこに * 1rem を掛けることで 1.5rem という値ができあがる、という仕組みです。@use 'functions' as fn; で読み込んだ関数は fn.rem() のように名前空間を付けて呼び出します。

実践例2|余白のスケールを map から引く関数

余白の値を 4px、8px、16px、32px のように決めておき、そこからしか選ばないようにすると、サイト全体の余白が揃います。マップ(キーと値の組)に一覧を持たせ、キーを渡すと値が返る関数を用意しておくと、使う側は数値を覚えなくて済みます。

_functions.scss
@use 'sass:map';

$spacers: (
  'xs': 4px,
  'sm': 8px,
  'md': 16px,
  'lg': 32px,
);

@function space($key, $scale: 1) {
  // 定義されていないキーならコンパイルを止める
  @if not map.has-key($spacers, $key) {
    @error '"#{$key}" は $spacers に定義されていません。';
  }

  @return map.get($spacers, $key) * $scale;
}
style.scss
.card {
  padding: space('md');
  margin-bottom: space('lg');
  gap: space('sm', 1.5); // 8px の 1.5 倍
}
style.css
.card {
  padding: 16px;
  margin-bottom: 32px;
  gap: 12px;
}

map.get() は存在しないキーを渡すと null を返して黙って通してしまいます。関数で包んでおけば、その手前でキーの誤りを検査できます。この「間に一枚かませる」ことで検査を挟めるのが、関数にするもうひとつの利点です。

実践例3|数値を丸める・上限下限に収める関数

行間の計算などで割り算をすると、1.5555555556 のような長い小数がそのまま CSS に出力されます。math.round() は整数に丸める関数なので、桁数を指定して丸めたいときは、いったん 10 の累乗を掛けて丸め、同じ数で割り戻します。

もうひとつ、値を「最小値と最大値の範囲に収める」には math.clamp() が使えます。引数の順番は math.clamp($min, $number, $max) で、CSS の clamp() と同じ並びです。こちらはコンパイル時に計算されるので、出力される CSS には固定の数値が入ります。

_functions.scss
@use 'sass:math';

// 小数点以下の桁数を指定して丸める
@function round-to($number, $digits: 0) {
  $factor: math.pow(10, $digits);
  @return math.div(math.round($number * $factor), $factor);
}

// 最小値と最大値の範囲に収める
@function clamp-size($number, $min, $max) {
  @return math.clamp($min, $number, $max);
}
style.scss
@use 'sass:math';

.ratio {
  line-height: round-to(math.div(28, 18), 2); // 1.5555... を小数第2位まで
  width: clamp-size(480px, 320px, 400px);     // 上限の 400px に丸まる
  height: clamp-size(200px, 320px, 400px);    // 下限の 320px に丸まる
}
style.css
.ratio {
  line-height: 1.56;
  width: 400px;
  height: 320px;
}

組み込み関数はモジュールごとに @use で読み込む

ここまで出てきた math.div() や map.get() は、Sass にあらかじめ用意されている組み込み関数です。これらは用途ごとのモジュールに分かれていて、使う前に @use で読み込み、名前空間を付けて呼び出すのが現在の書き方です。主なモジュールは次の5つです。

モジュール扱うもの代表的な関数
sass:math数値の計算math.div() / math.round() / math.ceil() / math.clamp() / math.pow() / math.unit() / math.is-unitless()
sass:color色の加工color.adjust() / color.scale() / color.mix()
sass:string文字列の操作string.quote() / string.unquote() / string.slice() / string.index()
sass:listリストの操作list.nth() / list.length() / list.append() / list.join()
sass:mapマップの操作map.get() / map.has-key() / map.merge() / map.keys()

古い記事では round() や nth() のように名前空間なしで書かれていることがありますが、これはモジュールシステムができる前の書き方です。新しく書くコードは @use 'sass:math'; と読み込んだうえで math.round() と呼ぶ形に揃えておくと、どのモジュール由来かが一目で分かります。なお @use はファイルの先頭に書き、書いたファイル自身にしか効かないので、使うファイルごとに必要です。

@if と @error で引数をチェックする

自作の関数は、想定と違う値を渡されても気づきにくいのが難点です。rem(24px) のつもりで rem(2em) と書いてしまうと、おかしな値が黙って出力されかねません。そこで @if で条件を調べ、当てはまらなければ @error でコンパイルを止めます。

_functions.scss
@use 'sass:math';

@function rem($px, $base: 16px) {
  @if math.is-unitless($px) {
    @error '#{$px} には px 単位を付けてください。';
  } @else if math.unit($px) != 'px' {
    @error '#{$px} の単位は px ではありません(#{math.unit($px)})。';
  }

  @return math.div($px, $base) * 1rem;
}

この状態で font-size: fn.rem(2em); と書くと、コンパイルは次のように止まります。エラー文には #{...} で値を埋め込めるので、どの値が悪かったのかまで伝えられます。

ターミナル
Error: "2em の単位は px ではありません(em)。"
  ╷
8 │   font-size: fn.rem(2em);
  │              ^^^^^^^^^^^
  ╵
  style.scss 8:14  root stylesheet

@error はコンパイルを完全に止めます。止めるほどではなく注意を促したいだけなら @warn を使えば、メッセージを表示したうえで処理を続けられます。

@function でエラーになるとき

自作の関数を書き始めると、決まったところでつまずきます。よく出会う4つのケースを実際のメッセージと一緒に見ていきます。

エラーも出ずに、呼び出しがそのまま CSS に出力される

いちばん気づきにくいのがこれです。@use を書き忘れた状態で margin: spacing(2); と書いても、エラーは出ずに margin: spacing(2); がそのまま CSS に出力されます。CSS には rgb() や var() のように無数の関数があるため、Sass は知らない名前の呼び出しを「CSS の関数だろう」と判断して素通しするからです。スタイルが効かないときは、出力された CSS に関数の呼び出しが文字のまま残っていないか確認してみてください。

ただしキーワード引数を使っていると Error: Plain CSS functions don't support keyword arguments. というエラーになります。これが出たら「Sass の関数として認識されていない」というサインです。関数名によっては別のメッセージが出ることもあり、たとえば CSS 側にも同名の rem() 関数があるため、自作の rem() が読み込まれていないと Error: 2 arguments required, but only 1 was passed. という一見無関係なエラーになります。

There is no module with namespace “math”.

math.div() を使っているのに、そのファイルに @use 'sass:math'; を書いていないと出るエラーです。math という名前空間が存在しない、という意味なので、ファイルの先頭に読み込みを追加すれば解決します。map.get() なら @use 'sass:map';、list.nth() なら @use 'sass:list'; と、使う関数に合わせてモジュールを読み込みます。

読み込みはしてあるのに Error: Undefined function. が出る場合は、そのモジュールに存在しない関数名を書いています。math.divide() のような打ち間違いがないか、公式ドキュメントで正式名を確認してください。

16px + 1rem のように単位が混ざっている

関数の中で計算するとき、足し算や引き算では単位をそろえる必要があります。16px + 1rem と書くと Error: 16px and 1rem have incompatible units. になります。px と rem は互いに換算できない(rem がいくつの px になるかはブラウザーの設定次第)ため、Sass は変換を拒否するのです。

対処としては、計算に入れる値の単位をどちらかに統一します。上で作った rem() のように math.unit() で単位を調べてエラーを出しておくと、原因が分かりやすい形で止まってくれます。なお 16px * 2 のように単位のない数値を掛けるのは問題ありませんし、16px + 8px のように単位がそろっていれば計算できます。

/ で割ったつもりが割り算になっていない

CSS では font: 16px/1.5 のようにスラッシュを区切り記号として使う書き方があります。そのため Sass は、数値をそのまま並べた line-height: 16px / 1.5; を割り算とは解釈せず、line-height: 16px/1.5; という文字の並びとして出力します。計算されないのにエラーも出ないので、値がおかしいことに気づきにくい書き方です。

変数や関数の戻り値を / で割ると割り算として扱われますが、こちらは DEPRECATION WARNING [slash-div] が出ます。Dart Sass 2.0.0 で削除される予定の書き方なので、割り算は math.div() に統一するのが確実です。既存のコードをまとめて書き換えたいときは、公式が用意している自動移行ツール(sass-migrator)も使えます。

まとめ

@function は「値ひとつ」を返す自作の関数を定義する機能です。スタイルのかたまりを出力する @mixin に対して、計算結果をプロパティの値として使えるのが @function です。定義は @function 名前($引数) { ... }、値を返すのは @return で、これは関数の中でしか書けません。引数まわりの記法は mixin と同じで、デフォルト値・キーワード引数・可変長引数($args...)がそのまま使えます。px を rem に変換する関数、マップから余白を引く関数、小数を丸める関数あたりを用意しておくと、日々のコーディングがぐっと楽になります。math.div() などの組み込み関数は @use 'sass:math'; で読み込んで名前空間付きで呼び出し、@if と @error で引数をチェックしておけば、間違った値を渡したときにコンパイルが止まって教えてくれます。

参考ページ