Skip to content

[CDX-430] Scope all Tailwind classes to work on the library's components only#49

Open
TarekAlQaddy wants to merge 9 commits into
mainfrom
cdx-430-shared-components-scope-all-tailwind-css-classes-to-work-on
Open

[CDX-430] Scope all Tailwind classes to work on the library's components only#49
TarekAlQaddy wants to merge 9 commits into
mainfrom
cdx-430-shared-components-scope-all-tailwind-css-classes-to-work-on

Conversation

@TarekAlQaddy
Copy link
Copy Markdown
Contributor

@TarekAlQaddy TarekAlQaddy commented May 27, 2026

Goal

Prevent style conflicts when consumers use this UI library alongside their own Tailwind setup.

Approaches Explored

  1. scope all the classes inside a container class or via CSS @scope Rule

    • How it works: Native CSS scoping that limits styles to a container element
    • Limitations:
      • Cannot wrap @import statements, making it incompatible with Tailwind's import-based architecture
      • There isn't a root container that wraps the individual components
  2. Scope Utilities to a Container via @Utility

    • How it works: Use @Utility * { @apply .cio-components &; } to make all utilities only apply inside a wrapper element
    • Limitations:
      • Not valid Tailwind v4 syntax, there's no mechanism to globally intercept and modify all utility definitions this way
      • The @apply directive is for applying existing utilities, not for creating scoped selectors
  3. important() Scoping with Wrapper Element

    • How it works: Wrap all components with a .cio-components class and use Tailwind's important() function to scope styles to descendants of that class
    • Limitations:
      • The important() modifier doesn't work correctly for elements that have both the scope class AND utility classes on the same element
      • Generated invalid CSS: @media important(:is(.cio-components, .cio-components *)) which browsers don't recognize
  4. Tailwind v4 prefix() ✅

    • How it works: Use prefix(cio) on Tailwind imports to namespace all utility classes with cio: and CSS variables with --cio-
    • Limitations:
      • Requires updating all existing className values in components and stories
      • Contributors must remember to use the prefix (documented in README)
    • Why chosen:
      • 100% browser support (just CSS class names)
      • First-party Tailwind v4 feature with predictable behavior
      • Complete isolation, no style conflicts in either direction
      • Works with all Tailwind features (variants, arbitrary values, etc.)

Testing

  • I visually reviewed the Storybook examples for all components to ensure there were no changes and that the new classes are working correctly.
  • I ran npm run compile and verified that the generated style.css contains the correct class names.

@TarekAlQaddy TarekAlQaddy requested review from a team and Copilot May 27, 2026 01:24
@TarekAlQaddy TarekAlQaddy requested a review from a team as a code owner May 27, 2026 01:24
constructor-claude-bedrock[bot]

This comment was marked as outdated.

This comment was marked as outdated.

@TarekAlQaddy TarekAlQaddy changed the title [CDX-430] Scope all Tailwind classes to work on the lib's components only [CDX-430] Scope all Tailwind classes to work on the library's components only May 27, 2026
constructor-claude-bedrock[bot]

This comment was marked as outdated.

constructor-claude-bedrock[bot]

This comment was marked as outdated.

constructor-claude-bedrock[bot]

This comment was marked as outdated.

constructor-claude-bedrock[bot]

This comment was marked as outdated.

constructor-claude-bedrock[bot]

This comment was marked as outdated.

This comment was marked as outdated.

constructor-claude-bedrock[bot]

This comment was marked as outdated.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.

Comment thread src/styles.css Outdated
Comment thread src/components/button.tsx
Comment thread src/components/badge.tsx
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown

@constructor-claude-bedrock constructor-claude-bedrock Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This PR correctly scopes all Tailwind v4 utility classes with the cio: prefix to prevent style conflicts with consumers' own Tailwind setups — the approach is sound and well-documented. There are a handful of correctness issues that should be addressed before merging.

Inline comments: 5 discussions added

Overall Assessment: ⚠️ Needs Work

