設定コンポーネントを作成する

スライダーとチェックボックスの設定コンポーネントを作成する方法の基本的な概要。

この記事では、レスポンシブで、複数のデバイス入力に対応し、ブラウザ間で動作するウェブ用の設定コンポーネントを構築する際の考え方を紹介します。デモをお試しください。

デモ

動画で確認したい場合や、構築中の UI/UX のプレビューを確認したい場合は、YouTube で短いチュートリアルをご覧ください。

概要

このコンポーネントの側面を次のセクションに分けました。

  1. レイアウト
  2. カスタム範囲の入力
  3. カスタム チェックボックス入力
  4. ユーザー補助に関する考慮事項
  5. JavaScript

レイアウト

これは、すべて CSS グリッドで作成された最初の GUI Challenge デモです。Chrome DevTools for grid で各グリッドをハイライト表示した例:

設定レイアウトを構成するすべてのボックスを表示するのに役立つ、カラフルなアウトラインとギャップ間隔のオーバーレイ

ギャップのみ

最も一般的なレイアウト:

foo {
  display: grid;
  gap: var(--something);
}

このレイアウトは、グリッドを使用してブロック間にギャップを追加するだけなので、「just for gap」と呼びます。

5 つのレイアウトでこの戦略が使用されています。それらをすべて表示すると次のようになります。

縦型グリッド レイアウトがアウトラインでハイライトされ、ギャップが塗りつぶされている

各入力グループ(.fieldset-item)を含む fieldset 要素は、gap: 1px を使用して要素間のヘアラインの境界線を作成しています。複雑な境界線ソリューションは不要です。

Filled gap
.grid {
  display: grid;
  gap: 1px;
  background: var(--bg-surface-1);

  & > .fieldset-item {
    background: var(--bg-surface-2);
  }
}
枠線のトリック
.grid {
  display: grid;

  & > .fieldset-item {
    background: var(--bg-surface-2);

    &:not(:last-child) {
      border-bottom: 1px solid var(--bg-surface-1);
    }
  }
}

自然なグリッドの折り返し

最も複雑なレイアウトは、マクロ レイアウト(<main><form> の間の論理レイアウト システム)でした。

ラップされたコンテンツを中央に配置する

Flexbox とグリッドはどちらも align-items または align-content の機能を提供します。要素の折り返しを扱う場合、content レイアウトの配置は、子要素のグループ間でスペースを分配します。

main {
  display: grid;
  gap: var(--space-xl);
  place-content: center;
}

メイン要素は place-content: center alignment ショートハンドを使用しているため、1 列と 2 列の両方のレイアウトで、子要素が垂直方向と水平方向の両方で中央に配置されます。

上の動画では、折り返しが発生しても「コンテンツ」が中央に配置されていることがわかります。

repeat auto-fit minmax

<form> は、各セクションにアダプティブ グリッド レイアウトを使用します。このレイアウトは、使用可能なスペースに応じて 1 列から 2 列に切り替わります。

form {
  display: grid;
  gap: var(--space-xl) var(--space-xxl);
  grid-template-columns: repeat(auto-fit, minmax(min(10ch, 100%), 35ch));
  align-items: flex-start;
  max-width: 89vw;
}

このグリッドでは、レスポンシブ レイアウトにカスタムのタッチを加えるため、row-gap(--space-xl)の値が column-gap(--space-xxl)の値と異なります。列が積み重ねられるときは、大きなギャップが必要ですが、ワイド画面の場合ほど大きくする必要はありません。

grid-template-columns プロパティは、repeat()minmax()min() の 3 つの CSS 関数を使用します。Una Kravets が、この件について優れたレイアウトに関するブログ投稿を公開しており、その中で RAM と呼んでいます。

Una のレイアウトと比較すると、このレイアウトには 3 つの特別な追加があります。

  • 追加の min() 関数を渡します。
  • align-items: flex-start を指定します。
  • max-width: 89vw スタイルがあります。

追加の min() 関数については、Evan Minto 氏のブログの投稿「Intrinsically Responsive CSS Grid with minmax() and min()」で詳しく説明されています。ぜひお読みください。flex-start の配置補正は、デフォルトのストレッチ効果を削除するためのものです。これにより、このレイアウトの子の高さが同じである必要がなくなり、自然な固有の高さにすることができます。この YouTube 動画では、この配置の追加について簡単に説明しています。

