Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Prevent Preflight from overriding Firefox's native `iframe:focus-visible` outline styles ([#20292](https://github.com/tailwindlabs/tailwindcss/pull/20292))
- Prevent `theme('colors.foo')` in JS plugins from returning an internal disambiguation object when a CSS theme key shares a prefix with a sibling key like `--color-foo-bar` ([#20299](https://github.com/tailwindlabs/tailwindcss/pull/20299))
- Ensure fractional opacity modifiers work with named shadow sizes like `shadow-sm/12.5`, `text-shadow-sm/12.5`, `drop-shadow-sm/12.5`, and `inset-shadow-sm/12.5` ([#20302](https://github.com/tailwindlabs/tailwindcss/pull/20302))
- Fix parsing selectors like `[data-foo]div` as one selector instead of two ([#20303](https://github.com/tailwindlabs/tailwindcss/pull/20303))

## [4.3.2] - 2026-06-26

Expand Down
38 changes: 37 additions & 1 deletion packages/tailwindcss/src/selector-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ describe('parse', () => {
])
})

it('parses universal selector after an attribute selector', () => {
it('parses the universal selector after an attribute selector', () => {
expect(parse('[data-foo]*')).toEqual([
{
kind: 'compound',
Expand All @@ -457,6 +457,42 @@ describe('parse', () => {
])
})

it('parses another attribute selector after an attribute selector', () => {
expect(parse('[data-foo][data-bar]')).toEqual([
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '[data-foo]' },
{ kind: 'selector', value: '[data-bar]' },
],
},
])
})

it('parses a type selector before an attribute selector', () => {
expect(parse('div[data-foo]')).toEqual([
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: 'div' },
{ kind: 'selector', value: '[data-foo]' },
],
},
])
})

it('parses a type selector after an attribute selector', () => {
expect(parse('[data-foo]div')).toEqual([
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '[data-foo]' },
{ kind: 'selector', value: 'div' },
],
},
])
})

it('should parse selector lists as real selectors', () => {
expect(parse('.foo[attr], .bar#id, .baz + .qux')).toEqual([
{
Expand Down
74 changes: 39 additions & 35 deletions packages/tailwindcss/src/selector-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,20 +196,14 @@ export function parse(input: string) {
let currentChar = input.charCodeAt(i)

switch (currentChar) {
// E.g.:
// Handle selector lists
//
// ```css
// .foo .bar
// .foo, .bar {}
// ^
//
// .foo > .bar
// ^^^
// ```
case COMMA: {
// Flush remaining buffer, mark it as a selector
//
// Combinators are handled separately, and functions end with `)` which
// means that the `buffer` will be empty at that point.
// Flush remaining buffer as a selector
if (buffer.length > 0) {
append(selector(buffer))
buffer = ''
Expand Down Expand Up @@ -244,19 +238,30 @@ export function parse(input: string) {
break
}

// Handle combinators
//
// E.g.:
//
// ```css
// .foo .bar
// ^
//
// .foo > .bar
// ^^^
// ```
case GREATER_THAN:
case NEWLINE:
case SPACE:
case PLUS:
case TAB:
case TILDE: {
// 1. Handle everything before the combinator as a selector
// Flush remaining buffer as a selector
if (buffer.length > 0) {
append(selector(buffer))
buffer = ''
}

// 2. Look ahead and find the end of the combinator
// Look ahead and find the end of the combinator
let start = i
let end = i + 1
for (; end < input.length; end++) {
Expand Down Expand Up @@ -288,7 +293,7 @@ export function parse(input: string) {
break
}

// Start of a function call.
// Start of a function call
//
// E.g.:
//
Expand All @@ -312,7 +317,7 @@ export function parse(input: string) {
let start = i + 1
let nesting = 0

// Find the closing bracket.
// Find the closing bracket
for (let j = i + 1; j < input.length; j++) {
peekChar = input.charCodeAt(j)
if (peekChar === OPEN_PAREN) {
Expand Down Expand Up @@ -347,7 +352,7 @@ export function parse(input: string) {
break
}

// End of a function call.
// End of a function call
//
// E.g.:
//
Expand All @@ -356,7 +361,7 @@ export function parse(input: string) {
// ^
// ```
case CLOSE_PAREN: {
// Handle everything before the closing paren a selector
// Flush remaining buffer as a selector
if (buffer.length > 0) {
append(selector(buffer))
buffer = ''
Expand All @@ -376,7 +381,7 @@ export function parse(input: string) {
break
}

// Split compound selectors.
// Split compound selectors
//
// E.g.:
//
Expand All @@ -392,16 +397,16 @@ export function parse(input: string) {
break
}

// Handle everything before the combinator as a selector and
// start a new selector
// Handle everything before the combinator as a selector and start a new
// selector
if (buffer.length > 0) {
append(selector(buffer))
}
buffer = input[i]
break
}

// Start of an attribute selector.
// Start of an attribute selector
//
// NOTE: Right now we don't care about the individual parts of the
// attribute selector, we just want to find the matching closing bracket.
Expand All @@ -410,16 +415,16 @@ export function parse(input: string) {
// future, then we can use the `AttributeSelectorParser` here (and even
// inline it if needed)
case OPEN_BRACKET: {
// Handle everything before the combinator as a selector
// Flush remaining buffer as a selector
if (buffer.length > 0) {
append(selector(buffer))
buffer = ''
}
buffer = ''

let start = i
let nesting = 0

// Find the closing bracket.
// Find the closing bracket
for (let j = i + 1; j < input.length; j++) {
peekChar = input.charCodeAt(j)
if (peekChar === OPEN_BRACKET) {
Expand All @@ -435,12 +440,11 @@ export function parse(input: string) {
}
}

// Adjust `buffer` to include the string.
buffer += input.slice(start, i + 1)
append(selector(input.slice(start, i + 1)))
break
}

// Start of a string.
// Start of a string
case SINGLE_QUOTE:
case DOUBLE_QUOTE: {
let start = i
Expand All @@ -452,43 +456,43 @@ export function parse(input: string) {
//
// ```css
// "This is a string with a 'quote' in it"
// ^ ^ -> These are not the end of the string.
// ^ ^ -> These are not the end of the string
// ```
for (let j = i + 1; j < input.length; j++) {
peekChar = input.charCodeAt(j)
// Current character is a `\` therefore the next character is escaped.
// Current character is a `\` therefore the next character is escaped
if (peekChar === BACKSLASH) {
j += 1
}

// End of the string.
// End of the string
else if (peekChar === currentChar) {
i = j
break
}
}

// Adjust `buffer` to include the string.
// Adjust `buffer` to include the string
buffer += input.slice(start, i + 1)
break
}

// Nesting `&` is always a new selector.
// Universal `*` is always a new selector.
// Nesting `&` is always a new selector
// Universal `*` is always a new selector
case AMPERSAND:
case ASTERISK: {
// 1. Handle everything before the combinator as a selector
// Flush remaining buffer as a selector
if (buffer.length > 0) {
append(selector(buffer))
buffer = ''
}

// 2. Handle the `&` or `*` as a selector on its own
// Handle the `&` or `*` as a selector on its own
append(selector(input[i]))
break
}

// Escaped characters.
// Escaped characters
case BACKSLASH: {
buffer += input[i] + input[i + 1]
i += 1
Expand All @@ -502,7 +506,7 @@ export function parse(input: string) {
}
}

// Collect the remainder as a word
// Collect the remainder as a selector
if (buffer.length > 0) {
append(selector(buffer))
}
Expand Down