カスタムプロパティ (CSS変数) を使った爆速レイアウト術

カスタムプロパティ (CSS変数) を使ってますか?

今回は、HTMLのstyle属性とカスタムプロパティを組み合わせて、多種多様なレイアウトを自由自在に爆速で仕上げる、汎用レイアウト術をご紹介します。

従来のレイアウト方法

従来のレイアウト方法は、主にclass属性値を追加することで実現していると思います。

WordPressのカラムブロックの場合

<div class="wp-block-columns are-vertically-aligned-center">
  <div class="wp-block-column" style="flex-basis: 50%;"> … </div>
  <div class="wp-block-column"> … </div>
  …
</div>

ベースとなるclass属性値 (.wp-block-columns.wp-block-column) に、オプションとなるclass属性値 (.are-vertically-aligned-center) を追加していきます。class属性値だけでは実現できないことは、style属性にCSSプロパティ (flex-basis: 50%;) を直接埋め込んだりします。

Bootstrapのグリッドシステムの場合

<div class="container">
  <div class="row">
    <div class="col"> … </div>
    <div class="col"> … </div>
    <div class="col"> … </div>
    <div class="col"> … </div>
  </div>
  <div class="row">
    <div class="col-8"> … </div>
    <div class="col-4"> … </div>
  </div>
  …
</div>

用意されているclass属性値を各要素に設定していきます。

参考: Grid system (グリッドシステム) ・ Bootstrap v5.0

従来のレイアウト方法の問題点

基本的に用意されているレイアウトしか実現できません。実現したいレイアウトができなければ、新規にCSSコードを書くか、既存のclass属性値を拡張するしかありません。その結果、CSSコードは肥大化していき、不具合の温床にもなりえます。

レイアウトに柔軟性を持たせるため、style属性にCSSプロパティを直接埋め込むこともありますが、これは、HTMLとCSSを1つのシステムで管理することを前提としています。そのため、システムを刷新してHTMLだけを流用するといった場合に不具合が生じる可能性があります。

また、style属性で設定されたCSSプロパティの詳細度が高いため、CSSファイル側で上書きしたい場合には、!important を使用するしかありません。

実際、WordPressのカラムブロックのCSSコードには、flex-basis: 100% !important; が設定された箇所があります。

参考: WordPress/style.css at master · WordPress/WordPress · GitHub

カスタムプロパティを使用したレイアウト方法の実例

従来のレイアウト方法の問題点をクリアしつつ、自由自在かつ爆速にレイアウトするには、必須と任意の2つのカスタムプロパティを使用します。

フレックスボックスレイアウトの場合

必須のカスタムプロパティ
--flex-flow
任意のカスタムプロパティ
--gap
--justify-content
--align-items
--child_flex (子要素への一括設定)
--flex (子要素専用)

最も簡単なHTMLコードは次のようになります。

<div style="--flex-flow: wrap;">
  <div> … </div>
  …
</div>

これだけで、子要素が横並びになり、自動で改行されます。このHTMLコードに適用されるCSSコードは次のようになります。

[style*="--flex-flow:"] {
  --gap: initial;
  --justify-content: initial;
  --align-items: initial;
  --child_flex: initial;
  display: flex;
  flex-flow: var(--flex-flow);
  gap: var(--gap);
  justify-content: var(--justify-content);
  align-items: var(--align-items);
}
[style*="--flex-flow:"] > * {
  --flex: var(--child_flex);
  flex: var(--flex);
  margin: 0;
  max-width: 100%;
  min-width: 0;
}
[style*="--flex-flow:"][type] > li {
  list-style-position: inside;
}
[style*="--flex-flow:"]:not([type]) > li {
  list-style-type: none;
}
dl[style*="--flex-flow:"] > div > * {
  margin-inline: 0;
}

style属性に --flex-flow が設定されると、display: flex; と各カスタムプロパティが設定されます。任意のカスタムプロパティがstyle属性に設定されると、初期値の initial を上書きする仕組みになっています。

