2024年6月にXに投稿したCSSテクニックのまとめ

カテゴリ:
CSS
投稿日:

広告

先月からXにCSSテクニックを定期的に投稿しているので、それのまとめです。

テキストの中央寄せだからといって text-align:center を指定したほうが良いかは考えたほうがいい

ポストを別枠で表示する

和文をtext-align:centerで中央寄せすると複数行になった際に見栄えが悪くなるケースが多いです。

inline-size:fit-contentmargin-inline:autoでセンタリングすることで、1行の場合は中央寄せ、複数行の場合は左寄せといった実装が可能となります。

h1 {
inline-size: fit-content;
margin-inline: auto;
}

英文を text-align:center するなら同時に text-wrap:balance も指定しておくとよい

ポストを別枠で表示する

text-wrapはテキストの折返しを制御するプロパティで、値にbalanceを指定すると各行の文字数が均等になるように折り返されます。

h1 {
text-align: center;
text-wrap: balance;
}
text-wrap:balanceの比較

See the Pen by tak-dcxi (@tak-dcxi) on CodePen.

text-wrap:balanceは新しめのプロパティで、現在では全モダンブラウザでサポートされていますがiOSは17.5からのサポートとなります。とはいえ、非対応の環境でtext-wrap:balanceの指定が悪さをすることはないのでプログレッシブ・エンハンスメントの一環で導入しても問題ないでしょう。

また、text-wrap:balanceはバランスを取るために処理負荷がかかるため、数行(環境によってバラツキあり、Chromeでは6行以下)のテキストブロックにのみ対応しています。そのため主に見出しや短いテキストブロックに指定することになるでしょう。加えてパフォーマンスに影響を与えるため、パフォーマンスをとことん重視する場合は使用を控えることも検討したほうがよいかもしれません。

個人的には日本語の見出しは前述したinline-size:fit-contentmargin-inline:auto、英語の見出しはtext-align:centertext-wrap:balanceで実装することが多いです。

アコーディオンの「+」アイコンは grid-area で重ねて実装する

ポストを別枠で表示する

アコーディオンの「+」アイコンは疑似要素で実装するケースが多いですが、それぞれの疑似要素を重ねるためにposition:absoluteを使用するよりもgrid-areaで同じカラムに重ねるほうがスマートに実装できます。

アコーディオンの実装例

See the Pen by tak-dcxi (@tak-dcxi) on CodePen.

アイコンの実装例
summary {
display: block grid;
grid-template: '. icon' / 1fr 1em;
column-gap: 1em;
align-items: center;
&::before,
&::after {
content: '';
grid-area: icon;
border-block-end: 1px solid;
}
&::after {
rotate: 90deg;
}
&:is([open] > *)::after {
opacity: 0;
}
}

ポストではgrid-row:1 / 2となっていますが、grid-row:1で十分です。上記の実装例のコードのようにエリア名を明示したほうが可読性は高くなります。

また、強制カラーモードでもアイコンを表示できるようにborderで「+」を描いています。

テキストを中央寄せ、かつ端にアイコンのデザインのボタンは display: inline-grid で実装する

ポストを別枠で表示する

下のサンプルのようにテキストを中央寄せにして、かつ右端にアイコンのデザインのボタンの実装方法は様々ですが、個人的にはinline-gridで実装するのがスマートだと考えます。

ボタンの実装例

See the Pen by tak-dcxi (@tak-dcxi) on CodePen.

CSSの実装例
.button {
display: inline grid;
grid-template-columns: 1fr auto 1fr;
column-gap: 1em;
align-items: center;
inline-size: min(100%, 400px);
&::before {
content: ''; /* 空の疑似要素を用意する */
}
&::after {
content: '';
justify-self: end; /* 右寄せにするために必須 */
inline-size: 0.5em;
aspect-ratio: 1;
border-block-start: 1px solid;
border-inline-end: 1px solid;
rotate: 45deg;
}
}

