Add scrollbar-width and scrollbar-color utilities (#19981)

## Summary

Adds utilities for the
[`scrollbar-width`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/scrollbar-width)
and
[`scrollbar-color`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/scrollbar-color)
CSS properties.

### `scrollbar-width`

Three static utilities mirroring the spec keywords:

| Class | CSS |
| --- | --- |
| `scrollbar-auto` | `scrollbar-width: auto;` |
| `scrollbar-thin` | `scrollbar-width: thin;` |
| `scrollbar-none` | `scrollbar-width: none;` |

### `scrollbar-color`

The `scrollbar-color` property takes two colors (thumb and track). To
allow them to be set independently, two color utilities are added that
share a pair of CSS variables (`--tw-scrollbar-thumb` and
`--tw-scrollbar-track`), following the same pattern as the gradient stop
utilities (`from-*` / `via-*` / `to-*`):

| Class | CSS |
| --- | --- |
| `scrollbar-thumb-<color>` | `--tw-scrollbar-thumb: <color>;
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);` |
| `scrollbar-track-<color>` | `--tw-scrollbar-track: <color>;
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);` |

Both go through `colorUtility`, so they automatically support the
standard color palette, theme keys (`--scrollbar-thumb-color` /
`--scrollbar-track-color` with `--color` as a fallback), arbitrary
values, and the `/<alpha>` opacity modifier. The variables are
registered with `@property` (initial value `#0000`), matching the
gradient-stop convention so an unset side falls back to transparent.

```html
<div class="scrollbar-thin scrollbar-thumb-red-500 scrollbar-track-zinc-200">…</div>
```

## Test plan

- [x] `pnpm test` (all 4480 tests pass)
- [x] New `scrollbar-width`, `scrollbar-thumb`, and `scrollbar-track`
test cases in `utilities.test.ts` covering palette colors, theme-key
colors, `current` / `inherit` / `transparent`, arbitrary colors,
`/<alpha>` modifiers, and invalid candidates
- [x] `intellisense.test.ts` snapshot updated to include the new class
names


---
_Generated by [Claude
Code](https://claude.ai/code/session_014ajg5maQ4gUHKmqBs5o7vD)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Adam Wathan 2026-05-04 11:16:50 -04:00 • committed by GitHub
parent 08cad84bbe
commit b4db3b99d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 444 additions and 1 deletions

View file

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- _Experimental_: add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))
- _Experimental_: add `scrollbar-{auto,thin,none}` utilities for `scrollbar-width`, and `scrollbar-thumb-*` / `scrollbar-track-*` color utilities for `scrollbar-color` ([#19981](https://github.com/tailwindlabs/tailwindcss/pull/19981))
- Allow using `@variant` with stacked variants (e.g. `@variant hover:focus { … }`) ([#19996](https://github.com/tailwindlabs/tailwindcss/pull/19996))
- Allow using `@variant` with compound variants (e.g. `@variant hover, focus { … }`) ([#19996](https://github.com/tailwindlabs/tailwindcss/pull/19996))
- Support `--default(…)` in `--value(…)` and `--modifier(…)` for functional `@utility` definitions ([#19989](https://github.com/tailwindlabs/tailwindcss/pull/19989))

View file

@ -11013,6 +11013,141 @@ exports[`getClassList 1`] = `
"scroll-py-96",
"scroll-py-px",
"scroll-smooth",
"scrollbar-auto",
"scrollbar-none",
"scrollbar-thin",
"scrollbar-thumb-current",
"scrollbar-thumb-current/0",
"scrollbar-thumb-current/5",
"scrollbar-thumb-current/10",
"scrollbar-thumb-current/15",
"scrollbar-thumb-current/20",
"scrollbar-thumb-current/25",
"scrollbar-thumb-current/30",
"scrollbar-thumb-current/35",
"scrollbar-thumb-current/40",
"scrollbar-thumb-current/45",
"scrollbar-thumb-current/50",
"scrollbar-thumb-current/55",
"scrollbar-thumb-current/60",
"scrollbar-thumb-current/65",
"scrollbar-thumb-current/70",
"scrollbar-thumb-current/75",
"scrollbar-thumb-current/80",
"scrollbar-thumb-current/85",
"scrollbar-thumb-current/90",
"scrollbar-thumb-current/95",
"scrollbar-thumb-current/100",
"scrollbar-thumb-inherit",
"scrollbar-thumb-inherit/0",
"scrollbar-thumb-inherit/5",
"scrollbar-thumb-inherit/10",
"scrollbar-thumb-inherit/15",
"scrollbar-thumb-inherit/20",
"scrollbar-thumb-inherit/25",
"scrollbar-thumb-inherit/30",
"scrollbar-thumb-inherit/35",
"scrollbar-thumb-inherit/40",
"scrollbar-thumb-inherit/45",
"scrollbar-thumb-inherit/50",
"scrollbar-thumb-inherit/55",
"scrollbar-thumb-inherit/60",
"scrollbar-thumb-inherit/65",
"scrollbar-thumb-inherit/70",
"scrollbar-thumb-inherit/75",
"scrollbar-thumb-inherit/80",
"scrollbar-thumb-inherit/85",
"scrollbar-thumb-inherit/90",
"scrollbar-thumb-inherit/95",
"scrollbar-thumb-inherit/100",
"scrollbar-thumb-transparent",
"scrollbar-thumb-transparent/0",
"scrollbar-thumb-transparent/5",
"scrollbar-thumb-transparent/10",
"scrollbar-thumb-transparent/15",
"scrollbar-thumb-transparent/20",
"scrollbar-thumb-transparent/25",
"scrollbar-thumb-transparent/30",
"scrollbar-thumb-transparent/35",
"scrollbar-thumb-transparent/40",
"scrollbar-thumb-transparent/45",
"scrollbar-thumb-transparent/50",
"scrollbar-thumb-transparent/55",
"scrollbar-thumb-transparent/60",
"scrollbar-thumb-transparent/65",
"scrollbar-thumb-transparent/70",
"scrollbar-thumb-transparent/75",
"scrollbar-thumb-transparent/80",
"scrollbar-thumb-transparent/85",
"scrollbar-thumb-transparent/90",
"scrollbar-thumb-transparent/95",
"scrollbar-thumb-transparent/100",
"scrollbar-track-current",
"scrollbar-track-current/0",
"scrollbar-track-current/5",
"scrollbar-track-current/10",
"scrollbar-track-current/15",
"scrollbar-track-current/20",
"scrollbar-track-current/25",
"scrollbar-track-current/30",
"scrollbar-track-current/35",
"scrollbar-track-current/40",
"scrollbar-track-current/45",
"scrollbar-track-current/50",
"scrollbar-track-current/55",
"scrollbar-track-current/60",
"scrollbar-track-current/65",
"scrollbar-track-current/70",
"scrollbar-track-current/75",
"scrollbar-track-current/80",
"scrollbar-track-current/85",
"scrollbar-track-current/90",
"scrollbar-track-current/95",
"scrollbar-track-current/100",
"scrollbar-track-inherit",
"scrollbar-track-inherit/0",
"scrollbar-track-inherit/5",
"scrollbar-track-inherit/10",
"scrollbar-track-inherit/15",
"scrollbar-track-inherit/20",
"scrollbar-track-inherit/25",
"scrollbar-track-inherit/30",
"scrollbar-track-inherit/35",
"scrollbar-track-inherit/40",
"scrollbar-track-inherit/45",
"scrollbar-track-inherit/50",
"scrollbar-track-inherit/55",
"scrollbar-track-inherit/60",
"scrollbar-track-inherit/65",
"scrollbar-track-inherit/70",
"scrollbar-track-inherit/75",
"scrollbar-track-inherit/80",
"scrollbar-track-inherit/85",
"scrollbar-track-inherit/90",
"scrollbar-track-inherit/95",
"scrollbar-track-inherit/100",
"scrollbar-track-transparent",
"scrollbar-track-transparent/0",
"scrollbar-track-transparent/5",
"scrollbar-track-transparent/10",
"scrollbar-track-transparent/15",
"scrollbar-track-transparent/20",
"scrollbar-track-transparent/25",
"scrollbar-track-transparent/30",
"scrollbar-track-transparent/35",
"scrollbar-track-transparent/40",
"scrollbar-track-transparent/45",
"scrollbar-track-transparent/50",
"scrollbar-track-transparent/55",
"scrollbar-track-transparent/60",
"scrollbar-track-transparent/65",
"scrollbar-track-transparent/70",
"scrollbar-track-transparent/75",
"scrollbar-track-transparent/80",
"scrollbar-track-transparent/85",
"scrollbar-track-transparent/90",
"scrollbar-track-transparent/95",
"scrollbar-track-transparent/100",
"select-all",
"select-auto",
"select-none",

View file

@ -1 +1,2 @@
export const enableContainerSizeUtility = process.env.FEATURES_ENV !== 'stable'
export const enableScrollbarUtilities = process.env.FEATURES_ENV !== 'stable'

View file

@ -11011,6 +11011,279 @@ test('scroll-behavior', async () => {
).toEqual('')
})
test('scrollbar-width', async () => {
expect(await run(['scrollbar-auto', 'scrollbar-thin', 'scrollbar-none'])).toMatchInlineSnapshot(`
".scrollbar-auto {
scrollbar-width: auto;
}
.scrollbar-none {
scrollbar-width: none;
}
.scrollbar-thin {
scrollbar-width: thin;
}"
`)
expect(
await run([
'scrollbar',
'-scrollbar-auto',
'-scrollbar-thin',
'-scrollbar-none',
'scrollbar-auto/foo',
'scrollbar-thin/foo',
'scrollbar-none/foo',
]),
).toEqual('')
})
test('scrollbar-thumb', async () => {
expect(
await compileCss(
css`
@theme {
--color-red-500: #ef4444;
--scrollbar-thumb-color-blue-500: #3b82f6;
}
@tailwind utilities;
`,
[
'scrollbar-thumb-red-500',
'scrollbar-thumb-red-500/50',
'scrollbar-thumb-red-500/[0.5]',
'scrollbar-thumb-blue-500',
'scrollbar-thumb-current',
'scrollbar-thumb-inherit',
'scrollbar-thumb-transparent',
'scrollbar-thumb-[#0088cc]',
'scrollbar-thumb-[#0088cc]/50',
],
),
).toMatchInlineSnapshot(`
"@layer properties {
@supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
*, :before, :after, ::backdrop {
--tw-scrollbar-thumb: #0000;
--tw-scrollbar-track: #0000;
}
}
}
:root, :host {
--color-red-500: #ef4444;
--scrollbar-thumb-color-blue-500: #3b82f6;
}
.scrollbar-thumb-\\[\\#0088cc\\] {
--tw-scrollbar-thumb: #08c;
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);
}
.scrollbar-thumb-\\[\\#0088cc\\]\\/50 {
--tw-scrollbar-thumb: oklab(59.9824% -.067 -.124 / .5);
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);
}
.scrollbar-thumb-blue-500 {
--tw-scrollbar-thumb: var(--scrollbar-thumb-color-blue-500);
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);
}
.scrollbar-thumb-current {
--tw-scrollbar-thumb: currentcolor;
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);
}
.scrollbar-thumb-inherit {
--tw-scrollbar-thumb: inherit;
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);
}
.scrollbar-thumb-red-500 {
--tw-scrollbar-thumb: var(--color-red-500);
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);
}
.scrollbar-thumb-red-500\\/50 {
--tw-scrollbar-thumb: #ef444480;
}
@supports (color: color-mix(in lab, red, red)) {
.scrollbar-thumb-red-500\\/50 {
--tw-scrollbar-thumb: color-mix(in oklab, var(--color-red-500) 50%, transparent);
}
}
.scrollbar-thumb-red-500\\/50 {
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);
}
.scrollbar-thumb-red-500\\/\\[0\\.5\\] {
--tw-scrollbar-thumb: #ef444480;
}
@supports (color: color-mix(in lab, red, red)) {
.scrollbar-thumb-red-500\\/\\[0\\.5\\] {
--tw-scrollbar-thumb: color-mix(in oklab, var(--color-red-500) 50%, transparent);
}
}
.scrollbar-thumb-red-500\\/\\[0\\.5\\] {
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);
}
.scrollbar-thumb-transparent {
--tw-scrollbar-thumb: transparent;
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);
}
@property --tw-scrollbar-thumb {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-scrollbar-track {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}"
`)
expect(
await run([
'scrollbar-thumb',
'-scrollbar-thumb-red-500',
'-scrollbar-thumb-red-500/50',
'scrollbar-thumb-red-500/foo',
'scrollbar-thumb-[#0088cc]/foo',
]),
).toEqual('')
})
test('scrollbar-track', async () => {
expect(
await compileCss(
css`
@theme {
--color-red-500: #ef4444;
--scrollbar-track-color-blue-500: #3b82f6;
}
@tailwind utilities;
`,
[
'scrollbar-track-red-500',
'scrollbar-track-red-500/50',
'scrollbar-track-red-500/[0.5]',
'scrollbar-track-blue-500',
'scrollbar-track-current',
'scrollbar-track-inherit',
'scrollbar-track-transparent',
'scrollbar-track-[#0088cc]',
'scrollbar-track-[#0088cc]/50',
],
),
).toMatchInlineSnapshot(`
"@layer properties {
@supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
*, :before, :after, ::backdrop {
--tw-scrollbar-thumb: #0000;
--tw-scrollbar-track: #0000;
}
}
}
:root, :host {
--color-red-500: #ef4444;
--scrollbar-track-color-blue-500: #3b82f6;
}
.scrollbar-track-\\[\\#0088cc\\] {
--tw-scrollbar-track: #08c;
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);
}
.scrollbar-track-\\[\\#0088cc\\]\\/50 {
--tw-scrollbar-track: oklab(59.9824% -.067 -.124 / .5);
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);
}
.scrollbar-track-blue-500 {
--tw-scrollbar-track: var(--scrollbar-track-color-blue-500);
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);
}
.scrollbar-track-current {
--tw-scrollbar-track: currentcolor;
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);
}
.scrollbar-track-inherit {
--tw-scrollbar-track: inherit;
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);
}
.scrollbar-track-red-500 {
--tw-scrollbar-track: var(--color-red-500);
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);
}
.scrollbar-track-red-500\\/50 {
--tw-scrollbar-track: #ef444480;
}
@supports (color: color-mix(in lab, red, red)) {
.scrollbar-track-red-500\\/50 {
--tw-scrollbar-track: color-mix(in oklab, var(--color-red-500) 50%, transparent);
}
}
.scrollbar-track-red-500\\/50 {
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);
}
.scrollbar-track-red-500\\/\\[0\\.5\\] {
--tw-scrollbar-track: #ef444480;
}
@supports (color: color-mix(in lab, red, red)) {
.scrollbar-track-red-500\\/\\[0\\.5\\] {
--tw-scrollbar-track: color-mix(in oklab, var(--color-red-500) 50%, transparent);
}
}
.scrollbar-track-red-500\\/\\[0\\.5\\] {
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);
}
.scrollbar-track-transparent {
--tw-scrollbar-track: transparent;
scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);
}
@property --tw-scrollbar-thumb {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-scrollbar-track {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}"
`)
expect(
await run([
'scrollbar-track',
'-scrollbar-track-red-500',
'-scrollbar-track-red-500/50',
'scrollbar-track-red-500/foo',
'scrollbar-track-[#0088cc]/foo',
]),
).toEqual('')
})
test('truncate', async () => {
expect(await run(['truncate'])).toMatchInlineSnapshot(`
".truncate {

View file

@ -12,7 +12,7 @@ import {
} from './ast'
import type { Candidate, CandidateModifier, NamedUtilityValue } from './candidate'
import type { DesignSystem } from './design-system'
import { enableContainerSizeUtility } from './feature-flags'
import { enableContainerSizeUtility, enableScrollbarUtilities } from './feature-flags'
import type { Theme, ThemeKey } from './theme'
import { compareBreakpoints } from './utils/compare-breakpoints'
import { DefaultMap } from './utils/default-map'
@ -2209,6 +2209,39 @@ export function createUtilities(theme: Theme) {
staticUtility('scroll-auto', [['scroll-behavior', 'auto']])
staticUtility('scroll-smooth', [['scroll-behavior', 'smooth']])
if (enableScrollbarUtilities) {
staticUtility('scrollbar-auto', [['scrollbar-width', 'auto']])
staticUtility('scrollbar-thin', [['scrollbar-width', 'thin']])
staticUtility('scrollbar-none', [['scrollbar-width', 'none']])
{
let scrollbarColorProperties = () => {
return atRoot([
property('--tw-scrollbar-thumb', '#0000', '<color>'),
property('--tw-scrollbar-track', '#0000', '<color>'),
])
}
colorUtility('scrollbar-thumb', {
themeKeys: ['--scrollbar-thumb-color', '--color'],
handle: (value) => [
scrollbarColorProperties(),
decl('--tw-scrollbar-thumb', value),
decl('scrollbar-color', 'var(--tw-scrollbar-thumb) var(--tw-scrollbar-track)'),
],
})
colorUtility('scrollbar-track', {
themeKeys: ['--scrollbar-track-color', '--color'],
handle: (value) => [
scrollbarColorProperties(),
decl('--tw-scrollbar-track', value),
decl('scrollbar-color', 'var(--tw-scrollbar-thumb) var(--tw-scrollbar-track)'),
],
})
}
}
staticUtility('truncate', [
['overflow', 'hidden'],
['text-overflow', 'ellipsis'],