From b4db3b99d1bc6209a936df34914e0fc34b3e5095 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Mon, 4 May 2026 11:16:50 -0400 Subject: [PATCH] Add scrollbar-width and scrollbar-color utilities (#19981) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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-` | `--tw-scrollbar-thumb: ; scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track);` | | `scrollbar-track-` | `--tw-scrollbar-track: ; 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 `/` 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
…
``` ## 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, `/` 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 Co-authored-by: Robin Malfait Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- CHANGELOG.md | 1 + .../__snapshots__/intellisense.test.ts.snap | 135 +++++++++ packages/tailwindcss/src/feature-flags.ts | 1 + packages/tailwindcss/src/utilities.test.ts | 273 ++++++++++++++++++ packages/tailwindcss/src/utilities.ts | 35 ++- 5 files changed, 444 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08a0a8282..a7ba50dcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap index 3bf790540..736bac1be 100644 --- a/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap +++ b/packages/tailwindcss/src/__snapshots__/intellisense.test.ts.snap @@ -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", diff --git a/packages/tailwindcss/src/feature-flags.ts b/packages/tailwindcss/src/feature-flags.ts index 5a262056c..2441c03d8 100644 --- a/packages/tailwindcss/src/feature-flags.ts +++ b/packages/tailwindcss/src/feature-flags.ts @@ -1 +1,2 @@ export const enableContainerSizeUtility = process.env.FEATURES_ENV !== 'stable' +export const enableScrollbarUtilities = process.env.FEATURES_ENV !== 'stable' diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 7046663b9..219302ae3 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -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: ""; + inherits: false; + initial-value: #0000; + } + + @property --tw-scrollbar-track { + syntax: ""; + 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: ""; + inherits: false; + initial-value: #0000; + } + + @property --tw-scrollbar-track { + syntax: ""; + 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 { diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index d5389631a..565b64523 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -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', ''), + property('--tw-scrollbar-track', '#0000', ''), + ]) + } + + 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'],