max-width: 89vw については、この投稿で簡単に説明します。スタイルを適用した場合と適用しない場合のレイアウトを次に示します。

何が起こっているのか?max-width を指定すると、auto-fit レイアウト アルゴリズムにコンテキスト、明示的なサイズ設定、または明確なサイズ設定が提供され、スペースに収まる繰り返し回数を把握できるようになります。スペースが「全幅」であることは明らかですが、CSS グリッド仕様では、明確なサイズまたは最大サイズを指定する必要があります。最大サイズを指定しました。

では、なぜ 89vw が必要なのでしょうか?レイアウトで「動作した」ためです。私と他の Chrome チームのメンバー数名で、100vw のようなより妥当な値では十分でない理由と、これが実際にバグであるかどうかを調査しています。

間隔

このレイアウトの調和の大部分は、7 つの限られたスペーシング パレットから生まれています。

:root {
  --space-xxs: .25rem;
  --space-xs:  .5rem;
  --space-sm:  1rem;
  --space-md:  1.5rem;
  --space-lg:  2rem;
  --space-xl:  3rem;
  --space-xxl: 6rem;
}

これらのフローは、グリッド、CSS @nest@media のレベル 5 構文と非常に相性が良いです。たとえば、完全に <main> レイアウト スタイルのセットは次のようになります。

main {
  display: grid;
  gap: var(--space-xl);
  place-content: center;
  padding: var(--space-sm);

  @media (width >= 540px) {
    & {
      padding: var(--space-lg);
    }
  }

  @media (width >= 800px) {
    & {
      padding: var(--space-xl);
    }
  }
}

コンテンツが中央に配置され、デフォルトで適度なパディングが適用されたグリッド(モバイルなど)。ただし、ビューポートのスペースが広くなると、パディングを増やして広がります。2021 年の CSS はかなり良い感じです。

先ほどの「just for gap」レイアウトを思い出してください。このコンポーネントでの表示方法をより詳しく見てみましょう。

header {
  display: grid;
  gap: var(--space-xxs);
}

section {
  display: grid;
  gap: var(--space-md);
}

色を効果的に使用することで、表現力がありながらもミニマルなデザインを実現しました。手順は次のとおりです。

:root {
  --surface1: lch(10 0 0);
  --surface2: lch(15 0 0);
  --surface3: lch(20 0 0);
  --surface4: lch(25 0 0);

  --text1: lch(95 0 0);
  --text2: lch(75 0 0);
}

サーフェスとテキストの色には、surface-darksurface-darker などの名前ではなく、数字で名前を付けます。これは、メディアクエリでそれらを反転させるため、明るい色と暗い色が意味をなさなくなるためです。

次のように、メディアクエリの設定で反転させます。

:root {
  ...

  @media (prefers-color-scheme: light) {
    & {
      --surface1: lch(90 0 0);
      --surface2: lch(100 0 0);
      --surface3: lch(98 0 0);
      --surface4: lch(85 0 0);

      --text1: lch(20 0 0);
      --text2: lch(40 0 0);
    }
  }
}

色の構文の詳細に入る前に、全体像と戦略を簡単に把握しておくことが重要です。少し先走りすぎたので、話を戻します。

LCH?

色彩理論に深く立ち入ることはしませんが、LCH は人間中心の構文であり、数学(255 など)で色を測定する方法ではなく、人間が色を認識する方法に対応しています。人間がより簡単に記述でき、他の人間もこれらの調整に同調できるため、明確な利点があります。

pod.link/csspodcast ウェブページのスクリーンショット。Color 2: Perception のエピソードが表示されている
CSS Podcast で知覚色(など)について学びましょう。

このデモでは、ライトモードとダークモードを切り替える構文と値に焦点を当てます。サーフェス色とテキスト色を 1 つずつ見てみましょう。

:root {
  --surface1: lch(10 0 0);
  --text1:    lch(95 0 0);

  @media (prefers-color-scheme: light) {
    & {
      --surface1: lch(90 0 0);
      --text1:    lch(40 0 0);
    }
  }
}

