Gemcook Tech Blog

【anchor-positioning】さよならJS。いい感じのTabコンポーネントをさくっと実装しよう。

2024/12/10に公開

こんにちは!
今回は、以下のような<Tab/>コンポーネントの実装をしていきたいと思います。

このコンポーネントの要点をまとめると、

  • 選択されているアクティブなTabがハイライト(何らかの形で目立つように)されている。
  • 他のTabをClickするなどしてアクティブにすると、ハイライト部分が新たにActiveになったTabまでアニメーションしながら動いていく。

というような、よくあるTabのUIです。...さて、こんなUIを実装してみたことがある方はわかるかと思いますがこの実装地味に面倒です。特に、Tab内部のコンテンツが可変な共通のコンポーネントを実装しようとすると、より面倒臭さが増していきます...。各所で計算が発生するJSでのもりもりな実装方法がまず思いつくんじゃないでしょうか?

今回は、タイトルにもある通り anchor-positioning を使って、その計算もりもりのJSをなくした実装方法を紹介したいと思います👏

そもそもanchor-positioningってなんだ?という方はこちら。

そもそもanchor-positioningってなんだ?というかたはこの記事を参考にしてみてください。きれいにビジュアライズされており、理解しやすいと思います!!
素敵な記事をありがとうございます...🙇

https://zenn.dev/d_kawaguchi/articles/css-anchor-positioning-294aa71a7f77fc

まずは結論

後ほど本質的ではないコード(不要なstyle等)を省いたものを紹介しますが、まずは細かなstyleも含めたコードを雑に貼りたいと思います。(なお、emotionが好きなのでemotionで書いてます。)

import { css } from "@emotion/react";

type Props = {
  tabs: string[];
  activeIndex: number;
  onChange: (activeIndex: number) => void;
};

export const Tab: React.FC<Props> = ({ tabs, activeIndex, onChange }) => {
  return (
    <div css={container}>
      <div css={activeHighlight} />
      <div css={tabsItemsWrapper}>
        {tabs.map((tab, index) => {
          const isActive = activeIndex === index;

          return (
            <button
              onClick={() => onChange(index)}
              css={tabItem(isActive)}
              type="button"
            >
              {tab}
            </button>
          );
        })}
      </div>
    </div>
  );
};

const ANCHOR_NAME = "--awesome" satisfies `--${string}`;

const container = css({
  width: "fit-content",
  padding: 2,
  borderRadius: 4,
  backgroundColor: "#f6f6f6",
  color: "#000000",
  position: "relative",
  anchorScope: ANCHOR_NAME,
});

const activeHighlight = css({
  backgroundColor: "white",
  borderRadius: 2,
  transition: "all .2s",
  positionAnchor: ANCHOR_NAME,
  width: `anchor-size(${ANCHOR_NAME} width)`,
  height: `anchor-size(${ANCHOR_NAME} height)`,
  position: "absolute",
  top: "anchor(top)",
  left: "anchor(left)",
});

const tabsItemsWrapper = css({
  display: "flex",
  gap: 4,
});

const tabItem = (isActive: boolean) =>
  css({
    border: "unset",
    backgroundColor: "unset",
    alignContent: "center",
    justifyContent: "center",
    borderRadius: 2,
    padding: 8,
    ...(isActive && {
      anchorName: ANCHOR_NAME,
    }),
  });

JSによる細かな計算....ないですよね!スッキリですよね!最高です!!

細かく実装を見ていく

実装していく<Tab/>は下記画像のような、大きく分けて以下の3つのセクションに分かれた構造になっています。各タブを押下すると、その下をハイライト部分が滑るように移動してくる。というわけですね。

  1. tabItem(各button部分)
  2. activeHighlight(アクティブなTabを示すハイライト部分)
  3. container (tabItem、activeHighlightを囲うラッパー部分)

先ほどのコードをこの3つのセクションに分解して解説していきたいと思います。(ここからは、anchor-positioningに直接関連のないstylingについては省いたコードで説明していきたいと思います。)

0. ANCHOR_NAME

先ほど3つのセクションに分ける。と言いましたがそれは嘘です。3つのセクションを説明する前に、すべてのセクションにて使用されている、ANCHOR_NAMEについて説明します。

const ANCHOR_NAME = "--awesome" satisfies `--${string}`;

anchor-positioningは、anchor-name:プロパティを付与された要素を目印にして、いろいろなstylingを行えることにメリットがあります。そのためanchor-name:に渡すための文字列であるANCHOR_NAMEは至る所で使用されることになります。定数として切り出し今後これを参照するようにします。

1. tabItem

