SCSSを深堀って設計してみた 2

2025.07.11 09:00
2025.07.10 23:07
SCSSを深堀って設計してみた 2

前回の続きです。
今回は他のマップも作ってみます。

color-map:ブランドカラーも入れてみた

色も最初は $red, $blue みたいにしてたんだけど、だんだん「この赤ってどの赤?危険用?ボタン用?」みたいになってきたので、フォントの時と同じく、意味ベースの color-map にしました。

@use './variables' as *;

$color-map: (

  // === テキスト系 ===
  'text': (
    'default': $color-black,                 // 主に本文テキストやラベルなど(濃いめ)
    'sub': $color-gray-600,                  // サブ情報・補足文など(薄いめ)
    'extra-sub': $color-gray-400,            // サブ情報・補足文など(薄いめ)
    'inverse': $color-white,                 // ダーク背景上の白文字(反転)
    'link': $color-blue,                     // アンカーリンク
    'error': $color-red,                     // バリデーションエラーや警告文など
    'accent': $color-blue,                   // アクセント文字(CTAなど)
  ),

  // === 背景色 ===
  'bg': (
    'plain': $color-white,
    'default': $color-gray,                  // 標準背景(ページ全体やベースレイヤーに使用)
    'inverse': $color-black,                 // 反転背景
    'inverse-veil': rgba(0, 0, 0, .1),
    'subtle': $color-gray-100,               // サブ背景(セクション分けやブロックの区切り)
    'emphasis': $color-gray-200,             // 強調背景(注目させたい領域やカード内背景など)
    'label': $color-gray-300,                // 強調背景(注目させたい領域やカード内背景など)
    'error': $color-red,                     // エラー表示用背景(バナーやアラートなど)
    'accent': $color-blue,                   // アクセント背景(CTA・強調ブロック・アイキャッチ領域など)
  ),

   // === ボタン系 ===
  'btn': (
    'primary-bg': $color-blue,               // プライマリボタン背景色
    'primary-text': $color-white,            // プライマリボタン文字色
    'secondary-bg': $color-gray-200,         // セカンダリボタン背景色
    'secondary-text': $color-black,          // セカンダリボタン文字色
    'error-bg': $color-red,                  // エラーボタン背景色
    'error-text': $color-white               // エラーボタン文字色
  ),

このマップを使うためのmixinも以下のように作ってみました。

// ==================================================
// Mixin/Function: color
// カラーマップから意味ベースで色を取得する
// ==================================================

@use 'sass:map';
@use '../foundation' as *;

@function color($group, $key) {
  @return map.get(map.get($color-map, $group), $key);
}

// 例:@include color('text', 'base');
@mixin color($group: 'text', $key: 'default', $property: null) {
  // プロパティの自動判定
  @if $property == null {
    @if $group == 'text' {
      $property: color;
    } @else if $group == 'bg' {
      $property: background-color;
    } @else if $group == 'border' {
      $property: border-color;
    } @else if str-index($key, '-text') {
      $property: color;
    } @else if str-index($key, '-bg') {
      $property: background-color;
    } @else {
      $property: color; // fallback
    }
  }

  $val: color($group, $key);

  @if $val == null {
    @warn "[color] No value found for (#{$group}, #{$key})";
  } @else {
    & {
      #{$property}: $val;
    }
  }
}

使い方はこんな感じです。

// 文字
@include color('text', 'accent');

// 背景
@include color('bg', 'accent');

// ボタン
@include color('button', 'accent');

意味で書けると後からテーマカラーを変えたいときも楽だし、「この色何だっけ問題」が減ります。
ただ、微妙に似てる色が増えてきたので、もう少し整理したいなとも思ってます。

space-map:余白も意味で書けるようにした

余白も最初は気分で書いてたけど、space-map を作って管理するようにしました。
Tailwind の考え方に近くて、xs, sm, md, lg, xl で書いてます。

@use './function' as *;

$space-map: (

  // 無しの場合
  'none': (
    'sm': rem(0),
    'md': rem(0),
    'lg': rem(0)
  ),

  // 最小余白・詰めたい箇所に使用
  'xs': (
    'sm': rem(4),
    'md': rem(4),
    'lg': rem(4)
  ),

  // 通常よりやや小さめの余白
  // 例:アイコンとテキストの間、インライン要素間など
  'sm': (
    'sm': rem(8),
    'md': rem(8),
    'lg': rem(8)
  ),

  // 汎用的な中サイズの余白
  // 例:カード内の余白、要素の上下余白など
  'md': (
    'sm': rem(12),
    'md': rem(16),
    'lg': rem(16)
  ),

  // セクション間の余白などに使いやすいサイズ
  'lg': (
    'sm': rem(20),
    'md': rem(24),
    'lg': rem(24)
  ),

  // セクション見出しの下や大きな間隔に使うサイズ
  'xl': (
    'sm': rem(24),
    'md': rem(32),
    'lg': rem(32)
  ),

  // HERO内など、広めの余白
  '2xl': (
    'sm': rem(32),
    'md': rem(40),
    'lg': rem(40)
  ),

  // セクション区切りなどの大きな余白
  '3xl': (
    'sm': rem(40),
    'md': rem(48),
    'lg': rem(48)
  ),

  // ファーストビュー下部など、画面を区切る余白
  '4xl': (
    'sm': rem(48),
    'md': rem(64),
    'lg': rem(64)
  ),

  // ページ最下部やHEROの下など特大余白
  '5xl': (
    'sm': rem(64),
    'md': rem(80),
    'lg': rem(80)
  ),

  // HERO直後やLP切り替えなど、特大のセクション間余白
  '6xl': (
    'sm': rem(80),
    'md': rem(96),
    'lg': rem(112)
  ),

上記を使うためのspace用mixin。

@use 'sass:string';
@use 'sass:map';
@use '../foundation' as *;

// --------------------------------------------------
// spacing用ショートハンド対応表
// --------------------------------------------------

$spacing-property-map: (
  mt: margin-top,
  mb: margin-bottom,
  ml: margin-left,
  mr: margin-right,
  pt: padding-top,
  pb: padding-bottom,
  pl: padding-left,
  pr: padding-right,
  m: margin,
  p: padding,
  gap: gap
);

// px/py/mx/my 用の展開マップ
$spacing-composite-map: (
  px: (padding-left, padding-right),
  py: (padding-top, padding-bottom),
  mx: (margin-left, margin-right),
  my: (margin-top, margin-bottom)
);

@mixin spacing($direction, $token) {
  $key: string.unquote($token);
  $property: map.get($spacing-property-map, $direction);
  $composite: map.get($spacing-composite-map, $direction);

  @if $property == null and $composite == null {
    @warn "spacing: 不正な direction '#{$direction}'";
    @error "指定された方向 '#{$direction}' は無効です。mt, mb, px, py などを指定してください";
  }

  @if map.get($space-map, $key) == null {
    @warn "spacing: space-map に '#{$key}' が見つかりません";
    @error "スペーシングトークン '#{$key}' が space-map に存在しません";
  }

  $apply-properties: if($composite != null, $composite, ($property,));

  @each $prop in $apply-properties {
    & {
      #{$prop}: map.get(map.get($space-map, $key), sm);
    }

    @include mq(md) {
      & {
        #{$prop}: map.get(map.get($space-map, $key), md);
      }
    }

    @include mq(lg) {
      & {
        #{$prop}: map.get(map.get($space-map, $key), lg);
      }
    }
  }
}

実際にはこんな感じで使います。

// margin
@include spacing('m', 'lg');

// margin-right
@include spacing('ml', 'lg');

// margin-x
@include spacing('mx', 'lg');

// padding-y
@include spacing('py', 'lg');

メディアクエリの書き方

メディアクエリもマップ化できちゃうみたいですね。
これはfoundation/_mq.scssというファイルのみで行います。

// Media Queries
@mixin mq($breakpoint) {
  // スマホ以上
  @if $breakpoint == sm {
    @media (min-width: 480px) { @content; }

  // スマホ・タブレット以上
  } @else if $breakpoint == md {
    @media (min-width: 768px) { @content; }

  // PC以上
  } @else if $breakpoint == lg {
    @media (min-width: 1024px) { @content; }
}

使い方はこんな感じ。

.container {
  width: 100%;
  @include mq(md) {
    // スマホ・タブレット以上にのみ反映させたいCSSを書く
    width: 80%;
  }
}

モバイルファーストで書いて、必要なときにだけ mq() で上書きするスタイル。
これも「もっと細かいブレークポイント欲しいかも…」と思うことがあるので、あとで見直すかもしれません。

でも今までは以下のようにブレークポイントごとに書いていたので、
何が何処にあるかわからなくなりやすかったんだけど、
今は要素の中にブレークポイントを指定できるので、
すっごくわかりやすくなりましたね。

@media all and (min-width: 0) and (max-width: 480px) {

という感じで、今回はここまでのメモでした!
書いてみて思ったのは、設計って「一度決めたら終わり」じゃなくて、運用しながら少しずつ直していくものなんだな、ってことですね。

今回は以上です!