--surface1: lch(10 0 0) は、10% の明度、彩度 0、色相 0 に変換されます。これは、非常に暗い無色のグレーです。次に、ライトモードのメディアクエリで、明度が --surface1: lch(90 0 0); を使用して 90% に反転されます。これが戦略の要点です。まず、2 つのテーマ間で明度を変更し、デザインで求められるコントラスト比またはアクセシビリティを維持できるコントラスト比を維持します。

lch() の利点は、明度が人間中心であることです。% の変化は、知覚的にも一貫して % の違いとして認識されるため、安心感があります。たとえば、hsl()信頼性が低いです。

色空間と lch() については、詳細をご覧ください。まもなくリリースされます。

現在の CSS では、これらの色にまったくアクセスできません。繰り返しますが、最新のモニターの 3 分の 1 の色にアクセスできません。しかも、これは単なる色ではなく、画面で表示できる最も鮮やかな色です。CSS 仕様とブラウザの実装よりもモニター ハードウェアの進化が速かったため、ウェブサイトが色あせて見える。

Lea Verou

color-scheme を使用した適応型フォーム コントロール

多くのブラウザ(現在のところ Safari と Chromium)にはダークモードのコントロールが搭載されていますが、CSS または HTML でデザインがそれらを使用することを指定する必要があります。

上記は、DevTools の [スタイル] パネルのプロパティの効果を示しています。このデモでは HTML タグを使用しています。これは一般的に、より適切な場所だと考えられます。

<meta name="color-scheme" content="dark light">

詳しくは、Thomas Steiner 氏によるcolor-scheme こちらの記事をご覧ください。ダークモードのチェックボックス入力以外にも、さまざまなメリットがあります。

CSS accent-color

フォーム要素の accent-color をめぐって、最近の動きがありました。これは、ブラウザの入力要素で使用される色合いを変更できる単一の CSS スタイルです。詳しくは、GitHub のこちらをご覧ください。このコンポーネントのスタイルに含めました。ブラウザがサポートするようになると、チェックボックスがピンクと紫のアクセントカラーと調和するようになります。

input[type="checkbox"] {
  accent-color: var(--brand);
}

Linux の Chromium のピンク色のチェックボックスのスクリーンショット

グラデーションとフォーカスインが固定されたカラーポップ

色は控えめに使用したときに最も効果を発揮します。そのために私がよく使う方法の 1 つが、カラフルな UI インタラクションです。

上記の動画には、UI のフィードバックとインタラクションのレイヤが多数含まれており、次のような方法でインタラクションに個性を与えるのに役立ちます。

  • コンテキストをハイライト表示します。
  • 範囲内の値が「どの程度」満たされているかを示す UI フィードバックを提供します。
  • フィールドが入力を受け付けていることを示す UI フィードバックを提供します。

要素が操作されているときにフィードバックを提供するために、CSS は :focus-within 疑似クラスを使用してさまざまな要素の外観を変更しています。.fieldset-item を分解してみましょう。これは非常に興味深いものです。

.fieldset-item {
  ...

  &:focus-within {
    background: var(--surface2);

    & svg {
      fill: white;
    }

    & picture {
      clip-path: circle(50%);
      background: var(--brand-bg-gradient) fixed;
    }
  }
}

この要素の子のいずれかに focus-within がある場合:

  1. .fieldset-item の背景には、コントラストの高いサーフェス カラーが割り当てられます。
  2. ネストされた svg は、コントラストを高めるために白で塗りつぶされています。
  3. ネストされた <picture> clip-path が円全体に拡大し、背景が明るい固定グラデーションで塗りつぶされます。

期間を指定

次の HTML 入力要素を例に、外観をカスタマイズする方法を説明します。

<input type="range">

カスタマイズする必要があるこの要素は、次の 3 つの部分に分かれています。

  1. 範囲要素 / コンテナ
  2. 追跡
  3. つまみ

範囲要素のスタイル

input[type="range"] {
  /* style setting variables */
  --track-height: .5ex;
  --track-fill: 0%;
  --thumb-size: 3ex;
  --thumb-offset: -1.25ex;
  --thumb-highlight-size: 0px;

  appearance: none;         /* clear styles, make way for mine */
  display: block;
  inline-size: 100%;        /* fill container */
  margin: 1ex 0;            /* ensure thumb isn't colliding with sibling content */
  background: transparent;  /* bg is in the track */
  outline-offset: 5px;      /* focus styles have space */
}

CSS の最初の数行はスタイルのカスタム部分です。明確にラベル付けすることで、理解しやすくなることを願っています。残りのスタイルは、コンポーネントの複雑な部分を構築するための一貫した基盤を提供するための、リセット スタイルがほとんどです。

トラックのスタイル

input[type="range"]::-webkit-slider-runnable-track {
  appearance: none; /* clear styles, make way for mine */
  block-size: var(--track-height);
  border-radius: 5ex;
  background:
    /* hard stop gradient:
        - half transparent (where colorful fill we be)
        - half dark track fill
        - 1st background image is on top
    */
    linear-gradient(
      to right,
      transparent var(--track-fill),
      var(--surface1) 0%
    ),
    /* colorful fill effect, behind track surface fill */
    var(--brand-bg-gradient) fixed;
}

このトリックは、鮮やかな塗りつぶし色を「表示」することです。これは、上部にハードストップ グラデーションを配置して行います。グラデーションは塗りつぶし率まで透明で、それ以降は塗りつぶされていないトラックのサーフェス カラーを使用します。塗りつぶされていないサーフェスの背後には、透明度によって表示されるのを待っている全幅の色があります。

トラックの塗りつぶしスタイル

私のデザインでは、塗りつぶしスタイルを維持するために JavaScript が必要です。CSS のみの戦略もありますが、サム要素の高さがトラックと同じである必要があり、その制限内で調和を見つけることができませんでした。

/* grab sliders on page */
const sliders = document.querySelectorAll('input[type="range"]')

/* take a slider element, return a percentage string for use in CSS */
const rangeToPercent = slider => {
  const max = slider.getAttribute('max') || 10;
  const percent = slider.value / max * 100;

  return `${parseInt(percent)}%`;
};

/* on page load, set the fill amount */
sliders.forEach(slider => {
  slider.style.setProperty('--track-fill', rangeToPercent(slider));

  /* when a slider changes, update the fill prop */
  slider.addEventListener('input', e => {
    e.target.style.setProperty('--track-fill', rangeToPercent(e.target));
  })
})

視覚的なアップグレードになると思います。スライダーは JavaScript なしでも正常に動作します。--track-fill プロパティは必須ではありません。指定しない場合は、塗りつぶしスタイルが適用されません。JavaScript が利用可能な場合は、カスタム プロパティを設定すると同時に、ユーザーの変更を監視し、カスタム プロパティを値と同期します。

CSS-TricksAna Tudor 氏によるこちらの投稿では、トラックの塗りつぶしに CSS のみを使用するソリューションが紹介されています。また、range 要素も非常に参考になりました。

サムネイルのスタイル

input[type="range"]::-webkit-slider-thumb {
  appearance: none; /* clear styles, make way for mine */
  cursor: ew-resize; /* cursor style to support drag direction */
  border: 3px solid var(--surface3);
  block-size: var(--thumb-size);
  inline-size: var(--thumb-size);
  margin-top: var(--thumb-offset);
  border-radius: 50%;
  background: var(--brand-bg-gradient) fixed;
}

これらのスタイルのほとんどは、円をきれいに描画するためのものです。ここでも、サム、トラック、関連する SVG 要素の動的な色を統一する固定の背景グラデーションが表示されます。インタラクションのスタイルを分離して、ホバー ハイライトに使用されている box-shadow 手法を分離しました。

@custom-media --motionOK (prefers-reduced-motion: no-preference);

::-webkit-slider-thumb {
  

  /* shadow spread is initally 0 */
  box-shadow: 0 0 0 var(--thumb-highlight-size) var(--thumb-highlight-color);

  /* if motion is OK, transition the box-shadow change */
  @media (--motionOK) {
    & {
      transition: box-shadow .1s ease;
    }
  }

  /* on hover/active state of parent, increase size prop */
  @nest input[type="range"]:is(:hover,:active) & {
    --thumb-highlight-size: 10px;
  }
}

