見出し画像

最近(2023末ごろまで)のCSS世界で気になるまとめ

この記事はnote株式会社 Advent Calendar 2023の6日目の記事です。

ぼくはUXエンジニアとして、デザインシステム構築やアクセシビリティ向上などなど、組織横断的なUX課題を頑張ろうということで活動しています🐈‍⬛


最近(2020~)のCSSを知りたい

最近(と言うか昨今というか20年代)CSSのキャッチアップをしっかりできてないなーということで、断片的に見かけた情報や、そういえばあれどうだったけ..というのを集めつつ、フロントエンドチームの勉強会に持ち込んで、みんなで共有してアップデートしようぜという会を行ってみたので、記事にメモしておこうと思います。

実用的なものに関わらず、おもしろそうなものや、個人的に興味が湧いたものを中心に紹介します。

Grid / Flex

まずは外せないけど、都度調べちゃうこと受け合いなGridに触れていきます。最近はどこでもよく見かけるようになりましたね、ぼくも積極的に使うようになった気がします。

CSS Gridは、ウェブページのレイアウトを作成するための強力なツールです。これは、行と列の2次元のグリッドシステムを提供し、このグリッド上に要素を配置することができます。以下に、CSS Gridの主な特徴をいくつか挙げます(結構たくさんある)。

<div class="grid-container">
  <div class="grid-item">1</div>
  <div class="grid-item">2</div>
  <div class="grid-item">3</div>
</div>
.grid-container {
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr 1fr 1fr;
}

CSS Grid 基礎

  • 2次元のレイアウト: CSS Gridは、行と列の両方で要素を配置することができます。これにより、複雑なレイアウトも簡単に作成できる

  • グリッド領域: CSS Gridでは、複数のセルを組み合わせて「グリッド領域」を作成することができます。これにより、大きな要素を簡単に配置することができます。

  • フレキシブルなサイズ指定: CSS Gridでは、fr単位を使用して、利用可能なスペースを等しく分割することができます。また、minmax関数を使用して、セルの最小サイズと最大サイズを指定することもできます。

  • グリッドギャップ: CSS Gridでは、行と列の間のギャップを簡単に設定することができます。これにより、要素間のスペースを一貫して管理することができます。

  • グリッドテンプレート: CSS Gridでは、 grid-template-areas プロパティを使用して、グリッドのレイアウトを視覚的に定義することができます。これにより、レイアウトの作成がより直感的になります。これらの特徴により、CSS Gridはウェブデザインにおける強力なツールとなっています。これを理解し、適切に使用することで、効率的に美しいレイアウトを作成することができます。

単位・関数について

  • fr 単位: fr は「fraction」の略で、利用可能なスペースの一部を表します。例えば、 grid-template-columns: 1fr 2fr; と指定すると、最初の列は利用可能なスペースの1/3を、2番目の列は2/3を占めるようになります。これにより、要素のサイズを柔軟に調整することができます。

  • minmax 関数: minmax 関数は、グリッドトラック(行や列)の最小サイズと最大サイズを指定します。例えば、 grid-template-columns: minmax(100px, 1fr); と指定すると、列の最小幅は100px、最大幅は利用可能なスペース全体(1fr)となります。これにより、要素のサイズが一定の範囲内に収まるように制御することができます。これらの機能を使用することで、CSS Gridは非常に柔軟なレイアウトを作成することが可能です。特にレスポンシブデザインにおいて、これらの機能は非常に有用で、さまざまなデバイスサイズに対応したレイアウトを効率的に作成することができます。

  • repeat() 関数: この関数は、引数として与えられたスタイルを繰り返します。

  • auto-fill: コンテナの幅を最大限に利用するために必要なだけ列を自動的に作成します。つまり、コンテナの幅に合わせて、可能な限り多くの列を作成します。

  • repeat(auto-fill, minmax(200px, 1fr))

    • 「最小幅200px、最大幅は利用可能なスペース全体で、コンテナの幅に合わせて可能な限り多くの列を作成する」