まずは、Tabを切り替えるための各button部分の実装を見ていきます。anchor-positioningにおいて重要なのは以下のstyleのみです。

const tabItem = (isActive: boolean) =>
  css({
    ..., // なんやかんや
    ...(isActive && {
      anchorName: ANCHOR_NAME,
    }),
  });

このコードは、ActiveなTabにのみ anchor-name:を設定し、他の要素から参照できるように しています。これにより後述のセクションたちで「ActiveなTabの xxx」が参照できるようになるわけです。

2. activeHighlight

次は、ActiveなTabが切り替えられた時「どのTabがActiveなのか」を示すためのハイライト部分を見ていきます。筆者はanchor-positioningを利用することによって楽になったことNo.1がこの要素のstylingだと感じています。

const activeHighlight = css({
  ..., // なんやかんや
  width: `anchor-size(${ANCHOR_NAME} width)`,
  height: `anchor-size(${ANCHOR_NAME} height)`,
  positionAnchor: ANCHOR_NAME,
  position: "absolute",
  top: "anchor(top)",
  left: "anchor(left)",
});

このコードの役割は大きく分けて2つです。

  • アンカーに指定されている要素のサイズを参照して、activeHighlightのサイズを決める。
  • アンカーに指定されている要素の位置を基準にして、activeHighlightの位置を決める。

です。それぞれ何をしているのか詳細にみていきましょう。

activeHighlightのサイズを決める。

width: `anchor-size(${ANCHOR_NAME} width)`,
height: `anchor-size(${ANCHOR_NAME} height)`,

anchor-size()関数を使用して、アンカー要素のサイズを参照することができます。
anchor-size()関数の第一引数に「参照先のアンカーのanchor-name:」を指定し、第二引数に「そのアンカーの何を参照したいのか」を指定します。

ここでは、widthheightを指定してそれぞれの値を取ってきています。これで、Tabの横幅がコンテンツ次第で可変だったとしても、JSやいろんな計算を行う必要なく期待通り指定できるわけです。yatta🤩

https://developer.mozilla.org/en-US/docs/Web/CSS/anchor-size

activeHighlightの位置を決める。

positionAnchor: ANCHOR_NAME,
position: "absolute",
top: "anchor(top)",
left: "anchor(left)",

さて、サイズだけではなく、もちろん位置もコンテンツ次第で可変になってしまいます。位置に関しては、anchor()関数とposition-anchor:プロパティを用いることで、アンカー要素の位置を参照することができます。

  1. position-anchor:プロパティで、「どのアンカー要素を参照するか?」を指定する。
  2. anchor()関数で、「そのアンカー要素のどの位置を参照するか?」を決定する。

上記の2ステップによって、アンカー要素の位置を参照することで、activeHighLightを次に解説するcontainerに対して絶対配置しています。

https://developer.mozilla.org/en-US/docs/Web/CSS/position-anchor
https://developer.mozilla.org/en-US/docs/Web/CSS/anchor

3. container

次は、各Tabをラップしている要素を見ていきます。ここはとてもシンプルです。

const container = css({
  ..., // なんやかんや
  position: "relative",
  anchorScope: ANCHOR_NAME,
});

ここでは2つのことを行なっています。

  • 先ほどのactiveHighlightが何に対して絶対配置されるのかを決めるためにposition: "relative"を指定している。
  • anchor-scope:を指定し、指定されたANCHOR_NAMEを参照できるスコープを決定している。

anchor-scope:を指定することで、対象のアンカー要素を参照できるスコープをanchor-scope:を指定した要素のサブツリー内に制限しています。

https://developer.chrome.com/blog/chrome-131-beta?hl=ja#css_anchor_positioning_anchor-scope

まとめ

参照したい要素にanchor-name:を付与してアンカー要素とし、そのアンカー要素を参照できるスコープをanchor-scope:設定し、そのスコープ内でanchor()や、anchor-size()を使っていい感じにstyleを参照する。

たったこれだけでぇ〜〜〜〜〜〜〜〜〜っ🐟🍝
いままで面倒だったUIがさくっと実装できてしまいました!!
anchor-positioningは他にも色々な便利機能が盛り沢山なので、うまく利用すればいろんなUIがいままでと比べものにならないほど簡単に実装できそうな、そんな予感がしております。
今回、anchor-name:のつけ外し部分はJSを残して実装しましたが、CSSのみで完結させることももちろん可能です!JSを撲滅したくて仕方ない方はぜひやってみてください!!!

...いいね。anchor-positioning⚓️

Gemcook Tech Blog
Gemcook Tech Blog

Discussion