次のように、各カスタムプロパティを自由自在に設定できるので、CSSコードを一切いじることなく、思い通りのレイアウトが実現できます。

<div style="--flex-flow: row-reverse wrap; --gap: 0 1em; --justify-content: center; --align-items: center; --child_flex: 1 1 auto;">
  <div style="--flex: 1 1 50%;"> … </div>
  <div> … </div>
  …
</div>

グリッドレイアウトの場合

必須のカスタムプロパティ
--grid-template
任意のカスタムプロパティ
--grid-template-areas
--gap
--place-content
--place-items
--grid-area (子要素専用)
--z-index (子要素専用)

最も簡単なHTMLコードは次のようになります。

<div style="--grid-template: none / repeat(2, 1fr);">
  <div> … </div>
  …
</div>

これだけで、行が暗黙的に生成される2等分の列が作られます。このHTMLコードに適用されるCSSコードは次のようになります。

[style*="--grid-template:"] {
  --grid-template-areas: initial;
  --gap: initial;
  --place-content: initial;
  --place-items: initial;
  display: grid;
  grid-template: var(--grid-template);
  grid-template-areas: var(--grid-template-areas);
  gap: var(--gap);
  place-content: var(--place-content);
  place-items: var(--place-items);
}
[style*="--grid-template:"] > * {
  --grid-area: initial;
  --z-index: initial;
  grid-area: var(--grid-area);
  margin: 0;
  max-width: 100%;
  min-width: 0;
  z-index: var(--z-index);
}
[style*="--grid-template:"][type] > li {
  list-style-position: inside;
}
[style*="--grid-template:"]:not([type]) > li {
  list-style-type: none;
}
dl[style*="--grid-template:"] > div > * {
  margin-inline: 0;
}

style属性に --grid-template が設定されると、display: grid; と各カスタムプロパティが設定されます。

グリッドレイアウトはフレックスボックスレイアウトとは異なり、要素が重なりあうようなレイアウトも簡単に実現できます。

<div style="--grid-template: repeat(3, auto) / repeat(3, 1fr);">
  <div style="--grid-area: 1 / 1 / 3 / 3;"> … </div>
  <div style="--grid-area: 2 / 2 / 4 / 4;"> … </div>
</div>

マルチカラムレイアウト (段組みレイアウト) の場合

必須のカスタムプロパティ
--columns
任意のカスタムプロパティ
--column-rule
--column-gap
--child_margin-block-end (子要素への一括設定)
margin-block-end (子要素専用)

最も簡単なHTMLコードは次のようになります。

<div style="--columns: 10em / 4;">
  <div> … </div>
  …
</div>

これだけで、最小で10文字分の列が最大で4つ作られます。このHTMLコードに適用されるCSSコードは次のようになります。

[style*="--columns:"] {
  --column-rule: initial;
  --column-gap: initial;
  --child_margin-block-end: initial;
  columns: var(--columns);
  column-rule: var(--column-rule);
  column-gap: var(--column-gap);
}
[style*="--columns:"] > * {
  break-inside: avoid-column;
  margin: 0;
}
[style*="--columns:"] > :not(:last-child) {
  --margin-block-end: var(--child_margin-block-end);
  margin-block-end: var(--margin-block-end);
}
[style*="--columns:"][type] < li {
  list-style-position: inside;
}
[style*="--columns:"]:not([type]) > li {
  list-style-type: none;
}
dl[style*="--columns:"] > div > * {
  margin-inline: 0;
}

マルチカラムレイアウトが使えるBootstrapのようなフレームワークは見たことがありません (調査不足なだけかもしれませんが)。というのも、何文字分の幅と何列必要になるかは、コンテンツの量によって異なるため、class属性値として定義するのが難しいためです。

style属性とカスタムプロパティであれば、コンテンツ量に応じて細かく設定できるので、マルチカラムレイアウトにおけるベストプラクティスかもしれません。

なぜレイアウトにカスタムプロパティなのか?