といった(めっちゃ多い)事前知識があれば、基本的にCSS Gridでやりたいことはできるでしょう。

grid-template-areas

<div class="grid-container">
  <header class="header">Header</header>
  <div class="sidebar">Sidebar</div>
  <main class="main">Main</main>
  <footer class="footer">Footer</footer>
</div>
.grid-container {
  display: grid;
  grid-template-columns: 200px 1fr;
  grid-template-rows: 50px 1fr 50px;
  grid-template-areas: 
    "header header"
    "sidebar main"
    "footer footer";
}

.header {
  grid-area: header;
}

.sidebar {
  grid-area: sidebar;
}

.main {
  grid-area: main;
}

.footer {
  grid-area: footer;
}

この例では、 grid-template-columns と grid-template-rows を使用して、コンテナを2列と3行のグリッドに設定しています。そして、 grid-template-areas を使用して、各エリアの配置を定義しています。各要素は、 grid-area プロパティを使用して、定義したエリアに配置されます。

Subgrid

また、子要素にgridを渡せるSubgridを利用すると、子ども同士の内部要素の高さをそろえるようなレイアウトを組めるようになります。次の記事では丁寧に解説されていて、イメージがわかりやすくおすすめです。

GridとFlexboxの使い分け

最後に使い分けや使い所についてまとめておきます。グリッドをどう引きたいかにつきますね。

  • Flexboxは、1次元のレイアウトを作成するためにつかう。つまり、要素を行または列に沿って配置する場合に使用します。

  • Gridは、2次元のレイアウトを作成するために最適です。つまり、要素を行と列の両方に沿って配置する場合に使用します。

container queries

従来のメディアクエリはビューポートの幅に基づいていますが、コンテナクエリはコンポーネントの親コンテナの幅に基づいてスタイルを適用します。

コンテナクエリを使用するには、親要素に container-type: inline-size; を設定、@containerの中で、特定の幅に基づいてスタイルを適用します。

<div class="grid">
  <div class="grid__item">
    <article class="article">
      <!-- content -->
    </article>
  </div>
  <!-- other articles.. -->
</div>
.grid__item {
  container-type: inline-size;
}

@container (min-width: 60rem) {
  .article {
    display: flex;
    flex-wrap: wrap;
  }
}

画面幅を意識せずにレイアウトデザインを設計できるのはかなり楽ですね。Figmaにcontainer queryのような概念が取り入れられると、捗りそうです。マルチデバイスに対応したレイアウトや、リキッドな要素のイメージを持ってデザインできると、どんどん活用できそう。

Container query length units

  • cqw (container query width): コンテナの幅の1%に相当します。

  • cqh (container query height): コンテナの高さの1%に相当します。

content visibility

コンテンツをセクションに分割し、各セクションに「content-visibility: auto」を適用することで、表示されていないセクションのレイアウトとスタイルの計算をスキップできます。

Pros

  • パフォーマンス向上

  • 表示されていないコンテンツのレンダリング作業がスキップされ、CPUとメモリ使用量が削減

  • キャッシュされたレイアウトステートを保持、コンテンツが再表示された時のレンダリングコストが低減

Cons

  • コンテンツサイズの推定が正しくない場合、スクロールバーの動きががくつくかも

  • アクセシビリティ面で注意

というように特徴をまとめましたが、考慮点は対策できる部分もあります。

contain-intrinsic-size

コンテンツに高さが指定されていない場合、ブラウザはその要素を高さ0でレイアウトします。これではスクロールバーの動きが不安定になる可能性があります。この問題を回避するために、contain-intrinsic-sizeを指定します。これにより、「コンテンツが含有されている場合の要素の自然なサイズ」を決められます。レンダリングされていないコンテンツセクションでも、指定された高さでレイアウトされます。

.section {
  content-visibility: auto;
  contain-intrinsic-size: 1000px; /* Explained in the next section. */
}

アクセシビリティ対策

