---
status: accepted
---

# Plugin selectors must not reference foreign component classes

A `tailwind-plugin-<name>.ts` may only use `.btu-*` class tokens from **its own namespace** in its selectors. Referencing another component's `.btu-*` class — bare, in a combinator, or wrapped in `:where()`/`:is()` — is forbidden. To affect another component, use (a) the inner element / custom-element **tag** selector, (b) a CSS custom property the other component consumes, or (c) the component's own scoped class.

## Why

Tailwind's `@apply` resolver clones every rule whose selector contains a class token, once per `@apply <that-class>` site in the compiled CSS. So `.btu-pagination-controls > btu-icon-button > .btu-button { … }` is duplicated once per `@apply btu-button` site — ~178 in the host build — then doubled by postcss-rtl and fanned by variants → ≈ 917 KB of cross-multiplied rules for pagination's 7 selectors alone. The token's presence in the selector is the sole trigger; `:where()`/`:is()` do **not** help (verified — the walker matches the token regardless of wrapping).

## We enforce; we do not auto-fix

There is no safe library-side "magic" that neutralizes a stencil: the clone fires in the **host's** Tailwind pass (not the library's), the only internal skip-flag (`raws.tailwind.layer`) also removes the class from the apply cache and so breaks the legitimate `@apply btu-button` reuse path, and a codemod can't infer which element a foreign selector intended to target. So the rule is enforced, not corrected:

- **`no-foreign-btu-class`** grep rule in `scripts/check-conventions.mjs` (pre-commit + CI) — flags any foreign `.btu-*` token in a plugin selector. Ownership is resolved by longest-matching plugin name (`btu-button` → `button`, `btu-button-group` → `button-group`).
- **Flat-slope regression test** — compiles the preset against N = 0/10/100 `@apply btu-button` sites and fails if output is not independent of N. Catches reintroductions the grep can't see (e.g. selectors built from variables).

## Considered and rejected

`:where()`/`:is()` wrapping (token still clones), static CSS (re-multiplies unless injected *after* Tailwind — no host slot exists in the "ship raw CSS for the consumer's pipeline" model), `@scope` (Baseline Dec-2025 floor; the donut form still needs the element selector to reach the button), inverted ownership (selector still carries the token), Shadow DOM / `::part` (contradicts [ADR 0001](./0001-monolith-css-delivery.md)), patching `@apply` behavior (wrong repo; breaks the reuse API).

This rule is the gate that makes [ADR 0002](./0002-complete-component-preset.md) safe and keeps Monolith growth linear (~3–6 KB per new component). Authoring guidance: `.ai/LESSONS-PLUGIN-ISOLATION.md`; complementary "compose, don't reinvent" rule: `.ai/LESSONS-BUTTON-REUSE.md` §3.