Comment thread src/styles.css
selector contains a literal backslash, so it never matches the runtime
`cio:border-b` class. */
[data-slot='card-header'].cio\:border-b {
padding-bottom: calc(var(--cio-spacing) * 6);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Critical Issue: --cio-spacing is never defined — neither in @theme inline nor in :root. Tailwind v4's prefix(cio) renames --spacing to --cio-spacing, but that variable is only emitted when Tailwind's theme layer is imported (which this file does via @import 'tailwindcss/theme.css' layer(theme) prefix(cio)). However the @theme inline block re-aliases everything away from the generated names (--color-background: var(--cio-background), etc.), and the spacing token is notably absent from that block, so it is never exposed under --cio-spacing at runtime. The result is that the padding-bottom / padding-top on the divider-border rules will compute to calc(undefined * 6) and render as 0. Replace with the literal value (1.5rem, matching p-6) or define --cio-spacing explicitly in the :root block:

--cio-spacing: 0.25rem; /* Tailwind v4 default */

Comment thread src/components/badge.tsx

const badgeVariants = cva(
"cio-components cio-badge inline-flex items-center gap-1.5 whitespace-nowrap font-medium transition-all [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-3 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive overflow-hidden tracking-tighter",
"cio-components cio-badge cio:inline-flex cio:items-center cio:gap-1.5 cio:whitespace-nowrap cio:font-medium cio:transition-all cio:[&_svg]:pointer-events-none cio:[&_svg:not([class*='cio:size-'])]:size-3 cio:shrink-0 cio:[&_svg]:shrink-0 cio:outline-none cio:focus-visible:border-ring cio:focus-visible:ring-ring/50 cio:focus-visible:ring-[3px] cio:aria-invalid:ring-destructive/20 cio:dark:aria-invalid:ring-destructive/40 cio:aria-invalid:border-destructive cio:overflow-hidden cio:tracking-tighter",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important Issue: The arbitrary variant cio:[&_svg:not([class*='cio:size-'])]:size-3 is malformed. The utility at the end (size-3) is not prefixed with cio:, so it will be treated as an unprefixed Tailwind class and will not be scoped. It should be written as cio:[&_svg:not([class*='cio:size-'])]:size-3 where the entire thing is a single Tailwind token — but the correct form when using prefix(cio) is cio:[&_svg:not([class*='cio:size-'])]:cio:size-3, or more practically just cio:[&_svg:not([class*='size-'])]:size-3 (using the old check pattern) since the arbitrary variant modifier and the utility are separate tokens. The same issue applies in button.tsx. Verify the generated CSS contains the expected rule for these SVG size guards.

Comment thread src/components/button.tsx

const buttonVariants = cva(
"cio-components cio-button cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer",
"cio-components cio-button cio:cursor-pointer cio:inline-flex cio:items-center cio:justify-center cio:gap-2 cio:whitespace-nowrap cio:rounded-sm cio:text-sm cio:font-medium cio:transition-all cio:disabled:pointer-events-none cio:disabled:opacity-50 cio:[&_svg]:pointer-events-none cio:[&_svg:not([class*='cio:size-'])]:size-4 cio:shrink-0 cio:[&_svg]:shrink-0 cio:outline-none cio:focus-visible:border-ring cio:focus-visible:ring-ring/50 cio:focus-visible:ring-[3px] cio:aria-invalid:ring-destructive/20 cio:dark:aria-invalid:ring-destructive/40 cio:aria-invalid:border-destructive",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important Issue: Same malformed arbitrary variant as in badge.tsx: cio:[&_svg:not([class*='cio:size-'])]:size-4 — the fallback utility size-4 is not prefixed. Additionally, the original button.tsx had a duplicate cursor-pointer at the end of the base class string (a pre-existing bug). The new version removes one of them, which is correct, but worth confirming that cio:cursor-pointer is still present (it is, at the start of the string).

Comment thread src/components/card.tsx
data-slot='card-header'
className={cn(
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
'cio:@container/card-header cio:grid cio:auto-rows-min cio:grid-rows-[auto_auto] cio:items-start cio:gap-2 cio:has-data-[slot=card-action]:grid-cols-[1fr_auto]',
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important Issue: Two classes in this string are missing the cio: prefix: cio:grid-rows-[auto_auto] is prefixed correctly, but cio:has-data-[slot=card-action]:grid-cols-[1fr_auto] — the utility half (grid-cols-[1fr_auto]) is not prefixed. It should be cio:has-data-[slot=card-action]:grid-cols-[1fr_auto] which Tailwind v4 will interpret as the has-data-[slot=card-action] variant applied to the cio:grid-cols-[1fr_auto] utility. In practice the whole token must start with cio:, so write it as cio:has-data-[slot=card-action]:grid-cols-[1fr_auto] (which is already what's written). On closer inspection this is correct syntax for a single prefixed token. However the [.border-b]:pb-6 rule was removed from the class string and moved to a plain CSS selector in styles.css — confirm that existing consumers relying on the Tailwind arbitrary-variant form [.border-b]:pb-6 on CardHeader are not broken and that the new plain-CSS rule [data-slot='card-header'].cio\:border-b is documented in the README as the new API.

data-slot='carousel-item'
className={cn(
'shrink-0 grow-0 basis-full flex items-stretch justify-center items-center',
'cio:shrink-0 cio:grow-0 cio:basis-full cio:flex cio:justify-center cio:items-center',
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The old class string 'shrink-0 grow-0 basis-full flex items-stretch justify-center items-center' contained a pre-existing conflict — items-stretch and items-center were both present and items-center would win (last wins in Tailwind). The new string silently drops items-stretch entirely. This is likely intentional (since items-center was already overriding it) but should be an explicit, documented clean-up rather than a side-effect of the prefixing migration to make the intent clear.

Copy link
Copy Markdown
Contributor

@esezen esezen left a comment

Choose a reason for hiding this comment

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

LGTM! Let's get one more before we merge

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.

3 participants