ユーザー フィードバックを簡単に管理できるアニメーションのハイライト表示を目標としました。ボックス シャドウを使用すると、エフェクトでレイアウトをトリガーすることを回避できます。ぼかしがなく、サム要素の円形に一致するシャドウを作成することで、これを実現します。次に、ホバー時にスプレッドのサイズを変更してトランジションします。

チェックボックスにもハイライト効果が簡単に適用できればいいのに…

クロスブラウザ セレクタ

クロスブラウザの整合性を実現するには、-webkit- セレクタと -moz- セレクタが必要であることがわかりました。

input[type="range"] {
  &::-webkit-slider-runnable-track {}
  &::-moz-range-track {}
  &::-webkit-slider-thumb {}
  &::-moz-range-thumb {}
}

カスタム チェックボックス

次の HTML 入力要素を例に、外観をカスタマイズする方法を説明します。

<input type="checkbox">

カスタマイズする必要があるこの要素は、次の 3 つの部分に分かれています。

  1. チェックボックス要素
  2. 関連付けられたラベル
  3. ハイライト効果

チェックボックス要素

input[type="checkbox"] {
  inline-size: var(--space-sm);   /* increase width */
  block-size: var(--space-sm);    /* increase height */
  outline-offset: 5px;            /* focus style enhancement */
  accent-color: var(--brand);     /* tint the input */
  position: relative;             /* prepare for an absolute pseudo element */
  transform-style: preserve-3d;   /* create a 3d z-space stacking context */
  margin: 0;
  cursor: pointer;
}

transform-style スタイルと position スタイルは、後で導入するハイライトのスタイル設定用の疑似要素の準備をします。それ以外は、私の個人的な意見に基づくスタイルに関する小さな変更がほとんどです。カーソルをポインタにしたい、アウトラインのオフセットが好き、デフォルトのチェックボックスが小さすぎる、accent-colorサポートされている場合は、これらのチェックボックスをブランドの配色に組み込んでほしい。

チェックボックスのラベル

チェックボックスにラベルを付けることは、次の 2 つの理由から重要です。1 つ目は、チェックボックスの値が何に使用されるかを表し、「何がオンまたはオフになるのか」に答えることです。2 つ目は UX です。ウェブユーザーは、関連付けられたラベルを介してチェックボックスを操作することに慣れています。

入力
<input
  type="checkbox"
  id="text-notifications"
  name="text-notifications"
>
ラベル
<label for="text-notifications">
  <h3>Text Messages</h3>
  <small>Get notified about all text messages sent to your device</small>
</label>

ラベルに、ID <label for="text-notifications"> でチェックボックスを参照する for 属性を配置します。チェックボックスでは、名前と ID の両方を 2 倍にして、マウスやスクリーン リーダーなどのさまざまなツールや技術で確実に検出できるようにします。 <input type="checkbox" id="text-notifications" name="text-notifications">:hover:active などが接続とともに追加され、フォームの操作方法が増えます。

チェックボックスのハイライト

インターフェースの一貫性を保ちたいのですが、スライダー要素にはチェックボックスで使用したいサムネイルのハイライトがあります。サムネイルでは、box-shadow とその spread プロパティを使用して、影を拡大縮小できました。ただし、この効果はここでは機能しません。チェックボックスは正方形であり、正方形である必要があります。

疑似要素と、残念ながらトリッキーな CSS を大量に使うことで、同じ視覚効果を実現できました。

@custom-media --motionOK (prefers-reduced-motion: no-preference);