ポイントはgrid-template-columns: 1fr auto 1frで疑似要素に1fr、ラベルテキストにautoを割り当てる点です。autoカラムはラベルテキストの内容に応じて自動的にサイズ調整され、左右の疑似要素に指定されている1frカラムが等しく余白を分け合います。

fr単位は利用可能な空間を比例配分するものなので、ラベルテキストが増えるにつれて左右の1frカラムが縮小します。特に::before疑似要素は空なので、テキストが折り返されるまでにスペースを圧迫すると完全に潰れるようになります。これによりラベルテキストが複数行になった場合は左寄せのように表示されます。

次の記事がうまくまとまっている印象です。

gridのアイテムに minmax() 関数を使用する際は min() 関数を絡めておく

ポストを別枠で表示する

grid-template-columnsminmax()関数を指定する方法はレスポンシブを容易にするため重宝されますが、minmax(360px, 1fr)のように最小値に固定値を指定するとグリッドコンテナ幅が360pxを下回ったときにはみ出してしまいます。

🙅‍♂ Not Recommended
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));

このような問題を回避するために、minmax()関数を使用する際は最小値をminmax(min(<length>, 100%), 1fr)のようにmin()関数と組み合わせて指定するクセをつけておくと良いでしょう。この方法を採用することで、グリッドコンテナ幅が固定値を下回った場合でもアイテムの横幅が100%に調整されるため、レイアウトの崩れを防ぐことができます。

🙆‍♂ Recommended
grid-template-columns: repeat(auto-fill, minmax(min(360px, 100%), 1fr));

一次元の横並びでも grid を使ったほうが適している場合もある

ポストを別枠で表示する

「一次元のレイアウトはflex、二次元のレイアウトはgridが適している」という一般的な認識がありますが、実際には一次元のレイアウトでもgridを使用したほうが効果的なケースが存在します。

例えばよく見かけるニュースリストの実装においてリストの行頭揃えを行う場合、flexでは各列に固定幅を設定しないと揃えることが困難ですが、subgrid機能を活用できるgridを使用すれば簡単に行頭揃えを実現できます。

ニュースリストの例

See the Pen by tak-dcxi (@tak-dcxi) on CodePen.

CSSの実装例
ul {
display: block grid;
grid-template-columns: max-content auto 1fr; /* 日付は改行が好ましくないので`max-content`を指定する */
}
li {
display: block grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;
column-gap: 24px;
padding-block: 32px;
border-block-end: 1px solid #dadada;
}

もしもli要素にborderpaddingなどの装飾が不要な場合は、li要素にdisplay:contentsを指定することでsubgridを使用せずとも行頭揃えが可能です。

前述したボタンやアコーディオンのアイコンの配置テクニックのように一次元の横並びでもgridを使ったほうが適しているケースは他にも存在するため、安易にflexを使用するのではなく、まずはgridの使用を検討し、Clusterレイアウトのようにflexの方が向いてそうなケースならflexを使うといった思考を持っておくと良いかもしれません。

Anchor Positioningでカードの背景をホバーで追従させる

ポストを別枠で表示する

実装例のようなカードの背景をホバーで追従させる方法、かつてはJSを使用しないと実装ができませんでしたが、最近Chromeで導入されたAnchor Positioningを使用すればCSSのみで実現可能です。

Anchor Positioningは主にツールチップやPopover APIでの位置調整に用いられるイメージがありますが、こういったホバーエフェクトにも流用可能です。

背景が追従するカードコンポーネントの例

See the Pen by tak-dcxi (@tak-dcxi) on CodePen.

.card-wrapper {
position: relative;
&::after {
content: '';
position: absolute;
inset: anchor(--card start);
z-index: -1;
inline-size: anchor-size(--card inline);
block-size: anchor-size(--card block);
background-color: var(--_background);
transition-duration: 0.3s;
transition-property: inset, inline-size, block-size;
}
}
.card {
@supports not (anchor-name: --a) {
transition: background-color var(--_duration);
&:has(:any-link:hover) {
@media (any-hover: hover) {
background-color: var(--_background);
}
}
}
&:is(:hover, :has(:focus-visible)) {
anchor-name: --card;
}
}

