[CDX-430] Scope all Tailwind classes to work on the library's components only#49
Conversation
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
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:
| 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); |
There was a problem hiding this comment.
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 */|
|
||
| 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", |
There was a problem hiding this comment.
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.
|
|
||
| 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", |
There was a problem hiding this comment.
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).
| 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]', |
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
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.
esezen
left a comment
There was a problem hiding this comment.
LGTM! Let's get one more before we merge
Goal
Prevent style conflicts when consumers use this UI library alongside their own Tailwind setup.
Approaches Explored
scope all the classes inside a container class or via CSS @scope Rule
Scope Utilities to a Container via @Utility
important() Scoping with Wrapper Element
Tailwind v4 prefix() ✅
Testing
npm run compileand verified that the generated style.css contains the correct class names.