From ed52d3e6c9c20ca2a1c23d57d346ac399ebe9400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3zsa=20Zolt=C3=A1n?= <67325669+rozsazoltan@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:12:20 +0100 Subject: [PATCH] feat: handle backslash in `@utility` name (#19626) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #19607 ## Summary I believe there is no obstacle to simply ignoring backslashes in the name. This way, various validators - which are not aware of Tailwind CSS's specific syntax (which allows the `/` character to be used directly in utility names) - can still be bypassed using backslashes. For example, instead of `@utility push-1/2`, one could use `@utility push-1\/2`, while the end result would be identical. ## Test plan I took a previous utility test as a baseline and extended it with backslashes, and I expect the same result in the output as in the original test case: * https://github.com/rozsazoltan/tailwindcss/blob/main/packages/tailwindcss/src/utilities.test.ts#L28553-L28567 (the original test case I started from) * https://github.com/rozsazoltan/tailwindcss/blob/feat/handle-blackslash-in-utility-name/packages/tailwindcss/src/index.test.ts#L4640-L4654 (the current PR's test) --------- Co-authored-by: Robin Malfait --- CHANGELOG.md | 1 + packages/tailwindcss/src/index.test.ts | 25 +++++++++++++++++++++++++ packages/tailwindcss/src/utilities.ts | 7 ++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3feec1331..f6d9407f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure changes to external files listed via `@source` trigger a full page reload when using `@tailwindcss/vite` ([#19670](https://github.com/tailwindlabs/tailwindcss/pull/19670)) - Improve performance Oxide scanner in bigger projects ([#19632](https://github.com/tailwindlabs/tailwindcss/pull/19632)) - Ensure import aliases in Astro v5 work without crashing ([#19677](https://github.com/tailwindlabs/tailwindcss/issues/19677)) +- Allow escape characters in `@utility` names to improve support with formatters such as Biome ([#19626](https://github.com/tailwindlabs/tailwindcss/pull/19626)) ### Deprecated diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 1534f84cf..3bc3edad3 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -4729,6 +4729,31 @@ describe('@utility', () => { ) }) + test('@utility can handle escape sequences correctly', async () => { + let { build } = await compile(css` + @layer utilities { + @tailwind utilities; + } + + @utility push-1\/2 { + right: 50%; + } + + @utility push-50\% { + right: 50%; + } + `) + let compiled = build(['push-1/2', 'push-50%']) + + expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` + "@layer utilities { + .push-1\\/2, .push-50\\% { + right: 50%; + } + }" + `) + }) + test('A functional @utility must end in -*', () => { return expect( compileCss(css` diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 015821e4e..69837e0d3 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -16,6 +16,7 @@ import { enableContainerSizeUtility } from './feature-flags' import type { Theme, ThemeKey } from './theme' import { compareBreakpoints } from './utils/compare-breakpoints' import { DefaultMap } from './utils/default-map' +import { unescape } from './utils/escape' import { inferDataType, isPositiveInteger, @@ -5939,7 +5940,11 @@ export const BARE_VALUE_DATA_TYPES = [ ] export function createCssUtility(node: AtRule) { - let name = node.params + // Allow escaped characters in the name for compatibility with formatters and + // other parsers, to ensure valid CSS syntax. E.g.: `@utility foo-1\/2`. + // + // Note: the actual utility will be `foo-1/2` + let name = unescape(node.params) // Functional utilities. E.g.: `tab-size-*` if (isValidFunctionalUtilityName(name)) {