Anchor Positioningは現在ではChromeおよびEdgeのみのサポートですが、Anchor Positioningの指定が非対応の環境で悪さをすることはないためプログレッシブ・エンハンスメントの一環で導入しても差し支えないでしょう。実装例では@supports機能クエリを使用してAnchor Positioning非対応のブラウザでは.cardの背景色を表示させるフォールバックを行っています。

当ブログのグローバルナビゲーションおよびフッターメニューのホバーエフェクトにもAnchor Positioningが使用されています。

計算を伴う値を指定する際は答えではなく計算式を指定する

ポストを別枠で表示する

remvwのように計算を伴う値を指定する際は解答を指定するのではなく、その答えに至る計算式をそのままcalc()関数内に記述することを推奨します。値の変更が必要になった場合にわざわざ計算をし直す必要はありませんし、後からコードを見返したときにどうしてその値になったのか?が分かりやすくなります。

僕はグローバルスコープのCSS変数にそれぞれのデザインカンプのフレームの大きさとベースのフォントサイズ、それらを基準としたpx→rempx→dvi(dvw)変換用の計算式を格納しておくことが多いです。

CSSの実装例
:root {
--layout-width-min: 375; /* SPのデザインカンプの横幅 */
--layout-width-max: 1280; /* PCのデザインカンプの横幅 */
--base-font-size: 16; /* デフォルトのフォントサイズ */
/* カスタムプロパティに計算式を格納すればそれ以降の指定が簡潔になる */
--fluid-ratio-min: calc(1 / var(--layout-width-min) * 100dvi);
--fluid-ratio-max: calc(1 / var(--layout-width-max) * 100dvi);
--rem: calc(1rem / var(--base-font-size));
}
.container {
/* デザインカンプの幅が375pxで、要素の内側の余白が32px */
padding-inline: calc(32 * var(--fluid-ratio-min));
}
.container {
/* デザインカンプの幅が1280pxで、要素の内側の余白が40px */
padding-inline: calc(40 * var(--fluid-ratio-max));
}
h1 {
/* 24pxをremに変換 */
font-size: calc(24 * var(--rem));
}

このようなアプローチを採用することで記述量は増えるもののコードの意図がより明確になり、将来の修正や調整が容易になっています。ついでにSassの@functionなども不要になります。(Tailwindを使用する場合はプラグインを自作します)

ただしclamp()関数の推奨値の計算は複雑なため、メタ言語やフレームワークを使用しないでCSSを書く場合はVSCodeのユーザースペニットに次のような指定を登録して呼び出すようにしています。

ユーザースペニットの例
{
"clamp": {
"prefix": "clamp",
"body": [
"--_clamp-min: $1;", // 最小値
"--_clamp-max: $2;", // 最大値
"--_clamp: clamp(var(--_clamp-min) * var(--rem), (var(--_clamp-min) - (var(--_clamp-max) - var(--_clamp-min)) / (var(--layout-width-max) - var(--layout-width-min)) * var(--layout-width-min)) * var(--rem) + (var(--_clamp-max) - var(--_clamp-min)) / (var(--layout-width-max) - var(--layout-width-min)) * 100dvi, var(--_clamp-max) * var(--rem));",
"$3: var(--_clamp);"
]
}
}

Font-size Clamp Generatorのようなジェネレーターもありますが、値の追加や変更をする場合に一々ジェネレーターを開くのは面倒くさいのでこのようなアプローチを取っています。

意図が分かりにくい数値を指定する場合はCSS変数で数値の意味を説明する

ポストを別枠で表示する

CSSの構造上key:valueの形で自然と意味が伝わる場合は必ずしも全ての数値をCSS変数で説明する必要はありませんが、数値の意図が分かりにくい場合はローカルスコープのCSS変数を活用して数値の意味を説明するようにしています。