カスタムプロパティは単なる変数のようなものなので、style属性値としてHTMLに埋め込んでも、それ単体では何も起きません。つまり、HTML単体の流用が容易になります。これはclass属性値と変わりありません。

そのため、埋め込んだカスタムプロパティをどのように扱うかは、class属性値と同様にCSSコードを書く側の裁量に委ねられます。

例えば、ウェブサイトの余白ルールが厳密に決まっている場合、初期値に既定値を設定したり、特定のカスタムプロパティを無視して固定値を設定できます。

/* 既定値の設定例 */
[style*="--grid-template:"] {
  --grid-template-areas: initial;
  --gap: 1rem 16px;
  --place-content: initial;
  --place-items: initial;
  …
}

/* 固定値の設定例 */
[style*="--grid-template:"] {
  …
  display: grid;
  grid-template: var(--grid-template);
  grid-template-areas: var(--grid-template-areas);
  gap: 1rem 16px;
  place-content: var(--place-content);
  place-items: var(--place-items);
}

このように、柔軟で汎用的に使用できるにも関わらず、フレックスボックスレイアウト、グリッドレイアウト、マルチカラムレイアウトの3つのCSSコードは合計しても、約1.5KBしかありません。

[style*="--columns:"]{--column-rule:initial;--column-gap:initial;--child_margin-block-end:initial;columns:var(--columns);column-rule:var(--column-rule);column-gap:var(--column-gap);}[style*="--columns:"]>*{break-inside:avoid-column;margin:0;}[style*="--columns:"]>:not(:last-child){--margin-block-end:var(--child_margin-block-end);margin-block-end:var(--margin-block-end);}[style*="--flex-flow:"]{--gap:initial;--justify-content:initial;--align-items:initial;--child_flex:initial;display:flex;flex-flow:var(--flex-flow);gap:var(--gap);justify-content:var(--justify-content);align-items:var(--align-items);}[style*="--flex-flow:"]>*{--flex:var(--child_flex);flex:var(--flex);margin:0;max-width:100%;min-width:0;}[style*="--grid-template:"]{--grid-template-areas:initial;--gap:initial;--place-content:initial;--place-items:initial;display:grid;grid-template:var(--grid-template);grid-template-areas:var(--grid-template-areas);gap:var(--gap);place-content:var(--place-content);place-items:var(--place-items);}[style*="--grid-template:"]>*{--grid-area:initial;--z-index:initial;grid-area:var(--grid-area);margin:0;max-width:100%;min-width:0;z-index:var(--z-index);}:is([style*="--columns:"],[style*="--flex-flow:"],[style*="--grid-template:"])[type]>li{list-style-position:inside;}:is([style*="--columns:"],[style*="--flex-flow:"],[style*="--grid-template:"]):not([type])>li{list-style-type:none;}dl:is([style*="--columns:"],[style*="--flex-flow:"],[style*="--grid-template:"])>div>*{margin-inline:0;}

使ってみたくなりませんか?

応用例

メディアクエリと組み合わせると、更に便利になります。フレックスボックスレイアウトの場合は次のようなCSSコードになります。