表示スタイルが「display: none」や「visibility: hidden」のランドマーク要素は、表示されていないにも関わらず、アクセシビリティツリー上には表示されてしまいます。このままではアクセシビリティツール上でランドマークが重複して表示され、よくない状態です。この問題を回避するためには、ランドマーク要素にaria-hidden=“true”を指定する必要があります。

aspect-ratio

padding を使ったレスポンシブテクニックは使わないようになり、aspect-ratioで指定できるいい時代になりました。よくこういうコードを見ますね。

.image {
  aspect-ratio: 16 / 9;
  width: 100%;
  object-fit: cover;
}

lvh, svh, dvh

これらは動的に伸縮されるビューポートを持つスマホなどで、相対的に指定できる単位です。

lvh

lvhはLarge Viewport Unitsの略で、ブラウザ側のUIが最も小さく、コンテンツが最も大きいときのサイズを基準にします。

svh

svhはSmall Viewport Unitsの略で、ブラウザ側のUIが最大で、ウェブサイトのコンテンツが最小のときのサイズを基準にします。

dvh

dvhはDynamic Viewport Unitsの略で、動的に拡大・縮小されるブラウザ側のUIを考慮して、変化に追従するコンテンツサイズを基準にします。

CSS Logical Properties

CSS Logical Propertiesは、HTML文書の方向に依存するプロパティを使用します。物理的な方向(上、下、左、右)ではなく、文書の方向(例:左から右、右から左)に基づいて動作します。i18nを突き詰めて考えるなら、この指定が必要になってくるので、大規模なコンポーネントライブラリなどで見かけますね。

パディング、ボーダー、マージン

.card {
  padding-inline-start: 2.5rem; /* 左右の文書方向に基づくパディングの開始 */
  padding-inline-end: 1rem;     /* 左右の文書方向に基づくパディングの終了 */
  border-inline-start: 6px solid blue; /* 左右の文書方向に基づくボーダーの開始 */
}

.element {
  inset-inline-end: 20px; /* 文書方向に基づいたインライン方向の終端の位置 */
}

アラインメント

.text {
  text-align: end; /* 文書方向に基づいたテキストの揃え方 */
}

ポジショニング

.element {
  inset-inline-end: 20px; /* 文書方向に基づいたインライン方向の終端の位置 */
}

フロート

.float-element {
  float: inline-start; /* 文書方向に基づいたインライン方向の開始部分にフロート */
}

ちなみにグリッドとフレックスボックスはデフォルトでに文書の方向に基づいて自動的に反転します。

CSS prefers-reduced-transparency

prefers-reduced-transparency は、ユーザーが透明度の低減を好むかどうかを検出するためのメディアクエリです。

これは、アクセシビリティを向上させるために設計されており、視覚的な特性を持つユーザーにとって有益なオプションとして機能します。ユーザーがOSの設定で透明度を低減するオプションを選択している場合、このメディアクエリは true を返します。これにより、開発者は透明度を使用したデザイン要素を調整し、より読みやすく目に優しいコンテンツを提供することができます。

例えば、次のように書きます。

@media (prefers-reduced-transparency: reduce) {
  /* 透明度を低減する設定を好むユーザー向けのスタイル */
  .element {
    background-color: rgba(255, 255, 255, 0.5); /* 半透明ではなく */
    background-color: #FFFFFF; /* 不透明な白色に変更する */
  }
}

light-dark()

二つの色値を引数として受け取り、ユーザーのシステム設定に基づいてどちらかの色を出力します。

次のコードは :root に color-scheme プロパティを設定し、カスタムプロパティを使用して light-dark() 関数を適用し、テキストの色をモードに応じて切り替えるサンプルです。