例えばポストのような角丸のデザインを実装する場合はアウターかインナーのどちらかのborder-radiusの値を基準として選び、そのborder-radiuspaddingをローカルスコープの変数に定義します。基準としない方の角丸はcalc()関数を使用して基準値から導出するようにします。

.card {
--_padding: 8px;
--_outer-radius: 24px;
--_inner-radius: calc(var(--_outer-radius) - var(--_padding));
padding: var(--_padding);
border-radius: var(--_outer-radius);
& > * {
border-radius: var(--_inner-radius);
}
}

フォームの入力欄のユーザー体験を向上させる field-sizing:content は導入したほうがいい

ポストを別枠で表示する

以前投稿した記事でも紹介しましたが、入力欄の大きさを内容に応じて調整することができるfield-sizing:contentは現状Google ChromeとMicrosoft Edgeのみの対応ではあるもののプログレッシブ・エンハンスメントの一環として導入する価値はあります。

CSSの実装例
textarea {
--_min-rows: 5; /* デフォルトの行数 */
--_max-rows: 20; /* 最大行数 */
--_padding: 1em;
box-sizing: border-box;
inline-size: 100%;
min-block-size: calc(var(--_min-rows) * 1lh + var(--_padding) * 2);
max-block-size: calc(var(--_max-rows) * 1lh + var(--_padding) * 2);
padding: var(--_padding);
field-sizing: content;
@supports (field-sizing: content) {
resize: none; /* field-sizing有効時にはリサイズ機能を無効にする */
}
}

ラジオボタンやチェックボックスを装飾する場合は input 要素に直接スタイルをあてる

ポストを別枠で表示する

ラジオボタンやチェックボックスの装飾を求められるケースは多くありますが、accent-colorで対応できないそれらのデザインを実装する場合、input要素を非表示にして空のspan要素やラッパーのlabel要素の疑似要素で代替する方法が一般的でした。

しかし、現代ではtype="radio"およびtype="checkbox"に限り、input要素にappearance:noneを指定することで疑似要素が使用可能な空の要素として扱えるようになり、直接スタイリングが可能になります。

[type='checkbox' i] {
display: inline flow-root; /* 幅と高さの指定を効くようにするため`inline-block`などの値を指定する */
appearance: none;
/* チェックボックスのスタイル */
&:checked::before {
/* チェックボックスがアクティブ時のスタイル */
}
}
  • has()や隣接セレクタを使用する必要がないため記述量を減らせる
  • input要素の非表示の方法によってはTab移動での選択が不可能になってしまうが、そういった事故を防げる
  • input要素のフォーカスリングが可視化される

といった理由から極力inputを隠す従来の実装は避けたほうがいいでしょう。

注意点としては縦横比が決まっている場合でもinputaspect-ratioを指定するとSafariで潰れて表示されてしまうため、縦幅と横幅は明示しておくことを推奨します。

🙅‍♂ Not Recommended
[type='checkbox' i] {
inline-size: 1lh;
aspect-ratio: 1;
}
🙆‍♂ Recommended
[type='checkbox' i] {
inline-size: 1lh;
block-size: 1lh;
}

以下は前回の記事で触れたinput[type="checkbox"]要素のswitch属性を使用したスイッチUIの実装例です。強制カラーモード対応周りなど、前回の記事の実装例より少しアップデートしています。

スイッチUIの実装例

See the Pen by tak-dcxi (@tak-dcxi) on CodePen.

スイッチUIの実装例
[switch] {
--_size-unit: 1lh;
--_background: #dadada;
--_foreground: #fcfcfc;
--_duration: 0.15s;
display: inline flex;
inline-size: calc(var(--_size-unit) * 2);
block-size: var(--_size-unit);
border: 2px solid transparent; /* 強制カラーモード対応のために`padding`ではなく`border`で余白をつくる */
border-radius: calc(1px / 0);
background-color: var(--_background);
vertical-align: middle;
appearance: unset;
transition: background-color var(--_duration);
&::before {
content: '';
flex: var(--_checked, 0);
transition: flex var(--_duration) linear;
}
&::after {
content: '';
flex-shrink: 0;
block-size: 100%;
aspect-ratio: 1;
border-radius: inherit;
background: linear-gradient(var(--_foreground) 0 0), CanvasText; /* 強制カラーモードでも視認できる背景色として扱うようにする */
}
&:checked {
--_checked: 1;
--_background: oklch(60% 0.4 240deg);
}
}