input[type="checkbox"]::before {
  --thumb-scale: .01;                        /* initial scale of highlight */
  --thumb-highlight-size: var(--space-xl);

  content: "";
  inline-size: var(--thumb-highlight-size);
  block-size: var(--thumb-highlight-size);
  clip-path: circle(50%);                     /* circle shape */
  position: absolute;                         /* this is why position relative on parent */
  top: 50%;                                   /* pop and plop technique (https://web.dev/centering-in-css#5-pop-and-plop) */
  left: 50%;
  background: var(--thumb-highlight-color);
  transform-origin: center center;            /* goal is a centered scaling circle */
  transform:                                  /* order here matters!! */
    translateX(-50%)                          /* counter balances left: 50% */
    translateY(-50%)                          /* counter balances top: 50% */
    translateZ(-1px)                          /* PUTS IT BEHIND THE CHECKBOX */
    scale(var(--thumb-scale))                 /* value we toggle for animation */
  ;
  will-change: transform;

  @media (--motionOK) {                       /* transition only if motion is OK */
    & {
      transition: transform .2s ease;
    }
  }
}

/* on hover, set scale custom property to "in" state */
input[type="checkbox"]:hover::before {
  --thumb-scale: 1;
}

円形の疑似要素を作成するのは簡単でしたが、それを付加する要素の背面に配置するのは難しかったです。修正前と修正後のコードは次のとおりです。

これは間違いなくマイクロ インタラクションですが、視覚的な一貫性を保つために重要です。アニメーションのスケーリング手法は、他の場所で使用しているものと同じです。カスタム プロパティに新しい値を設定し、モーション設定に基づいて CSS でトランジションさせます。ここで重要な機能は translateZ(-1px) です。親が 3D 空間を作成し、この疑似要素の子が z 空間で少し後ろに配置することで、その空間を利用しています。

ユーザー補助

この設定コンポーネントのマウス、キーボード、スクリーン リーダーの操作については、YouTube 動画でわかりやすく説明しています。ここでは、詳細の一部を紹介します。

HTML 要素の選択

<form>
<header>
<fieldset>
<picture>
<label>
<input>

それぞれに、ユーザーのブラウジング ツールに関するヒントとアドバイスが含まれています。要素の中には、操作のヒントを提供するもの、インタラクティビティを接続するもの、スクリーン リーダーがナビゲートするアクセシビリティ ツリーの形成を支援するものがあります。

HTML 属性

スクリーン リーダーで不要な要素(この場合はスライダーの横にあるアイコン)を非表示にできます。

<picture aria-hidden="true">

上記の動画は、Mac OS でのスクリーン リーダーのフローを示しています。入力フォーカスが 1 つのスライダーから次のスライダーに直接移動していることに注目してください。これは、次のスライダーへの途中で停止する可能性のあるアイコンを非表示にしたためです。この属性がない場合、ユーザーは停止して、見えない可能性のある写真を聞いて、その写真をスキップする必要があります。

SVG は数学の集まりです。マウスホバー タイトルと、数学が何を作成しているかについての人間が読めるコメントのために、<title> 要素を追加しましょう。

<svg viewBox="0 0 24 24">
  <title>A note icon</title>
  <path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
</svg>

それ以外は、明確にマークされた HTML を十分に活用しているため、マウス、キーボード、ゲーム コントローラ、スクリーン リーダーでフォームのテストが非常にうまく機能します。

JavaScript

JavaScript からトラックの塗りつぶしの色がどのように管理されているかについてはすでに説明しましたので、ここでは <form> 関連の JavaScript を見てみましょう。

const form = document.querySelector('form');

form.addEventListener('input', event => {
  const formData = Object.fromEntries(new FormData(form));
  console.table(formData);
})

フォームが操作されて変更されるたびに、コンソールはフォームをオブジェクトとしてテーブルに記録し、サーバーに送信する前に簡単に確認できるようにします。

コンソールに表示された console.table() の結果のスクリーンショット。フォームデータが表形式で表示されている

まとめ

私がどのようにして達成したかをご理解いただけたかと思います。では、あなたならどうしますか?これにより、コンポーネント アーキテクチャが楽しくなります。お気に入りのフレームワークでスロット付きの最初のバージョンを作成するのは誰ですか?🙂

アプローチを多様化し、ウェブで構築するさまざまな方法を学びましょう。デモを作成して、ツイートでリンクを送信してください。下のコミュニティ リミックス セクションに追加します。

コミュニティ リミックス

  • チェックボックス ラベルのホバー領域に関するスタイルについて、@tomayac さんからご提案がありました。このバージョンでは、要素間にホバー ギャップはありません。デモソースをご覧ください。