:root {
    color-scheme: light dark;
    --text-color: light-dark(#000000, #ffffff);
}

.element {
  color: var(--text-color);
  background-color: light-dark(#ffffff, #000000);
}

xywh()

CSSで矩形を定義するために使用する形状関数です。指定された距離(左端(x)および上端(y)からの距離)と指定された幅(w)および高さ(h)を使用して矩形を作成します​​。

.thing-that-moves {
  offset-path: xywh(0 20px 100% calc(100% - 20px));
}

.thing-that-is-clipped {
  clip-path: xywh(0 0 100% 100% round 5px);
}

右端や下端からクリップするのではなく、ボックスのサイズを指定することができるので矩形の制御が直感的でrect()より使いやすそうですね。また、border-radius との組み合わせることができ、円形の角を持つ矩形の作成など、より複雑な形状のデザインを実現できます。

lab(), lch(), color()

これらの関数は、色の表現力を大幅に拡張してくれます。特に、lab()lch() は人の視覚に近い方法で色を表現するため、より自然で鮮明な色の表現が可能になります。また、color() 関数により、開発者はさまざまな色空間を柔軟に利用できるようになりました。

lab() 関数

lab() 関数は、CIELAB色空間を使用して色を指定します。CIELAB色空間は、人間の視覚に近い形で色の差異を表現できるため、より自然な色の変化を表現することができます。この関数は、明度(L)、a成分(赤から緑への色相)、b成分(黄から青への色相)をパラメータとして受け取ります。写真のアプリケーションのようなパラメーターでわかりやすいですね。

   .element {
     color: lab(68.29% 20 -47);
   }

この例では、 lab() 関数を使用して特定の色を指定しています。

lch() 関数

lch() 関数は、CIELCH色空間(CIELAB色空間の円筒座標版)を使用して色を指定します。この関数は、明度(L)、彩度(C)、色相(H)をパラメータとして受け取ります。色相は角度(度)で指定され、彩度は色の鮮やかさを表します。

   .element {
     color: lch(68.29% 50 230);
   }

color() 関数

color() 関数は、色の指定に色空間とそのパラメータを直接使用します。この関数は、色空間(例えばsRGB、P3など)とその色空間のパラメータを受け取ります。また、フォールバックの色も指定できます。

   .element {
     color: color(display-p3 1 0.5 0);
   }

Separated Functional Color Notations

これまでのCSSでは、rgb(), rgba(), hsl(), hsla() などの関数で色を指定する際、引数がカンマで区切られていましたが、新しい仕様では、スペースで区切るようになりました。

また新しい仕様では、rgba() 関数が rgb() 関数のエイリアスとなり、透明度(アルファ値)を指定する場合も rgb() 関数内でスラッシュとともに指定することが可能です​​​​。

div {
  color: rgb(0 0 0);
  color: rgb(0 0 0 / 1);
  color: rgb(0 0 0 / 100%);
}

新しい色関数である lab(), lch() および color() は、このスペース区切りの構文のみをサポートしています。

CSS Nesting

Sassで感動していたネストはCSSでも可能になりました。CSS Nestingモジュールは、スタイルシートを読みやすく、よりモジュラーで、保守しやすくするために設計されています。できることはさまざまですが、コード例を載せてみます。

子孫要素のネスト

.parent {
  /* parent styles */
  .child {
    /* child of parent styles */
  }
}

複合セレクタのネスト

.a {
  .b {
    /* styles for element with class="b" which is a descendant of class="a" */
  }
  &.b {
    /* styles for element with class="a b" */
  }
}

コンテキストの反転

.foo {
  /* .foo styles */
  .bar & {
    /* .bar .foo styles */
  }
}

メディアクエリ

h1 {
  font-size: 2rem;

  @media (width >= 30rem) {
    font-size: 8r6em;
  }
}

:has()

:has() はとても機能的な擬似クラスで、引数として渡されたセレクタが少なくとも1つの要素にマッチする場合、その要素を指定するものです。この擬似クラスは、参照要素に関して親要素や前の兄弟要素を選択する方法を提供します。

次の例では、h1 要素の直後に p 要素がある場合にのみ、h1 のmargin-bottomを0に設定します。

h1:has(+ p) {
  margin-bottom: 0;
}

AまたはB、あるいは両方が存在するかどうかの書き方は次のようになります。

div:has(video, audio) {
  /* コンテンツにオーディオまたはビデオが含まれる場合に適用するスタイル */
}
div:has(video):has(audio) {
  /* コンテンツにオーディオとビデオの両方が含まれる場合に適用するスタイル */
}

他にも色々な書き方ができますが、どんな構造が入ってくるのかわからないようなコンテンツで力を発揮してくれますね。

individual transform properties

従来の transform プロパティでは、スケール、回転、移動などの変形を1つのプロパティに記述していました。ながーい transform の値を書いた経験はみなさんご存知というところもあると思います。また、変形関数の適用順序は左から右へとなり、この順序によって視覚的な結果が異なるところが考慮点としてありました。

ということで、新たに rotate、scale、translate の3つの個別変形プロパティが導入されました。

.item {
  rotate: 180deg;
  scale: 1.5;
  translate: 50% 0;
}

新しいプロパティでは適用順序が固定されているため、順序を気にする必要はなく、 translate、 rotate、 scale の順に適用されます。

CSS relative color syntax

相対的な色構文を書いて、既存の色に基づいて新しい色を定義することが可能になります。from キーワードを元の色の前で宣言し、色関数内で新しい色のチャネルを指定します。

次のように書くと、名前付きの色を操作するように。

rgb(from tomato calc(r - 20) calc(g - 20) calc(b - 20));

次のように書くと、透過度を調整します。

rgb(from tomato r g b / 50%);

@layer

CSS at-rule @layer は、カスケードレイヤーを宣言し優先順位を定義します。これにより、スタイルの重複や競合を防ぎ、CSSファイルを明示的に制御できるようになります。

優先順位の決定

宣言された順序に基づいてレイヤーの優先順位が決まります。複数のレイヤーで宣言が見つかった場合、最後にリストされたレイヤーが優先されます。特定性や出現順序は、レイヤー順序が確立されると無視されます

@layer base {
  body {
    font-family: sans-serif;
    background-color: #f5f5f5;
  }
}

@layer theme {
  body {
    color: #ffffff;
    background-color: #000000; /* ✅ */
  }
}

@layer utilities {
  .text-center {
    text-align: center;
  }
}

カスケードレイヤー内外のルール

レイヤー外で宣言されたスタイルは、すべての宣言されたレイヤーの後にある単一の匿名レイヤーに集められ、レイヤー内で宣言されたスタイルを上書きします。

body {
  background-color: #222222; /* ✅ */
}

@layer base {
  body {
    font-family: sans-serif;
    background-color: #f5f5f5;
  }

  h1 {
    font-size: 2rem;
  }
}

レイヤーへのルールの追加

レイヤー名を再宣言することで、CSSルールをレイヤーに追加できます。スタイルはレイヤーに追加され、レイヤーの順序は変わりません。

@layer theme;

@layer theme {
  .theme-dark {
    background-color: #343a40;
    color: white;
  }
}

大規模なコードベース、デザインシステム、またはサードパーティのスタイルを扱う際に、頼りになりそう。まずはResetやutilityで試して!importantを減らしてみたいです。

Media Query Range Contexts

Media Query Range Contextsは、ビューポートの幅の範囲をターゲットにする際に演算子(`<`、`>`、`=`、`<=`、`>=`)を使用します。レスポンシブなスタイルの適用を不等式のようなスタイルで、直感的に記述できるようになります。

@media (width >= 25rem) {
  .element {
    /* スタイルの適用 */
  }
}
@media (25rem <= width <= 35rem) {
  /* スタイルの適用 */
} 

@property

CSS at-rule @property は、CSS Houdini APIの一部であり、開発者がCSSカスタムプロパティを明示的に定義することを可能にします。これにより、プロパティの型チェック、制約の設定、デフォルト値の設定、カスタムプロパティが値を継承するかどうかを定義することができます

@property --property-name {
  syntax: "<color>";
  inherits: false;
  initial-value: #c0ffee;
}

カスタムプロパティにいろんな角度で制約を持たせられるのはありがたいですね。

Scroll-driven animations

スクロール駆動アニメーション(Scroll-driven animations)は、ユーザーがページをスクロールするときに発生するアニメーションです。従来はscrollイベントを間引いてあれこれしたり、ちょっと前はIntersection Observer APIで要素を検知したりしていましたが、それらはCSSのみで完結するかもしれません。新しいタイムラインが2つ定義されています。

Scroll Progress Timeline

@keyframes fadeAndSlide {
  from {
    opacity: 0;
    transform: translateY(100%);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.scroll-animation {
  animation-name: fadeAndSlide;
  animation-timeline: scroll();
}

View Progress Timeline

@keyframes animate-in-and-out {
  entry 0%  {
    opacity: 0; transform: translateY(100%);
  }
  entry 100%  {
    opacity: 1; transform: translateY(0);
  }
  exit 0% {
    opacity: 1; transform: translateY(0);
  }
  exit 100% {
    opacity: 0; transform: translateY(-100%);
  }
}

.list-view li {
  animation: linear animate-in-and-out;
  animation-timeline: view();
}

これらを使用することで、スクロール駆動アニメーションをCSSのみで実装できます。また、アニメーションの計算をメインスレッドから分離することができるため、スムーズで応答性の高いユーザー体験を提供できそうです。

@scope

@scope ルールを使用すると、そのスコープ内で定義されたCSSセレクタは、スコープ外の要素には影響を与えないようになり、スタイルの衝突を避けることができます。

対象とするサブツリーの上限を決定するスコープ・ルートを設定することで行います。スコープ・ルートが設定されると、含まれるスタイル・ルール(スコープ付きスタイル・ルールと呼ばれます)は、DOM のその限られたサブツリーからのみ選択できるようになります。

@scope (.my-component) {
  .button {
    background-color: blue; /* ✅ */
  }
  h2 {
    color: red;
  }
}

この例では、 .my-component クラスを持つ要素内でのみ、 .button クラスと h2 タグに対するスタイルが適用されます。 .my-component の外側にある .button クラスや h2 タグには、このスタイルは適用されません。 @scope 機能は、WebコンポーネントやCSSモジュールの概念と相性が良く、スタイルカプセル化の有効な手段となりますね。

三角関数 sin(), cos(), tan(), asin(), acos()

これもsassのお家芸だった三角関数が、CSSにやってきてくれましたね。円周上に要素を配置するようなレイアウトを書いてみます。

<div class="wrap" style="--count:3">
	<div class="item" style="--index: 0;"></div>
	<div class="item" style="--index: 1;"></div>
	<div class="item" style="--index: 2;"></div>
</div>
.wrap {
	position: relative;
	height: 60px;
	width: 60px;
}
.item {
	width: 0.25rem;
	height: 0.25rem;
	background-color: rgb(10 20 30 / 0.5);
	position: absolute;
	top: 50%;
	left: 50%;

	--angle: calc(360deg / var(--count) * var(--index));
	--x: calc(cos(var(--angle)) * 30px);
	--y: calc(sin(var(--angle)) * 30px);
	translate: calc(var(--x) - 50%) calc(var(--y) - 50%);
}

::marker

::marker 疑似要素は、リストアイテムのマーカーボックス(通常は箇条書きの点や番号)を選択するために使用されます。::marker は、<li> や <summary> などの display: list-item に設定された要素や疑似要素に作用します​​。次のような限られたプロパティをサポートします。

  • フォント周り

  • white-space

  • color

  • content

  • アニメーションおよびトランジションプロパティ

  • etc…

.list li::marker {
  color: red;       /* マーカーの色を赤に設定 */
  font-size: 1.5em; /* マーカーのフォントサイズを1.5emに設定 */
}

まとめ

といったように、たくさんの最近のCSSをあげてみました。知識の棚卸しや整理整頓は大事だなあと思いながら、業務に活かせそうなものも発見できてよかったです。

おわります。




▼さらにnoteの技術記事が読みたい方へ

▼noteで働いてみたいエンジニアの方へ