実装例ではflexで空の疑似要素を収縮させてスライドアニメーションを行っています。grid-template-columnsを使うとよりスマートに実装できますが、Safariでハンドル部分のblock-size:100%がうまく動かないのでこのようなアプローチをとっています。

見出しやリストのアイコンを中央寄せする場合に align-items:center を指定したほうが良いかは考えたほうがいい

ポストを別枠で表示する

特にリストのアイコンに対してですが、アイコンをalign-items:centerで実装して複数行になった際に不格好になっているケースが散見されます。

行に対してセンタリングしたい場合はアイコンにmargin-block: calc((1lh - <アイコンのサイズ>)) / 2)を指定すれば実現できるので、こちらの方法も検討すると良いでしょう。

また、個人的にはリストコンポーネントはspanstrongが使用されるケースに備えてflexgridは使わずにposition:absolutefloatでアイコンの位置調整をするケースが多いです。

ポストを別枠で表示する
floatを使用した実装例
li {
--_icon-size: 1em;
--_gap: 1em;
--_icon-offset: calc(var(--_icon-size) + var(--_gap));
display: block flow-root;
padding-inline-start: var(--_icon-offset);
&::before {
content: '';
float: inline-start; /* floatが指定された要素は`display:block`として扱われるので明示せずとも`margin-block`が使用できる */
clip-path: var(--shape-star);
block-size: var(--_icon-size);
aspect-ratio: 1;
margin-block: calc((1lh - var(--_icon-size)) / 2);
margin-inline-start: calc(var(--_icon-offset) * -1); /* `float`した要素は`text-indent`で動かせない */
background: linear-gradient(currentColor 0 0), CanvasText; /* 強制カラーモードでも視認できる背景色として扱うようにする */
}
}

::marker疑似要素はSafariで画像が使用できなかったり位置調整がやりにくいなどのデメリットが目立つので使用するケースは少ないです。

1行を均等割り付けしたいなら text-align-last:justify を指定する

ポストを別枠で表示する

見出しを均等割り付けする方法はかつては一文字ずつspanで囲ってjustify-content:space-betweenするという苦労が必要でしたが、現在ではtext-align-last:justifyを指定するだけで1行を均等割り付けすることが可能です。

CSSの実装例
th {
text-align-last: justify;
}

text-align-lastはブロックの最後の行もしくは強制的な改行の直前の行をどのように配置するかを設定するプロパティです。故に複数行になった場合は破綻してしまうのでその点は注意してください。

calc() 関数内でゼロ除算するとどうなるか?

ポストを別枠で表示する

算数のテストにおけるゼロ除算の解答に関して先月話題になっていましたが、CSSのcalc()関数内でゼロ除算を行うとinfinityとして扱われます。

「これはバグなんじゃないか?」とコメントを頂きましたが、これはバグではなく仕様であり、0以外の数値をゼロ除算すると標準の符号規則に従って +∞ または -∞ が生成されると仕様書には記されています。

Dividing a value by zero produces either +∞ or −∞, according to the standard sign rules.

CSS Values and Units Module Level 4より引用

ゼロ除算の有効活用としては border-radius: calc(1px / 0)border-radius: calc(infinity * 1px) と同じように扱われるため絶対に破綻しない角丸を実装できること、あとは z-index: calc(1 / 0) で上限値の2147483647と同等になるため同一レイヤー内であれば原則的に最前面に表示させることができるという活用法でしょう。

calc()関数内でinfinityを参照する方法と比較した時のメリットはゼロ除算のほうがタイプ数が少なくなる点、デメリットはCSSの仕様に精通していないとハックを疑われるという点でしょうか。

