safelist accepts string literals only. Patterns are explicitly rejected — while blocklist, in adjacent code operating on the same config object, accepts glob patterns. The machinery exists on the exclusion side and was never built for the inclusion side.
The asymmetry
dist/chunk-jbn8cyfb.js:470 — safelist:
for (let G of $.safelist)
if (typeof G === "string") q.add(G)
else console.warn(`⚠️ Ignoring non-string safelist entry (patterns are not supported): ${JSON.stringify(G)}`)
dist/chunk-jbn8cyfb.js:452 — blocklist, same file, same config:
for (let U of $.blocklist)
if (U.includes("*")) Z.push(new RegExp(`^${U.replace(/\*/g, ".*")}$`))
else q.add(U)
Type surface confirms it: dist/types.d.ts:41 — safelist: string[].
Through the stx pipeline it degrades further: @stacksjs/stx/src/dev-server/crosswind.ts:705-707 does for (const cls of safelist) generator.generate(cls), and generate() string-parses its argument — so a RegExp there produces nothing at all, without even the warning above.
To be clear about what does work
Literal-string safelist entries work fine end-to-end. I verified safelist: ['z-[100]', 'max-w-[22rem]', 'flex-col-reverse', 'rotate-180'] all emit with zero matching markup, and stx explicitly merges user safelist with the base (dev-server/crosswind.ts:664-668). So this isn't "safelist is broken" — it's that safelist cannot express a family.
Why it matters
The case that needs a pattern is a colour family driven by data. Our app has status chips whose tone is computed at runtime across four colours and several shades. Written as a safelist that's ~40 literal entries covering the cross product of (bg|text|border) × (emerald|amber|red|blue) × (50|200|700|800), which then has to be maintained by hand every time a status is added.
We avoided it by moving the whole thing to data attributes plus hand-written preflight CSS — which works, but means those surfaces are no longer expressed in utilities at all, and their dark-mode variants had to be hand-written too. One pattern entry would have kept them as ordinary utility classes.
Ask
Accept string | RegExp | { pattern: RegExp, variants?: string[] }, matched against the generatable utility space rather than against scanned source:
safelist: [
{ pattern: /^(bg|text|border)-(emerald|amber|red|blue)-(50|200|700|800)$/, variants: ['dark', 'hover'] },
'rotate-180',
]
This is Tailwind's safelist: [{ pattern, variants }] shape, which is what most people arriving here will already expect — and given the warning message says "patterns are not supported", the shape was clearly anticipated.
Whatever the outcome, the stx path (dev-server/crosswind.ts:705) should at least not swallow a non-string entry more quietly than crosswind itself does.
Environment
crosswind 0.2.15, consumed through @stacksjs/stx 0.2.153, Bun 1.3.1.
safelistaccepts string literals only. Patterns are explicitly rejected — whileblocklist, in adjacent code operating on the same config object, accepts glob patterns. The machinery exists on the exclusion side and was never built for the inclusion side.The asymmetry
dist/chunk-jbn8cyfb.js:470— safelist:dist/chunk-jbn8cyfb.js:452— blocklist, same file, same config:Type surface confirms it:
dist/types.d.ts:41—safelist: string[].Through the stx pipeline it degrades further:
@stacksjs/stx/src/dev-server/crosswind.ts:705-707doesfor (const cls of safelist) generator.generate(cls), andgenerate()string-parses its argument — so aRegExpthere produces nothing at all, without even the warning above.To be clear about what does work
Literal-string safelist entries work fine end-to-end. I verified
safelist: ['z-[100]', 'max-w-[22rem]', 'flex-col-reverse', 'rotate-180']all emit with zero matching markup, and stx explicitly merges user safelist with the base (dev-server/crosswind.ts:664-668). So this isn't "safelist is broken" — it's that safelist cannot express a family.Why it matters
The case that needs a pattern is a colour family driven by data. Our app has status chips whose tone is computed at runtime across four colours and several shades. Written as a safelist that's ~40 literal entries covering the cross product of
(bg|text|border) × (emerald|amber|red|blue) × (50|200|700|800), which then has to be maintained by hand every time a status is added.We avoided it by moving the whole thing to data attributes plus hand-written preflight CSS — which works, but means those surfaces are no longer expressed in utilities at all, and their dark-mode variants had to be hand-written too. One pattern entry would have kept them as ordinary utility classes.
Ask
Accept
string | RegExp | { pattern: RegExp, variants?: string[] }, matched against the generatable utility space rather than against scanned source:This is Tailwind's
safelist: [{ pattern, variants }]shape, which is what most people arriving here will already expect — and given the warning message says "patterns are not supported", the shape was clearly anticipated.Whatever the outcome, the stx path (
dev-server/crosswind.ts:705) should at least not swallow a non-string entry more quietly than crosswind itself does.Environment
crosswind 0.2.15, consumed through @stacksjs/stx 0.2.153, Bun 1.3.1.