[style*="--flex-flow:"] {
  --gap: initial;
  --justify-content: initial;
  --align-items: initial;
  --child_flex: initial;
  display: flex;
  flex-flow: var(--flex-flow);
  gap: var(--gap);
  justify-content: var(--justify-content);
  align-items: var(--align-items);
}
[style*="--flex-flow:"] > * {
  --flex: var(--child_flex);
  flex: var(--flex);
  margin: 0;
  max-width: 100%;
  min-width: 0;
}
[style*="--flex-flow:"][type] > li {
  list-style-position: inside;
}
[style*="--flex-flow:"]:not([type]) > li {
  list-style-type: none;
}
dl[style*="--flex-flow:"] > div > * {
  margin-inline: 0;
}
@media (min-width: 513px) {
  [style*="--flex-flow:"][style*="--larger_flex-flow:"] {
    flex-flow: var(--larger_flex-flow);
  }
  [style*="--flex-flow:"][style*="--larger_gap:"] {
    gap: var(--larger_gap);
  }
  [style*="--flex-flow:"][style*="--larger_justify-content:"] {
    justify-content: var(--larger_justify-content);
  }
  [style*="--flex-flow:"][style*="--larger_align-items:"] {
    align-items: var(--larger_align-items);
  }
  [style*="--flex-flow:"][style*="--larger_child_flex:"] > * {
    flex: var(--larger_child_flex);
  }
  [style*="--flex-flow:"] > [style*="--larger_flex:"] {
    flex: var(--larger_flex);
  }
}
@media (min-width: 1025px) {
  [style*="--flex-flow:"][style*="--largest_flex-flow:"] {
    flex-flow: var(--largest_flex-flow);
  }
  [style*="--flex-flow:"][style*="--largest_gap:"] {
    gap: var(--largest_gap);
  }
  [style*="--flex-flow:"][style*="--largest_justify-content:"] {
    justify-content: var(--largest_justify-content);
  }
  [style*="--flex-flow:"][style*="--largest_align-items:"] {
    align-items: var(--largest_align-items);
  }
  [style*="--flex-flow:"][style*="--largest_child_flex:"] > * {
    flex: var(--largest_child_flex);
  }
  [style*="--flex-flow:"] > [style*="--largest_flex:"] {
    flex: var(--largest_flex);
  }
}

もしくは、カスタムプロパティの値を先に確定しておく、次のような書き方もできます。

[style*="--flex-flow:"] {
  --gap: initial;
  --justify-content: initial;
  --align-items: initial;
  --child_flex: initial;
  --larger_flex-flow: var(--flex-flow);
  --larger_gap: var(--gap);
  --larger_justify-content: var(--justify-content);
  --larger_align-items: var(--align-items);
  --larger_child_flex: var(--child_flex);
  --largest_flex-flow: var(--larger_flex-flow);
  --largest_gap: var(--larger_gap);
  --largest_justify-content: var(--larger_justify-content);
  --largest_align-items: var(--larger_align-items);
  --largest_child_flex: var(--larger_child_flex);
  flex-flow: var(--flex-flow);
  gap: var(--gap);
  justify-content: var(--justify-content);
  align-items: var(--align-items);
}
[style*="--flex-flow:"] > * {
  --flex: var(--child_flex);
  --larger_flex: var(--larger_child_flex);
  --largest_flex: var(--largest_child_flex);
  flex: var(--flex);
}
@media (min-width: 513px) {
  [style*="--flex-flow:"] {
    flex-flow: var(--larger_flex-flow);
    gap: var(--larger_gap);
    justify-content: var(--larger_justify-content);
    align-items: var(--larger_align-items);
  }
  [style*="--flex-flow:"] > * {
    flex: var(--larger_flex);
  }
}
@media (min-width: 1025px) {
  [style*="--flex-flow:"] {
    flex-flow: var(--largest_flex-flow);
    gap: var(--largest_gap);
    justify-content: var(--largest_justify-content);
    align-items: var(--largest_align-items);
  }
  [style*="--flex-flow:"] > * {
    flex: var(--largest_flex);
  }
}

次のHTMLコードのように設定した場合、画面サイズが512px以下なら1列、画面サイズが513px以上なら2列、画面サイズが1025px以上なら3列になります。

<div style="--flex-flow: wrap; --gap: 1rem; --child_flex: 0 0 100%; --larger_child_flex: 0 0 calc((100% - 1rem) / 2); --largest_child_flex: 0 0 calc((100% - 2rem) / 3);">
  …
</div>

HTMLのstyle属性を使うのは、好きではありません。でも、

<div class="wp-block-media-text is-vertically-aligned-center has-media-on-the-right is-stacked-on-mobile" style="grid-template-columns:auto 75%"> … </div>

「セマンティック」とは言えない長いclass属性値をいくつも付けて、さらにstyle属性にCSSプロパティを直に埋め込むよりも、マシなんじゃないかと思います。

関連記事