単色背景を親要素の幅を超えて画面いっぱいに表示したいなら border-image を使ったほうが良い

ポストを別枠で表示する

親要素の幅を超えて画面いっぱいに表示させる方法はmargin-inline: calc(50% - 50vi)が有名ですが、この方法は横スクロールを発生させたり、コンテンツを親要素の幅に戻す処理を行う必要があったりとデメリットも目立ちます。

単純に単色背景を親要素の幅を超えて画面いっぱいに表示させたいなら横スクロールに影響を及ぼさないborder-imageで実装するとベターでしょう。

See the Pen by tak-dcxi (@tak-dcxi) on CodePen.

CSSの実装例
section {
--_background: #1c1c1c;
border-image: linear-gradient(var(--_background) 0 0) fill 0 / /0 100lvi;
}

border-imageで単色背景を指定する場合はlinear-gradient()などのグラデーション関数を使用します。最短の記述でlinear-gradient(<color> 0 0)と指定すると単色になります。

Sassを使用している方はコメント扱いされないように連続するスラッシュの間に半角スペースを含めておきましょう。

ホバー時の opacity の指定は避けたほうがいい

ポストを別枠で表示する

ホバーしたときに背景色を透過する実装はよく見かけますが、考えなしにopacityを指定してしまうと文字含めて全てが透過してしまいます。もしもホバー時に背景色を透過することが望まれる場合はcolor-mix()関数などを使用して背景色だけ透過することを推奨します。

🙅‍♂ Not Recommended
.button {
background-color: var(--_background);
&:hover {
@media (any-hover: hover) {
opacity: 0.8;
}
}
}
🙆‍♂ Recommended
.button {
background-color: color-mix(in sRGB, var(--_background) var(--_opacity, 100%), transparent);
&:hover {
@media (any-hover: hover) {
--_opacity: 80%;
}
}
}

ただし、ホバー時に透過するという実装自体が「意識させたいはずのものを目立たなくさせる」という本末転倒感あるアプローチのため、なるべくなら代替案を考えたほうが良いかもしれません。color-mix()関数を使用したこの実装は色を暗くするor明るくするといったアプローチにも流用可能なため、そちらも考慮するとよいでしょう。

