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

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

この投稿では、レスポンシブで、複数のデバイス入力をサポートし、ブラウザ間で動作するウェブ用の設定コンポーネントの構築に関する考えを共有したいと思います。デモをお試しください。

デモ

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

概要

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

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

レイアウト

これは、すべて CSS グリッドで作成された最初の GUI チャレンジ デモです。Chrome DevTools for grid でハイライト表示された各グリッドを以下に示します。

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

ギャップのみ

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

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

このレイアウトは、ブロック間にギャップを追加するためにのみグリッドを使用するため、「ギャップ専用」と呼んでいます。

この戦略を使用する 5 つのレイアウトをすべて表示します。

アウトラインで強調表示され、ギャップが埋められた縦型グリッド レイアウト

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

ギャップを埋めた
.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 配置の省略形を使用しているため、子要素は 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 グリッド仕様に従って、スペースが「全幅」であることは明らかですが、明確なサイズまたは最大サイズを指定する必要があります。max-size を指定しました。

では、なぜ 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 はかなり良さそうです。

先ほどの「ギャップ専用」のレイアウトを覚えていますか?このコンポーネントでの表示方法のより完全なバージョンを次に示します。

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-dark や surface-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 つのサーフェスと 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 の [Styles] パネルからプロパティの効果を示しています。このデモでは 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 でピンク色のチェックボックスが表示されているスクリーンショット

固定グラデーションと focus-within を使用したカラーポップ

色は控えめに使用すると最も引き立ちます。私が好む方法の一つは、カラフルな 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 が利用可能な場合は、カスタム プロパティに値を入力するとともに、ユーザーによる変更を監視して、カスタム プロパティを値と同期します。

Ana Tudor 氏が CSS-Tricks に投稿したこちらの記事では、トラックの塗りつぶしを 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 属性を配置します。チェックボックスでは、マウスやスクリーン リーダーなど、さまざまなツールやテクノロジーで検出できるように、name と id の両方を指定します。 <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">

上の動画は、macOS でのスクリーン リーダーのフローを示しています。入力フォーカスが 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 氏に感謝します。このバージョンでは、要素間にホバーギャップがありません。デモとソース。