Tailwind CSS 4において意図せずoutline-colorがtransitionしてしまう問題の対処法

Tailwind CSS 4では、transitionユーティリティおよびtransition-colorsユーティリティのtransition-propertyプロパティにoutline-colorプロパティが含まれるように変更された(参考: Upgrade guide - Getting started - Tailwind CSS)。

変更前(Tailwind CSS 3):

.transition {
  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 150ms;
}

.transition-colors {
  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 150ms;
}

変更後(Tailwind CSS 4):

.transition {
  transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events;
  transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
  transition-duration: var(--tw-duration, var(--default-transition-duration));
}

.transition-colors {
  transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
  transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
  transition-duration: var(--tw-duration, var(--default-transition-duration));
}

これにともなって、要素をフォーカスしたときのインジケータの色が一瞬currentcolorになってしまう問題が生じることがある。

Chromeのユーザーエージェントスタイルシートでは、フォーカス時のoutline-colorプロパティの値として-webkit-focus-ring-colorが指定されている。

:focus-visible {
  outline: auto 1px -webkit-focus-ring-color
}
third_party/blink/renderer/core/html/resources/html.css - chromium/src.git - Git at Google

通常は即座にこの色に変化するが、transitionの対象になっていると、currentcolorを開始点として-webkit-focus-ring-colorに遷移するという挙動になる。これによって、一瞬黒く見えてしまう。

このような挙動になるのはChromeのみで、SafariやFirefoxではtransitionが有効にならない。とはいえ、このような挙動は好ましくないため、何かしらの方法で修正する必要がある。

Tailwind CSSのUpgrade Guideでは、そのための回避策として、条件にかかわらずoutline-colorの値が変化しないように指定することが提案されている。

-<button class="transition hover:outline-2 hover:outline-cyan-500"></button>
+<button class="outline-cyan-500 transition hover:outline-2"></button>
Upgrade guide - Getting started - Tailwind CSS

しかし、フォーカス可能なすべての要素に対してこのような指定をすることは現実的ではない。このような個別の指定が必要になると、必然的に間違いが起こるだろう。そのため、代わりとなる別のアプローチをいくつか挙げて検討してみる。

1. transition-propertyのデフォルト値を上書きする

最初に考えられる方法としては、transition-propertyプロパティの値からoutline-colorプロパティを取り除くことだ。そのために、それぞれのユーティリティの実装を調べてみる。

まず、transitionユーティリティは次のように実装されている。

functionalUtility('transition', {
  defaultValue:
    'color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events',
  themeKeys: ['--transition-property'],
  handle: (value) => [
    decl('transition-property', value),
    decl('transition-timing-function', defaultTimingFunction),
    decl('transition-duration', defaultDuration),
  ],
})
tailwindcss/packages/tailwindcss/src/utilities.ts at v4.1.11 · tailwindlabs/tailwindcss

もう一方の、transition-colorsユーティリティは次のように実装されている。

staticUtility('transition-colors', [
  [
    'transition-property',
    'color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to',
  ],
  ['transition-timing-function', defaultTimingFunction],
  ['transition-duration', defaultDuration],
])
tailwindcss/packages/tailwindcss/src/utilities.ts at v4.1.11 · tailwindlabs/tailwindcss

これらを上書きするには、@utilityディレクティブを使ってCSSを記述する。ここで指定されている値からoutline-colorプロパティを取り除いて指定する。

@utility transition {
  transition-property:
    color, background-color, border-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events;
}

@utility transition-colors {
  transition-property:
    color, background-color, border-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
}

ただし、これらユーティリティのプロパティ値は予期せず変更される可能性があるため、Tailwind CSSのアップデートにともなう変更を監視して追従できるようにしておく必要がある。

2. transitionユーティリティの対象プロパティを個別に指定する

transitionユーティリティ自体の実装に手を入れずとも、arbitrary valuesとしてユーティリティの値を調整することはできる。次のようにして対象のプロパティだけを個別に指定すれば、意図しないプロパティのtransitionが有効になってしまうことを回避できる。

<button class="bg-blue-500 transition-[background-color] hover:bg-indigo-500"><!-- ... --></button>

3. フォーカス可能な要素すべてのoutline-colorプロパティにデフォルト値を指定する

先ほどの説明の繰り返しになるが、outline-colorプロパティの値が変化しないように指定すれば意図しないtransitionを回避できる。

-<button class="transition hover:outline-2 hover:outline-cyan-500"></button>
+<button class="outline-cyan-500 transition hover:outline-2"></button>
Upgrade guide - Getting started - Tailwind CSS

この例はカスタムのoutline-colorプロパティを適用するものだが、ブラウザデフォルトのフォーカスリングを適用したい場合には次のようにする。

<button class="outline-[-webkit-focus-ring-color] transition"><!-- ... --></button>

これによって、要素がフォーカスされてフォーカスリングが表示される際のtransitionを無効化できる。

Firefoxではフォーカス時のoutline-colorプロパティの値はcurrentcolorになっているが、このように-webkit-focus-ring-colorを指定した場合はパースエラーとして扱われて無視される。

4. すべての要素のoutline-colorプロパティのデフォルト値を上書きする

outline-colorプロパティを個別に設定するアプローチでは、やはり作業の抜け漏れが生じる可能性がある。それよりも、あらかじめすべての要素のプロパティ値を指定してしまうのが良いだろう。次のようにして実装する。

@layer base {
  *,
  ::after,
  ::before,
  ::backdrop,
  ::file-selector-button {
    outline-color: -webkit-focus-ring-color;
  }
}