色を暗くする
.button {
background-color: color-mix(in sRGB, var(--_background), #000 var(--_darken, 0%));
&:hover {
@media (any-hover: hover) {
--_darken: 20%;
}
}
}
色を明るくする
.button {
background-color: color-mix(in sRGB, var(--_background), #fff var(--_lighten, 0%));
&:hover {
@media (any-hover: hover) {
--_lighten: 20%;
}
}
}

なお、昨日リリースされたFirefox 128より相対カラー構文が全モダンブラウザでサポートされたので、こちらの使用も検討してみても良いでしょう。

Using relative colors - CSS: Cascading Style Sheets | MDN

The CSS colors module defines relative color syntax, which allows a CSS <color> value to be defined relative to another color. This is a powerful feat...

developer.mozilla.org

Scroll-driven Animations で途中横スクロールするセクションを実装する

ポストを別枠で表示する

ギャラリーサイトでよく見かける途中で横スクロールするセクションの実装、GSAPのScrollTriggerで実装されるケースが殆どですが、現代ではScroll-driven Animationsを使用すればCSSのみで実装できます。Scroll-driven AnimationsはまたしてもChromeおよびEdgeのみのサポートですがScroll-timeline Polyfillを導入すれば非サポートのSafariやFirefoxでも動作します。

途中横スクロールするセクションの実装例

See the Pen by tak-dcxi (@tak-dcxi) on CodePen.

CSSの実装例
.horizontal {
--_scroll-amount: 500;
overflow: clip;
min-block-size: calc(var(--_scroll-amount) * 1dvb);
view-timeline-name: --horizontal-section;
}
.container {
position: sticky;
inset-block-start: 0;
inline-size: calc(var(--_scroll-amount) / 2 * 1dvmax);
min-block-size: 100dvb;
animation: linear horizontal-scroll both;
animation-timeline: --horizontal-section;
animation-range: contain 0% contain 100%;
}
@keyframes horizontal-scroll {
to {
translate: calc(-100% + 100dvi);
}
}

難点としては横スクロールする要素に固定値の幅を指定しないといけないということ。故に内容物に合わせてスクロール量を決定するのが難しいため、後述するパララックスとは違いScrollTriggerで実装したほうがベターな気がします。

Scroll-driven Animations でパララックスを実装する

ポストを別枠で表示する

こちらもScroll-driven Animationsを使用したアニメーションの紹介。パララックスに関しては非常に少ない記述量で実現できます。こちらもPolyfillに対応。

パララックスの実装例

See the Pen by tak-dcxi (@tak-dcxi) on CodePen.

CSSの実装例
.parallax {
--_translate: 200px;
animation: parallax linear both;
animation-timeline: scroll(root);
}
@keyframes parallax {
from {
translate: 0 calc(var(--_translate) * -1);
}
to {
translate: 0 var(--_translate);
}
}

記述量の少なさ、アニメーションをCSSで完結できる、JSを使用した実装と比較してコストがかなり低いなどといった理由から、ゴリゴリのアニメーションではなく部分的にパララックスを導入したい場合はPolyfillをアニメーションライブラリのように扱ってScroll-driven Animationで実装したほうがよいかもしれません。

Scroll-driven Animations と @property ルールでページのスクロール量をカウントアップする表示を実装する

ポストを別枠で表示する

当ブログの左下にスクロールインジケーターを導入しましたが、こちらもScroll-driven Animationsを使用して実装しています。また、カウントアップに関しては@propertyルールでカスタムプロパティを定義することで実現しています。

HTMLの実装例
<div class="progress">
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40"></circle>
</svg>
</div>
CSSの実装例
/* プログレスサークルの親要素 */
.progress {
display: block grid;
grid-template-areas: 'stack';
place-items: center;
inline-size: 200px;
aspect-ratio: 1;
counter-set: increment-counter var(--_increment);
font-variant-numeric: tabular-nums;
animation: increment-counter linear;
animation-timeline: scroll(root);
@supports not (animation-timeline: scroll(root)) {
display: none;
}
&::after,
& > * {
grid-area: stack;
}
&::after {
content: counter(increment-counter) '%';
}
& svg {
--_diameter: 80; /* 直径 */
--_circumference: calc(var(--_diameter) * 3.14); /* 円周 */
--_dashoffset: calc(var(--_circumference) - (var(--_increment) / 100) * var(--_circumference));
inline-size: 100%;
block-size: 100%;
fill: none;
stroke: currentColor;
stroke-dasharray: var(--_circumference);
stroke-dashoffset: var(--_dashoffset);
stroke-width: 5;
rotate: -90deg;
}
}
/* @propertyでCSS変数を定義することでテキストのカウントアップが可能に */
@property --_increment {
syntax: '<integer>';
inherits: true;
initial-value: 0;
}
@keyframes increment-counter {
to {
--_increment: 100;
}
}

@propertyルールは昨日リリースされたFirefox 128により全モダンブラウザでサポートされました(iOSは16.4よりサポート)。CSS変数に型定義ができるだけでなく、background-imageborder-imageといった本来アニメーションできなかったプロパティもアニメーションできるようになったり、@keyframes内でカスタムプロパティの定義を行えるようになったりとアニメーションの幅が広がったと言えます。

@propertyを使用したアニメーションの実装例

See the Pen by tak-dcxi (@tak-dcxi) on CodePen.

しかし、いくつかバグが存在するようで、このスクロールインジケーターにおいてもSafariだとカウントアップが動かず、Firefoxではstroke-dashoffsetのアニメーションがうまくいかないことを観測しているのでScroll-driven Animations非対応のブラウザでは@supports機能クエリで非表示にしています。


これらの投稿が良かったという方はXのフォローをよろしくおねがいします!

本文上部へ戻る

折りたたみメニュー