Skip to content

fix(v4): output static components inside @layer components (issue #15045)#20131

Closed
saitejabandaru-in wants to merge 4 commits into
tailwindlabs:mainfrom
saitejabandaru-in:feature-fix-plugin-components
Closed

fix(v4): output static components inside @layer components (issue #15045)#20131
saitejabandaru-in wants to merge 4 commits into
tailwindlabs:mainfrom
saitejabandaru-in:feature-fix-plugin-components

Conversation

@saitejabandaru-in
Copy link
Copy Markdown

Description

This pull request resolves issue #15045 by correctly placing styles registered via the JavaScript plugin compatibility API addComponents inside the static @layer components cascade layer rather than @layer utilities in Tailwind CSS v4.

Background

In Tailwind CSS v3, plugins using addComponents (such as daisyui or @tailwindcss/forms) registered custom components whose styles were output inside @tailwind components (predefined @layer components). This allowed developers to cleanly override component-level styles with utility classes (which are ordered later in @layer utilities) in their HTML.

In v4, the compatibility layer defined addComponents as a simple alias for addUtilities:

    addComponents(components, options) {
      this.addUtilities(components, options)
    },

This causes component styles to be generated inside the dynamic @layer utilities layer. Because they share the same cascade layer as standard utility classes, they are ordered based on property counts and property-overlap heuristics rather than cascade layering. This breaks specificity, making it extremely difficult or impossible to override component classes (like .btn, .card) with utility classes (like bg-red-100) in markup without resorting to !important everywhere.

Changes

  1. Static Component Compilation: Updated the addComponents implementation in plugin-api.ts to parse component styles using objectToAst and wrap them inside an explicit @layer components at-rule, pushing it directly to the root ast array (similar to how addBase works for global baseline styles):
    addComponents(components, options) {
      if (referenceMode) return
      let componentNodes = objectToAst(components)
      
      // Prefix all class selectors with the configured theme prefix
      if (designSystem.theme.prefix) {
        walk(componentNodes, (node) => {
          if (node.kind === 'rule') {
            let selectorAst = SelectorParser.parse(node.selector)
            walk(selectorAst, (node) => {
              if (node.kind === 'selector' && node.value[0] === '.') {
                node.value = `.${designSystem.theme.prefix}\\\\:${node.value.slice(1)}`
              }
            })
            node.selector = SelectorParser.toCss(selectorAst)
          }
        })
      }
    
      featuresRef.current |= substituteFunctions(componentNodes, designSystem)
      let rule = atRule('@layer', 'components', componentNodes)
      walk([rule], (node) => {
        node.src = src
      })
      ast.push(rule)
    }
  2. Prefix Support: Properly applied configured theme prefixing to class selectors inside component rules, ensuring backward compatibility with prefix-based configurations.
  3. Tests: Updated plugin-api.test.ts to verify that addComponents outputs compiled rules cleanly wrapped inside @layer components in the generated CSS, rather than placing them straight into the flat utilities listing.

This is a critical backward compatibility fix that enables a seamless transition to Tailwind CSS v4 for standard component libraries (such as daisyUI, flowbite, tailwindcss-forms, etc.) that rely on cascade layering specificity.

…abs#15045)

Aligns v4 compatibility layer behavior with v3 by placing styles registered via `addComponents` inside the static `@layer components` layer rather than `@layer utilities`. This correctly preserves layer specificity so that component libraries like daisyUI can be overridden by standard utility classes.
…nents

Updates the snapshot assertion to verify that component styles are properly wrapped inside `@layer components`.
@saitejabandaru-in saitejabandaru-in requested a review from a team as a code owner May 29, 2026 17:44
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

This PR changes the addComponents() method in the Tailwind CSS plugin API to emit component styles wrapped in an @layer components at-rule instead of delegating to addUtilities. The implementation applies theme-prefix selector rewriting and function substitution to the component nodes before wrapping them in the layer. Test coverage was updated to verify the new behavior, with snapshot expectations showing the explicit @layer components structure around emitted component selectors and their declarations.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: fixing component output to use @layer components instead of utilities, and directly references issue #15045.
Description check ✅ Passed The description comprehensively explains the issue, background, and implementation details, directly relating to the changeset's purpose of fixing component layering.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 29, 2026

Confidence Score: 2/5

Not safe to merge — the matchComponents rewrite produces invalid CSS and breaks an existing test.

The addComponents fix is correct and achieves the stated goal. However, the matchComponents rewrite returns a @layer at-rule as children of the outer style rule, which is not valid CSS and silently does nothing in browsers. Any plugin using matchComponents would see its components output in the wrong layer. The corresponding snapshot test was also not updated, so CI will fail.

packages/tailwindcss/src/compat/plugin-api.ts — the matchComponents compileFn wrapping logic; packages/tailwindcss/src/compat/plugin-api.test.ts — the matchComponents snapshot needs to be updated alongside a correct implementation.

Reviews (2): Last reviewed commit: "Update addComponents test to expect styl..." | Re-trigger Greptile

…abs#15045)

Aligns v4 compatibility layer behavior with v3 by placing styles registered via `addComponents` inside the static `@layer components` layer rather than `@layer utilities`. This correctly preserves layer specificity so that component libraries like daisyUI can be overridden by standard utility classes.
…nents

Updates the snapshot assertion to verify that component styles are properly wrapped inside `@layer components`.
@RobinMalfait
Copy link
Copy Markdown
Member

Hey! Appreciate the PR, but we're not sure yet if we even want this. While this is a breaking change, it was an intentional breaking change to simplify the system.

The PR currently is incomplete, but since we're not sure if we even want this, I'm going to close this for now. Thanks!

Comment on lines +620 to +627
if (!candidate.modifier) {
modifier = null
} else if (modifiers === 'any' || candidate.modifier.kind === 'arbitrary') {
modifier = candidate.modifier.value
} else if (modifiers && Object.hasOwn(modifiers, candidate.modifier.value)) {
modifier = modifiers[candidate.modifier.value]
} else if (isColor && !Number.isNaN(Number(candidate.modifier.value))) {
modifier = `${candidate.modifier.value}%`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 @layer block nested inside selector rule — invalid CSS output

The compileFn here returns [rule] where rule = atRule('@layer', 'components', ast). In the compilation pipeline (compileAstNodes in compile.ts), the array returned by compileFn becomes the nodes of an outer StyleRule: { kind: 'rule', selector: '.prose', nodes: [atRule('@layer', 'components', …)] }. The serialised CSS is therefore .prose { @layer components { --container-size: normal; } } — an @layer at-rule nested inside a style rule, which is invalid CSS. Browsers silently ignore it, so declarations end up in the wrong cascade layer. The fix for addComponents (pushing a root-level @layer components block directly to ast) cannot be reused here because functional components are generated on demand per candidate, but wrapping the inner compileFn return value is the wrong place to apply it.

Comment on lines 4221 to 4258
@@ -4242,13 +4244,15 @@ describe('matchComponents()', () => {
),
).toMatchInlineSnapshot(`
"
.prose {
--container-size: normal;
}
@layer components {
.prose {
--container-size: normal;
}

@media (hover: hover) {
.hover\\:prose-lg:hover {
--container-size: lg;
@media (hover: hover) {
.hover\\:prose-lg:hover {
--container-size: lg;
}
}
}
"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 matchComponents test snapshot not updated — will fail

The matchComponents implementation was rewritten to return [atRule('@layer', 'components', ast)] from each compileFn, so the generated CSS for every candidate is now wrapped in @layer components. The existing snapshot still reflects the original output (no @layer wrapping), so the test suite will fail as-is. The snapshot and test description ('is an alias for matchUtilities') both need to be updated to match the intended new output once the underlying implementation bug is resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants