👬

atoms で活用したい CSS 隣接セレクタ

2021/03/13に公開

CSS 隣接セレクタ(隣接兄弟結合子)を活用すると、JavaScript のみで制御するよりもスマートな atoms を作ることができます。また、JavaScript の処理を削減することが出来ます。

【本稿サンプル】https://github.com/takefumi-yoshii/atoms-example

装飾は「状態管理」に依存させない

以下は関連記事をベースに作った Component です。ref forwarding が何故必要かは、そちらの記事を参照してください。

  • label 要素に囲まれており、状態をもたない
  • input 要素を保持しているが、type は決まっていない
  • Props で「3種の形状切り替え」が可能("checkbox" | "radio" | "toggle")
import React from "react";
import styles from "./styles.module.css";
// ______________________________________________________
//
type Props = {
  shape: "checkbox" | "radio" | "toggle";
  inputProps: React.ComponentPropsWithRef<"input">;
  labelProps?: React.ComponentPropsWithRef<"label">;
};
// ______________________________________________________
//
const Input = React.forwardRef<
  HTMLInputElement,
  React.ComponentPropsWithoutRef<"input">
>((props, ref) => <input {...props} ref={ref} />);

const Label = React.forwardRef<
  HTMLLabelElement,
  React.ComponentPropsWithoutRef<"label">
>((props, ref) => <label {...props} ref={ref} />);
// ______________________________________________________
//
export const LabeledInput: React.FC<Props> = ({
  children,
  shape,
  inputProps,
  labelProps,
}) => (
  <Label {...labelProps} className={styles[shape]}>
    <Input {...inputProps} />
    <span />
    {children}
  </Label>
);

この Component の Storybook キャプチャです。「3種の形状切り替え」を可能としつつ、inputProps で全 input 要素の指定を親 Component で決定することが出来ます。

LabeledInput

装飾は同じでも「振る舞い」は親が決める

キャプチャのうち "Controlled AsyncToggle" はトグル毎に fetch を行う制御コンポーネントとなっていますが、その他は非制御コンポーネントです。CSS 擬似クラスを活用することで、親 Component は「制御・非制御」を選択することができ、汎用性の高い atoms となります。

// 非制御コンポーネント
<LabeledInput
  shape="toggle"
  inputProps={{ type: "checkbox", name: "n3", value: "0" }}
>
  Uncontrolled
</LabeledInput>

// 制御コンポーネント
<LabeledInput
  shape="toggle"
  inputProps={{
    type: "checkbox",
    name: "n3",
    value: "1",
    checked: values.checked,
    disabled: values.inProgress,
    onChange: handlers.handleChange,
  }}
>
  {values.inProgress ? "loading..." : "Controlled AsyncToggle"}
</LabeledInput>

隣接セレクタの活用

以下はshape="radio"に対応する指定です。「+」が隣接兄弟結合子で、input:checked + spanは「チェック済要素直後の span」を指しています。ほかにも「disabled」などの擬似クラスも、Component の状態として参照することが出来ます。

.radio {
  display: flex;
  position: relative;
}

.radio > input {
  width: 0;
  height: 0;
  position: absolute;
}

.radio > input:checked + span {
  background-color: #fff;
  border-color: #419ce1;
}

.radio > input:checked + span::before {
  background-color: #419ce1;
  transform: scale(1);
}

.radio > input:disabled + span {
  pointer-events: none;
  border-color: #ededed;
}

.radio > input:disabled + span::before {
  background-color: #ededed;
}

.radio > input + span {
  display: inline-block;
  width: 24px;
  height: 24px;
  margin-right: 0.5em;
  position: relative;
  border: 1px solid #ccc;
  border-radius: 24px;
  background-color: #fff;
  transition-duration: 0.2s;
  transition-property: background-color;
  cursor: pointer;
}

.radio > input + span::before {
  content: "";
  position: absolute;
  width: 14px;
  height: 14px;
  top: 5px;
  left: 5px;
  border-radius: 14px;
  background-color: #ccc;
  border-color: #ccc;
  transition-duration: 0.2s;
  transition-property: transform;
  transform: scale(0);
}

擬似要素や隣接セレクタを有効に活用してみてはいかがでしょうか。

Discussion