Conversation
🦋 Changeset detectedLatest commit: 4dab9b1 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
📝 WalkthroughWalkthroughAdded an optional Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes The changes follow a highly repetitive pattern applied across 67+ component files, each adding the same Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
CoverageThis report compares the PR with the base branch. "Δ" shows how the PR affects each metric.
Coverage improved or stayed the same. Great job! Run |
There was a problem hiding this comment.
Actionable comments posted: 18
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
src/components/ui/Combobox/fragments/ComboboxRoot.tsx (1)
15-21:⚠️ Potential issue | 🔴 Critical
detachcurrently leaks generated classes in Combobox (-root).
At Line 21,${rootClass}-rootbecomes-rootwhendetachis true (rootClass === ''), which breaks the feature contract of emitting no Rad UI-generated classes.🐛 Suggested fix
const ComboboxRoot = React.forwardRef<ComboboxRootElement, ComboboxRootProps>( ({ customRootClass, detach = false, children, defaultValue, value, onValueChange, shift, ...props }, forwardedRef) => { const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach); + const scopedRootClass = rootClass ? `${rootClass}-root` : ''; return ( <ComboboxRootContext.Provider value={{ rootClass }}> <ComboboxPrimitive.Root - className={`${rootClass}-root`} + className={scopedRootClass} defaultValue={defaultValue} value={value} onValueChange={onValueChange}Also apply the same guard pattern in Combobox child fragments that build
${rootClass}-*(e.g., item/text) to fully satisfy detach mode.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/Combobox/fragments/ComboboxRoot.tsx` around lines 15 - 21, The Combobox is leaking a generated class when detach is true because `${rootClass}-root` becomes `-root` if rootClass is empty; change the class construction in the ComboboxRoot component (where you use customClassSwitcher, rootClass, and render ComboboxPrimitive.Root inside ComboboxRootContext.Provider) to only append the `-root` suffix when rootClass is non-empty (e.g., return an empty string/undefined or omit the className when rootClass === ''), and apply the identical guard pattern to all child fragments that build `${rootClass}-*` (e.g., item, text) so no `-*` classes are emitted in detach mode.src/components/ui/CheckboxCards/fragments/CheckboxCardsRoot.tsx (1)
28-28:⚠️ Potential issue | 🔴 CriticalCritical bug: Invalid CSS class names generated when
detach={true}When
detachistrue,customClassSwitcherreturns an empty string. The template literals across all CheckboxCards fragments then produce invalid class names:-root,-item,-tick-icon, and-content. This defeats the purpose ofdetach, which should suppress all generated classes.Affected files:
- CheckboxCardsRoot.tsx:28 →
-root- CheckboxCardsItem.tsx:15 →
-item- CheckboxCardsIndicator.tsx:34 →
-tick-icon- CheckboxCardsContent.tsx:52 →
-content🐛 Required fixes
CheckboxCardsRoot.tsx:28
- <CheckboxGroupPrimitive.Root ref={ref} className={clsx(`${rootClass}-root`, rootClass, className)} {...props} {...composedAttributes()}> + <CheckboxGroupPrimitive.Root ref={ref} className={clsx(rootClass && `${rootClass}-root`, rootClass, className)} {...props} {...composedAttributes()}>CheckboxCardsItem.tsx:15
- <CheckboxGroupPrimitive.Trigger ref={ref} className={clsx(`${rootClass}-item`, className)} value={value} {...props}> + <CheckboxGroupPrimitive.Trigger ref={ref} className={clsx(rootClass && `${rootClass}-item`, className)} value={value} {...props}>CheckboxCardsIndicator.tsx:34
- <svg ref={ref} width="15" height="15" viewBox="0 0 15 15" className={clsx(`${rootClass}-tick-icon`, className)} fill="none" xmlns="http://www.w3.org/2000/svg" {...props}><path d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3355 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.55529 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"></path></svg> + <svg ref={ref} width="15" height="15" viewBox="0 0 15 15" className={clsx(rootClass && `${rootClass}-tick-icon`, className)} fill="none" xmlns="http://www.w3.org/2000/svg" {...props}><path d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3355 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.55529 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"></path></svg>CheckboxCardsContent.tsx:52
- <CheckboxGroupPrimitive.Content ref={ref} className={`${rootClass}-content`} {...props} >{children}</CheckboxGroupPrimitive.Content> + <CheckboxGroupPrimitive.Content ref={ref} className={clsx(rootClass && `${rootClass}-content`, props.className)} {...props} >{children}</CheckboxGroupPrimitive.Content>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/CheckboxCards/fragments/CheckboxCardsRoot.tsx` at line 28, The generated classnames use template literals like `${rootClass}-root` which produce invalid names when customClassSwitcher returns an empty string; update each occurrence to only append the suffix when rootClass is truthy (e.g., change clsx(`${rootClass}-root`, rootClass, className) to clsx(rootClass && `${rootClass}-root`, rootClass, className)) so no leading hyphen is emitted; apply the same pattern in CheckboxCardsRoot (CheckboxGroupPrimitive.Root), CheckboxCardsItem (the item class), CheckboxCardsIndicator (the tick-icon class) and CheckboxCardsContent (the content class).src/components/ui/Splitter/fragments/SplitterRoot.tsx (1)
10-44:⚠️ Potential issue | 🔴 CriticalFix invalid CSS class names when
detachis enabled.When
detach = true,customClassSwitcherreturns an empty string. This causes child components to generate invalid CSS class names:
SplitterPanelproducesclassName="-panel"(line 34)SplitterHandleproducesclassName="-handle"(line 23)CSS class names cannot start with a hyphen. The
clsxfunction filters falsy values only, not invalid strings, so these malformed classes are included in the output.Fix: Avoid appending suffixes when
rootClassis empty. For example:className={clsx(rootClass && `${rootClass}-panel`, className)}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/Splitter/fragments/SplitterRoot.tsx` around lines 10 - 44, When detach is true customClassSwitcher can return an empty string causing rootClass to be falsy and yielding invalid class names like "-panel" and "-handle"; update SplitterRoot rendering logic so className concatenation for child components (SplitterPanel, SplitterHandle) only appends suffixes when rootClass is non-empty (e.g. use conditional expressions with rootClass before `${rootClass}-panel` / `${rootClass}-handle`), keep using clsx for merging but ensure you pass either the full suffixed string or nothing when rootClass is empty to prevent leading-hyphen class names.src/components/ui/Switch/fragments/SwitchRoot.tsx (1)
20-57:⚠️ Potential issue | 🔴 CriticalFix SwitchThumb to handle empty
rootClasswhendetachis enabled.When
detach=true,rootClassbecomes an empty string, causing SwitchThumb's className to render as"-indicator"(line 27), which is an invalid CSS class. SwitchThumb must conditionally build the className to avoid this:Suggested fix for SwitchThumb.tsx
className={rootClass ? `${rootClass}-indicator ${className || ''}` : className || ''}This pattern should also be checked in any other child components that interpolate
rootClass.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/Switch/fragments/SwitchRoot.tsx` around lines 20 - 57, The SwitchThumb className improperly interpolates rootClass when detach is true (rootClass becomes an empty string) causing an invalid "-indicator" class; update the SwitchThumb component to conditionally prepend `${rootClass}-indicator` only when rootClass is non-empty (use the rootClass value computed in SwitchRoot via customClassSwitcher) and fall back to the original className when rootClass is falsy, and audit other child components that concatenate rootClass similarly to apply the same conditional pattern.src/components/ui/Spinner/Spinner.tsx (1)
17-23:⚠️ Potential issue | 🟠 MajorDetach still emits
-containerin Spinner.
rootClasscan be empty, but the container always concatenates-container, so detach does not fully remove generated classes.Proposed fix
- <div className={`${rootClass}-container`}> + <div className={rootClass ? `${rootClass}-container` : undefined}>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/Spinner/Spinner.tsx` around lines 17 - 23, The Spinner always appends "-container" to rootClass even when customClassSwitcher returns an empty string (when detach is true), producing a stray "-container" class; update the Spinner render logic (in the React.forwardRef component named Spinner) to only append the "-container" suffix when rootClass is non-empty (e.g., conditionally build the container class using rootClass ? `${rootClass}-container` : '' or omit the class attribute), and ensure any other places that concatenate rootClass (referencing customClassSwitcher, COMPONENT_NAME, customRootClass, detach, and useCreateDataAttribute) follow the same conditional pattern so detach fully removes generated classes.src/components/ui/Slider/fragments/SliderRoot.tsx (1)
35-50:⚠️ Potential issue | 🔴 Critical
detachbreaks slider runtime targeting and still leaks generated class tokens.Line 50 can make
rootClassempty. Downstream code insrc/components/ui/Slider/fragments/SliderRangeSlider.tsx(Lines 27-82) andsrc/components/ui/Slider/fragments/SliderTrack.tsx(Lines 11-19) derives selectors/classes fromrootClass, causing collisions (data-slider-root="") and class tokens like-thumb/-trackinstead of removing generated classes.Suggested direction (separate runtime key from CSS namespace)
+ const runtimeRootId = React.useId(); const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach); + const rootQueryKey = detach ? runtimeRootId : rootClass;- data-slider-root={rootClass} + data-slider-root={rootQueryKey}Then update slider fragments to use the runtime key for querying and avoid
${rootClass}-...when detached.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/Slider/fragments/SliderRoot.tsx` around lines 35 - 50, The detach flag currently makes rootClass empty which breaks runtime querying and leaks generated class tokens; ensure SliderRoot always emits a stable runtime key (e.g., data-slider-root or runtimeClassKey) independent of the CSS namespace and keep customClassSwitcher(COMPONENT_NAME, detach) only for the CSS namespace; update the SliderRangeSlider and SliderTrack fragments to query elements by that runtime key (e.g., attribute selector or runtimeClassKey) instead of constructing selectors using `${rootClass}-...`, and when detach is true avoid appending generated class tokens like `-thumb`/`-track` to the CSS namespace so tokens are not leaked. Ensure symbols involved: detach, rootClass, customClassSwitcher, COMPONENT_NAME, SliderRangeSlider, SliderTrack and the runtime key are created in SliderRoot and consumed by the fragments.src/components/ui/Select/fragments/SelectRoot.tsx (1)
16-21:⚠️ Potential issue | 🟠 MajorEmpty
rootClasspropagates malformed class names to all Select fragments.When
detach=true,rootClassbecomes'', and the context-provided value propagates to child fragments producing:
- Line 21:
${rootClass}-root→-rootSelectTrigger:${rootClass}-trigger→-triggerSelectContent:${rootClass}-content→-contentSelectItem:${rootClass}-item,${rootClass}-text→-item,-textEither propagate
detachthroughSelectRootContextso children can conditionally skip class application, or guard template literals against emptyrootClass.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/Select/fragments/SelectRoot.tsx` around lines 16 - 21, rootClass can be an empty string when detach=true, causing malformed class names like `-root` to be emitted; change the provider and rendering to avoid emitting suffix-only classes by either passing detach through SelectRootContext or guarding template literals: update customClassSwitcher/SelectRoot to include detach in the context value (e.g., provide { rootClass, detach }) and then in child fragments (SelectTrigger, SelectContent, SelectItem) read detach from SelectRootContext and only append suffixes when rootClass is non-empty (or alternatively wrap `${rootClass}-...` with a conditional that returns the suffix class only when rootClass) so ComboboxPrimitive.Root and all fragment className interpolations no longer produce `-root`, `-trigger`, `-content`, or `-item` when detached.src/components/ui/Table/fragments/TableRoot.tsx (1)
20-28:⚠️ Potential issue | 🟠 Major
-wrapperclass generated whendetach=true.When
detach=true,rootClassbecomes'':
- Line 26:
`${rootClass}-wrapper`→-wrapper(malformed)- Line 28:
clsx(rootClass)→''(correctly empty)The wrapper div will have an unintended
-wrapperclass. Consider guarding the template literal:🛠️ Suggested fix
- return <div ref={ref} className={clsx(`${rootClass}-wrapper`, className)} {...props} > + return <div ref={ref} className={clsx(rootClass && `${rootClass}-wrapper`, className)} {...props} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/Table/fragments/TableRoot.tsx` around lines 20 - 28, The wrapper div builds a malformed `-wrapper` class when `rootClass` is empty (detach=true); update the JSX in TableRoot.tsx so the wrapper class is only added when rootClass is truthy (e.g., replace className={clsx(`${rootClass}-wrapper`, className)} with a conditional such as className={clsx(rootClass ? `${rootClass}-wrapper` : undefined, className)} or className={clsx(rootClass && `${rootClass}-wrapper`, className)}; keep the table className={clsx(rootClass)} unchanged and no changes to customClassSwitcher or detach logic.
🧹 Nitpick comments (1)
src/components/ui/ToggleGroup/stories/ToggleGroup.stories.tsx (1)
18-18: Avoid hardcodingcustomRootClassin the shared template.
Line 18 forcescustomRootClass='s'for every story variant, which masks default class-generation behavior.♻️ Suggested update
- <ToggleGroup.Root type={args.type} className={args.className} color={args.color} customRootClass='s'> + <ToggleGroup.Root + type={args.type} + className={args.className} + color={args.color} + customRootClass={args.customRootClass} + >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/ToggleGroup/stories/ToggleGroup.stories.tsx` at line 18, The shared story template hardcodes customRootClass='s' on ToggleGroup.Root which forces the same class for every story and hides default class-generation; remove the hardcoded customRootClass prop from the template (or make it driven by args like args.customRootClass) so ToggleGroup.Root uses its default behavior or the story-provided value—update the ToggleGroup.Root usage in the story file to stop passing a literal 's' and instead rely on args or omit the prop.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/ui/Accordion/fragments/AccordionRoot.tsx`:
- Line 44: The AccordionRoot currently always appends the "-root" token because
rootClass is computed unconditionally by calling
customClassSwitcher(customRootClass, COMPONENT_NAME, detach); update the logic
so that when detach is true no generated "-root" token is emitted: either change
the call site in AccordionRoot to set rootClass = '' when detach === true, or
modify customClassSwitcher to early-return an empty string when the detach flag
is true; reference the symbols AccordionRoot, customClassSwitcher,
customRootClass, COMPONENT_NAME and the detach variable when making the change
so the rootClass no longer contains the "-root" suffix in detach mode.
In `@src/components/ui/Avatar/fragments/AvatarRoot.tsx`:
- Around line 19-20: AvatarRoot can produce dash-prefixed child classes when
rootClass is empty (e.g., AvatarImage/AvatarFallback end up with
"-image"/"-fallback") because rootClass is blindly concatenated; update
AvatarRoot to ensure rootClass is non-empty before it is used to build
descendant class names (use customClassSwitcher(customRootClass, COMPONENT_NAME,
detach) but guard/normalize its return to '' when it would produce a lone dash),
and update any places that derive `${rootClass}-image` / `${rootClass}-fallback`
so they only append the dash when rootClass is truthy; focus changes in the
AvatarRoot component (variables: rootClass, customRootClass, detach, and
function customClassSwitcher) and ensure AvatarImage and AvatarFallback class
construction logic checks rootClass before adding the '-' prefix.
In `@src/components/ui/CheckboxGroup/fragments/CheckboxGroupRoot.tsx`:
- Around line 19-20: CheckboxGroupRoot builds rootClass via
customClassSwitcher(customRootClass, COMPONENT_NAME, detach) but later
unconditionally appends `${rootClass}-root`, which yields `-root` when detach
true (rootClass empty); update the JSX/className assembly in the
CheckboxGroupRoot component so the `${rootClass}-root` token is only added when
rootClass is non-empty (e.g., guard the addition based on rootClass truthiness)
and continue to respect props customRootClass and detach so no stray `-root`
class is emitted.
In `@src/components/ui/ContextMenu/fragments/ContextMenuRoot.tsx`:
- Around line 18-28: The string interpolation that appends part suffixes (e.g.,
`${rootClass}-root`) emits leading hyphens when rootClass is empty due to
detach; update ContextMenuRoot (and sibling fragments ContextMenuContent,
ContextMenuTrigger, ContextMenuItem) to only append
`-root`/`-content`/`-trigger`/`-item` when rootClass is non-empty—use the
existing customClassSwitcher/COMPONENT_NAME logic to compute rootClass and guard
the template concatenation so that when detach produces an empty rootClass you
render just `rootClass` (empty) plus any className instead of `-root` (i.e.,
conditionally join the suffix only if rootClass !== "").
In `@src/components/ui/DataList/fragments/DataListRoot.tsx`:
- Around line 11-15: The DataListRoot component's detach prop causes
customClassSwitcher to return an empty rootClass which makes child components
that build classes like `${rootClass}-value`, `${rootClass}-label`,
`${rootClass}-item` produce malformed names (e.g., `-value`); fix by either
removing the unused detach prop from DataListRoot and its type, or updating one
of two spots: (A) change customClassSwitcher so when detach=true it returns a
sensible default base class (not an empty string), or (B) update child
components that reference rootClass (the templates producing `${rootClass}-...`)
to guard against empty rootClass by using a prefix expression such as
`${rootClass ? rootClass + '-' : ''}value` so no leading hyphen is produced.
Ensure references include DataListRoot, detach, customClassSwitcher and the
child class interpolation sites.
In `@src/components/ui/Dialog/fragments/DialogRoot.tsx`:
- Around line 20-23: DialogRoot currently sets contextProps.rootClass to an
empty string when detach is true, which lets downstream fragments
(Trigger/Content/Overlay/Close) build invalid classes like "-trigger"; update
DialogRoot and the fragments so that the suffixes are only generated when
rootClass is non-empty: compute and expose either an empty string or full
suffixes (e.g., `${rootClass}-trigger`) or expose rootClass and have each
fragment guard when composing their class (use a conditional like rootClass ?
`${rootClass}-<suffix>` : ''), and apply this same guard pattern to Trigger,
Content, Overlay and Close so no leading hyphen classes are emitted when detach
is true.
In `@src/components/ui/Disclosure/fragments/DisclosureRoot.tsx`:
- Line 18: rootClass (computed by customClassSwitcher with arguments
customRootClass, COMPONENT_NAME, detach) can be an empty string causing
className templates like `${rootClass}-root` to produce `-root`; update the
DisclosureRoot render so you only append `-root` when rootClass is non-empty
(e.g., conditionally add `${rootClass}-root`), and confirm whether the
`${rootClass}-root` should be applied to RovingFocusGroup.Group or the inner div
(RovingFocusGroup.Group and the inner div currently both receive it) — remove
the duplicate application if it was unintentional and apply the conditional
logic to the className expressions for RovingFocusGroup.Group and the inner div
so no `-root` appears when detach=true.
In `@src/components/ui/DropdownMenu/fragments/DropdownMenuRoot.tsx`:
- Around line 17-21: The root currently concatenates `${rootClass}-root` even
when detach is true and rootClass is empty, producing a stray "-root"; update
DropdownMenuRoot to only append the suffix when rootClass is non-empty (e.g.,
compute the className as rootClass ? `${rootClass}-root` : undefined or omit the
generated token) before passing it to MenuPrimitive.Root and into
DropdownMenuContext.Provider, and apply the same guard in all child fragments
that construct `${rootClass}-...` class names (use the same conditional
concatenation pattern wherever rootClass is used).
In `@src/components/ui/Menubar/fragments/MenubarRoot.tsx`:
- Line 18: MenubarRoot is still appending the generated "-root" class token when
detach is true; update the construction of rootClass (and any slot class
concatenation at the slot line mentioned) so that when detach === true you do
not include the generated token from customClassSwitcher. Specifically, change
the logic around the rootClass assignment and the slot class concatenation in
MenubarRoot to use only customRootClass (and skip adding
COMPONENT_NAME-derived/generated tokens) when detach is true; keep calling
customClassSwitcher(customRootClass, COMPONENT_NAME, detach) only in the
non-detached branch so the generated "-root" class is not emitted while
detached.
In `@src/components/ui/Minimap/fragments/MinimapRoot.tsx`:
- Around line 17-18: MinimapRoot can produce an empty rootClass which causes
child fragments to emit `-item`/`-line`/`-track`; update the descendant
fragments that build class names from rootClass (currently using
`${rootClass}-item`, `${rootClass}-line`, `${rootClass}-track`) to conditionally
prefix the dash only when rootClass is non-empty (for example compute const
rootPrefix = rootClass ? `${rootClass}-` : '' and use `${rootPrefix}item` /
`${rootPrefix}line` / `${rootPrefix}track`), or otherwise use the ternary
`${rootClass ? `${rootClass}-` : ''}item` inline so no leading dash is produced.
In `@src/components/ui/NavigationMenu/fragments/NavigationMenuRoot.tsx`:
- Line 35: NavigationMenuRoot currently builds a slot class even when
customClassSwitcher returns an empty rootClass (so detach mode still emits a
"-root" token); update the logic in NavigationMenuRoot (around the rootClass
computation that calls customClassSwitcher with COMPONENT_NAME and detach) to
only construct the slot class string when rootClass is truthy — e.g., if
rootClass is empty, set the slot class to an empty string instead of
concatenating suffixes like "-root"; this ensures no generated "-root" token
appears in detach mode.
In `@src/components/ui/RadioGroup/fragments/RadioGroupRoot.tsx`:
- Around line 27-29: In RadioGroupRoot, the computed DOM class always includes
the literal suffix "-root" even when detach is true because rootClass can be
empty; change the className construction (where rootClass is derived via
customClassSwitcher and used to form `${rootClass}-root`) to conditionally
append the "-root" token only when rootClass is non-empty (e.g., use rootClass ?
`${rootClass}-root` : ''), preserving any passed-in className and
dataAttributes; update the code that builds the final class string used by the
component (referencing rootClass, customClassSwitcher, detach, and
COMPONENT_NAME) so no "-root" token is emitted when detached.
In `@src/components/ui/Separator/Separator.tsx`:
- Around line 33-34: The current logic builds orientationClass using rootClass
even when detach is true, causing generated fragments like "-vertical" or
"-horizontal" when customClassSwitcher(customRootClass, COMPONENT_NAME, detach)
returns an empty string; update the orientationClass computation in
Separator.tsx so it only appends `-vertical`/`-horizontal` when rootClass is
non-empty (i.e., when detach is false or customClassSwitcher returned a
non-empty rootClass); keep the existing variables (customClassSwitcher,
customRootClass, COMPONENT_NAME, detach, rootClass, orientationClass,
orientation) and guard the concatenation so no leading hyphen fragments are
produced in detach mode.
In `@src/components/ui/Steps/fragments/StepRoot.tsx`:
- Around line 34-37: rootClass can be an empty string when detach=true which
produces malformed class names like "-horizontal"; update the JSX and context so
suffixes are only added when rootClass is truthy and propagate the detach state
to children: in the component that computes rootClass (using customClassSwitcher
and COMPONENT_NAME) compute conditional classes (e.g., orientationClass =
rootClass ? `${rootClass}-${orientation}` : '' and itemClass = rootClass ?
`${rootClass}-item` : '') and pass either the boolean detach or the computed
rootClass/itemClass into StepsContext.Provider (value: { currentStep,
setCurrentStep, rootClass, orientation, detachOrItemClass }) so child fragments
(e.g., StepItem) can avoid rendering `${rootClass}-item` when rootClass is
empty. Ensure all template usages of `${rootClass}-...` check rootClass
truthiness before concatenation.
In `@src/components/ui/TabNav/fragments/TabNavRoot.tsx`:
- Line 26: The child slot class (`${rootClass}-link`) can become `-link` when
rootClass is empty in detach mode; fix by making TabNavRoot provide detach-aware
class info to descendants: compute safe slot classes (e.g., slotClasses.link =
detach ? '' : `${rootClass}-link`) in TabNavRoot (where rootClass is created via
customClassSwitcher and COMPONENT_NAME) and pass either the detach flag or the
precomputed slotClasses through the existing context/provider so TabNavLink (and
other fragments) use the context value instead of deriving `${rootClass}-link`
themselves; ensure the same change is applied to the other slot constructions
mentioned around the later block (48-52) so no generated classes are emitted in
detach mode.
In `@src/components/ui/Toolbar/fragments/ToolbarRoot.tsx`:
- Around line 35-36: rootClass can be empty when detach is true, but child
fragments that build classes by suffixing `${rootClass}-button` still emit
leftover "-button" classes; update the child fragments that suffix rootClass to
guard against falsy rootClass (use the same detach check used in
customClassSwitcher/customRootClass/COMPONENT_NAME logic) so they only append
`-button` (or other suffixes) when rootClass is truthy—locate usages that
reference rootClass in toolbar subcomponents and change them to compute suffix
classes only if rootClass is non-empty.
In `@src/components/ui/Tree/fragments/TreeRoot.tsx`:
- Around line 16-24: The TreeItem component currently interpolates rootClass
into className as clsx(`${rootClass}-item`, className), which yields "-item"
when customClassSwitcher returned an empty string (detach=true); update TreeItem
(component TreeItem) to guard the interpolation by using conditional logic so
the item class is only added when rootClass is truthy (e.g., use clsx(rootClass
&& `${rootClass}-item`, className)), referencing the rootClass produced in
TreeRoot via customClassSwitcher to avoid emitting a leading dash when detached.
In `@src/core/customClassSwitcher/index.ts`:
- Around line 7-10: customClassSwitcher currently returns an empty string when
detach=true which leads to malformed child class names like `-item`; change the
contract so customClassSwitcher(customRootClass, componentName, detach) returns
a clear sentinel (e.g., null/undefined) when detach is requested and then update
all child fragments that use the returned value (places that build classes with
template literals such as `${rootClass}-item`, `${rootClass}-trigger`,
`${rootClass}-wrapper`) to conditionally omit the entire class attribute when
rootClass is the sentinel. Alternatively, if you prefer propagation, pass the
detach boolean through context (from where customClassSwitcher is called) so
child components can check detach and skip assigning class names instead of
relying on an empty string.
---
Outside diff comments:
In `@src/components/ui/CheckboxCards/fragments/CheckboxCardsRoot.tsx`:
- Line 28: The generated classnames use template literals like
`${rootClass}-root` which produce invalid names when customClassSwitcher returns
an empty string; update each occurrence to only append the suffix when rootClass
is truthy (e.g., change clsx(`${rootClass}-root`, rootClass, className) to
clsx(rootClass && `${rootClass}-root`, rootClass, className)) so no leading
hyphen is emitted; apply the same pattern in CheckboxCardsRoot
(CheckboxGroupPrimitive.Root), CheckboxCardsItem (the item class),
CheckboxCardsIndicator (the tick-icon class) and CheckboxCardsContent (the
content class).
In `@src/components/ui/Combobox/fragments/ComboboxRoot.tsx`:
- Around line 15-21: The Combobox is leaking a generated class when detach is
true because `${rootClass}-root` becomes `-root` if rootClass is empty; change
the class construction in the ComboboxRoot component (where you use
customClassSwitcher, rootClass, and render ComboboxPrimitive.Root inside
ComboboxRootContext.Provider) to only append the `-root` suffix when rootClass
is non-empty (e.g., return an empty string/undefined or omit the className when
rootClass === ''), and apply the identical guard pattern to all child fragments
that build `${rootClass}-*` (e.g., item, text) so no `-*` classes are emitted in
detach mode.
In `@src/components/ui/Select/fragments/SelectRoot.tsx`:
- Around line 16-21: rootClass can be an empty string when detach=true, causing
malformed class names like `-root` to be emitted; change the provider and
rendering to avoid emitting suffix-only classes by either passing detach through
SelectRootContext or guarding template literals: update
customClassSwitcher/SelectRoot to include detach in the context value (e.g.,
provide { rootClass, detach }) and then in child fragments (SelectTrigger,
SelectContent, SelectItem) read detach from SelectRootContext and only append
suffixes when rootClass is non-empty (or alternatively wrap `${rootClass}-...`
with a conditional that returns the suffix class only when rootClass) so
ComboboxPrimitive.Root and all fragment className interpolations no longer
produce `-root`, `-trigger`, `-content`, or `-item` when detached.
In `@src/components/ui/Slider/fragments/SliderRoot.tsx`:
- Around line 35-50: The detach flag currently makes rootClass empty which
breaks runtime querying and leaks generated class tokens; ensure SliderRoot
always emits a stable runtime key (e.g., data-slider-root or runtimeClassKey)
independent of the CSS namespace and keep customClassSwitcher(COMPONENT_NAME,
detach) only for the CSS namespace; update the SliderRangeSlider and SliderTrack
fragments to query elements by that runtime key (e.g., attribute selector or
runtimeClassKey) instead of constructing selectors using `${rootClass}-...`, and
when detach is true avoid appending generated class tokens like
`-thumb`/`-track` to the CSS namespace so tokens are not leaked. Ensure symbols
involved: detach, rootClass, customClassSwitcher, COMPONENT_NAME,
SliderRangeSlider, SliderTrack and the runtime key are created in SliderRoot and
consumed by the fragments.
In `@src/components/ui/Spinner/Spinner.tsx`:
- Around line 17-23: The Spinner always appends "-container" to rootClass even
when customClassSwitcher returns an empty string (when detach is true),
producing a stray "-container" class; update the Spinner render logic (in the
React.forwardRef component named Spinner) to only append the "-container" suffix
when rootClass is non-empty (e.g., conditionally build the container class using
rootClass ? `${rootClass}-container` : '' or omit the class attribute), and
ensure any other places that concatenate rootClass (referencing
customClassSwitcher, COMPONENT_NAME, customRootClass, detach, and
useCreateDataAttribute) follow the same conditional pattern so detach fully
removes generated classes.
In `@src/components/ui/Splitter/fragments/SplitterRoot.tsx`:
- Around line 10-44: When detach is true customClassSwitcher can return an empty
string causing rootClass to be falsy and yielding invalid class names like
"-panel" and "-handle"; update SplitterRoot rendering logic so className
concatenation for child components (SplitterPanel, SplitterHandle) only appends
suffixes when rootClass is non-empty (e.g. use conditional expressions with
rootClass before `${rootClass}-panel` / `${rootClass}-handle`), keep using clsx
for merging but ensure you pass either the full suffixed string or nothing when
rootClass is empty to prevent leading-hyphen class names.
In `@src/components/ui/Switch/fragments/SwitchRoot.tsx`:
- Around line 20-57: The SwitchThumb className improperly interpolates rootClass
when detach is true (rootClass becomes an empty string) causing an invalid
"-indicator" class; update the SwitchThumb component to conditionally prepend
`${rootClass}-indicator` only when rootClass is non-empty (use the rootClass
value computed in SwitchRoot via customClassSwitcher) and fall back to the
original className when rootClass is falsy, and audit other child components
that concatenate rootClass similarly to apply the same conditional pattern.
In `@src/components/ui/Table/fragments/TableRoot.tsx`:
- Around line 20-28: The wrapper div builds a malformed `-wrapper` class when
`rootClass` is empty (detach=true); update the JSX in TableRoot.tsx so the
wrapper class is only added when rootClass is truthy (e.g., replace
className={clsx(`${rootClass}-wrapper`, className)} with a conditional such as
className={clsx(rootClass ? `${rootClass}-wrapper` : undefined, className)} or
className={clsx(rootClass && `${rootClass}-wrapper`, className)}; keep the table
className={clsx(rootClass)} unchanged and no changes to customClassSwitcher or
detach logic.
---
Nitpick comments:
In `@src/components/ui/ToggleGroup/stories/ToggleGroup.stories.tsx`:
- Line 18: The shared story template hardcodes customRootClass='s' on
ToggleGroup.Root which forces the same class for every story and hides default
class-generation; remove the hardcoded customRootClass prop from the template
(or make it driven by args like args.customRootClass) so ToggleGroup.Root uses
its default behavior or the story-provided value—update the ToggleGroup.Root
usage in the story file to stop passing a literal 's' and instead rely on args
or omit the prop.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5e579faa-df87-4d32-8481-081073c7f451
📒 Files selected for processing (61)
.changeset/floppy-spoons-appear.mdsrc/components/ui/Accordion/fragments/AccordionRoot.tsxsrc/components/ui/AlertDialog/fragments/AlertDialogRoot.tsxsrc/components/ui/AspectRatio/AspectRatio.tsxsrc/components/ui/Avatar/fragments/AvatarRoot.tsxsrc/components/ui/AvatarGroup/fragments/AvatarGroupRoot.tsxsrc/components/ui/Badge/Badge.tsxsrc/components/ui/BlockQuote/BlockQuote.tsxsrc/components/ui/Button/Button.tsxsrc/components/ui/Callout/fragments/CalloutRoot.tsxsrc/components/ui/Card/fragments/CardRoot.tsxsrc/components/ui/Checkbox/fragments/CheckboxRoot.tsxsrc/components/ui/CheckboxCards/fragments/CheckboxCardsRoot.tsxsrc/components/ui/CheckboxGroup/fragments/CheckboxGroupRoot.tsxsrc/components/ui/Code/Code.tsxsrc/components/ui/Collapsible/fragments/CollapsibleRoot.tsxsrc/components/ui/Combobox/fragments/ComboboxRoot.tsxsrc/components/ui/ContextMenu/fragments/ContextMenuRoot.tsxsrc/components/ui/DataList/fragments/DataListRoot.tsxsrc/components/ui/Dialog/fragments/DialogRoot.tsxsrc/components/ui/Disclosure/fragments/DisclosureRoot.tsxsrc/components/ui/DropdownMenu/fragments/DropdownMenuRoot.tsxsrc/components/ui/Em/Em.tsxsrc/components/ui/Heading/Heading.tsxsrc/components/ui/HoverCard/fragments/HoverCardRoot.tsxsrc/components/ui/Kbd/Kbd.tsxsrc/components/ui/Link/Link.tsxsrc/components/ui/Menubar/fragments/MenubarRoot.tsxsrc/components/ui/Minimap/fragments/MinimapRoot.tsxsrc/components/ui/NavigationMenu/fragments/NavigationMenuRoot.tsxsrc/components/ui/Progress/fragments/ProgressRoot.tsxsrc/components/ui/Quote/Quote.tsxsrc/components/ui/Radio/Radio.tsxsrc/components/ui/RadioCards/fragments/RadioCardsRoot.tsxsrc/components/ui/RadioGroup/fragments/RadioGroupRoot.tsxsrc/components/ui/ScrollArea/fragments/ScrollAreaRoot.tsxsrc/components/ui/Select/fragments/SelectRoot.tsxsrc/components/ui/Separator/Separator.tsxsrc/components/ui/Skeleton/Skeleton.tsxsrc/components/ui/Slider/fragments/SliderRoot.tsxsrc/components/ui/Spinner/Spinner.tsxsrc/components/ui/Splitter/fragments/SplitterRoot.tsxsrc/components/ui/Steps/fragments/StepRoot.tsxsrc/components/ui/Strong/Strong.tsxsrc/components/ui/Switch/fragments/SwitchRoot.tsxsrc/components/ui/TabNav/fragments/TabNavRoot.tsxsrc/components/ui/Table/fragments/TableRoot.tsxsrc/components/ui/Tabs/fragments/TabContent.tsxsrc/components/ui/Tabs/fragments/TabRoot.tsxsrc/components/ui/Text/Text.tsxsrc/components/ui/TextArea/fragments/TextAreaRoot.tsxsrc/components/ui/Toggle/Toggle.tsxsrc/components/ui/Toggle/tests/Toggle.test.jssrc/components/ui/ToggleGroup/fragments/ToggleGroupRoot.tsxsrc/components/ui/ToggleGroup/stories/ToggleGroup.stories.tsxsrc/components/ui/Toolbar/fragments/ToolbarRoot.tsxsrc/components/ui/Tree/fragments/TreeRoot.tsxsrc/components/ui/VisuallyHidden/VisuallyHidden.tsxsrc/core/customClassSwitcher/customClassSwitcher.test.tsxsrc/core/customClassSwitcher/index.tssrc/core/utils/mergeRefs.ts
| }, forwardedRef) => { | ||
| const accordionRef = useRef<HTMLDivElement | null>(null); | ||
| const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME); | ||
| const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach); |
There was a problem hiding this comment.
Detach mode still outputs -root class in AccordionRoot.
Line 149 unconditionally appends -root. With detach=true (Line 44 path), this still emits a generated token (-root) instead of no class output.
Also applies to: 149-149
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/ui/Accordion/fragments/AccordionRoot.tsx` at line 44, The
AccordionRoot currently always appends the "-root" token because rootClass is
computed unconditionally by calling customClassSwitcher(customRootClass,
COMPONENT_NAME, detach); update the logic so that when detach is true no
generated "-root" token is emitted: either change the call site in AccordionRoot
to set rootClass = '' when detach === true, or modify customClassSwitcher to
early-return an empty string when the detach flag is true; reference the symbols
AccordionRoot, customClassSwitcher, customRootClass, COMPONENT_NAME and the
detach variable when making the change so the rootClass no longer contains the
"-root" suffix in detach mode.
| const AvatarRoot = React.forwardRef<React.ElementRef<typeof AvatarPrimitiveRoot>, AvatarRootProps>(({ children, customRootClass = '', detach = false, className = '', size = '', variant = '', color = '', ...props }, ref) => { | ||
| const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach); |
There was a problem hiding this comment.
detach currently leaks dash-prefixed Avatar child classes.
When Line [20] yields rootClass = '', AvatarImage / AvatarFallback still build ${rootClass}-image and ${rootClass}-fallback, producing -image / -fallback instead of no generated classes.
Proposed fix for descendant class guards
# src/components/ui/Avatar/fragments/AvatarImage.tsx
- className={clsx(`${rootClass}-image`)}
+ className={clsx(rootClass && `${rootClass}-image`)}
# src/components/ui/Avatar/fragments/AvatarFallback.tsx
- className={clsx(`${rootClass}-fallback`, className)}
+ className={clsx(rootClass && `${rootClass}-fallback`, className)}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/ui/Avatar/fragments/AvatarRoot.tsx` around lines 19 - 20,
AvatarRoot can produce dash-prefixed child classes when rootClass is empty
(e.g., AvatarImage/AvatarFallback end up with "-image"/"-fallback") because
rootClass is blindly concatenated; update AvatarRoot to ensure rootClass is
non-empty before it is used to build descendant class names (use
customClassSwitcher(customRootClass, COMPONENT_NAME, detach) but guard/normalize
its return to '' when it would produce a lone dash), and update any places that
derive `${rootClass}-image` / `${rootClass}-fallback` so they only append the
dash when rootClass is truthy; focus changes in the AvatarRoot component
(variables: rootClass, customRootClass, detach, and function
customClassSwitcher) and ensure AvatarImage and AvatarFallback class
construction logic checks rootClass before adding the '-' prefix.
| const CheckboxGroupRoot = forwardRef<CheckboxGroupRootElement, CheckboxGroupRootProps>(({ children, customRootClass = '', detach = false, className = '', color = '', variant = '', size = '', ...props }, ref) => { | ||
| const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach); |
There was a problem hiding this comment.
detach=true still emits a generated -root class.
Line [20] can set rootClass to empty, but Line [28] always adds ${rootClass}-root, which becomes -root.
Proposed fix
- <CheckboxGroupPrimitive.Root ref={ref} className={clsx(`${rootClass}-root`, rootClass, className)} {...props} {...composedAttributes()}>
+ <CheckboxGroupPrimitive.Root
+ ref={ref}
+ className={clsx(rootClass && `${rootClass}-root`, rootClass, className)}
+ {...props}
+ {...composedAttributes()}
+ >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/ui/CheckboxGroup/fragments/CheckboxGroupRoot.tsx` around lines
19 - 20, CheckboxGroupRoot builds rootClass via
customClassSwitcher(customRootClass, COMPONENT_NAME, detach) but later
unconditionally appends `${rootClass}-root`, which yields `-root` when detach
true (rootClass empty); update the JSX/className assembly in the
CheckboxGroupRoot component so the `${rootClass}-root` token is only added when
rootClass is non-empty (e.g., guard the addition based on rootClass truthiness)
and continue to respect props customRootClass and detach so no stray `-root`
class is emitted.
| const ContextMenuRoot = forwardRef<ContextMenuRootElement, ContextMenuRootProps>(({ children, customRootClass, detach = false, className, open, defaultOpen = false, onOpenChange, ...props }, ref) => { | ||
| const [isOpen, setIsOpen] = useControllableState( | ||
| open, | ||
| defaultOpen, | ||
| onOpenChange | ||
| ); | ||
| const [coords, setCoords] = React.useState({ x: 0, y: 0 }); | ||
| const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME); | ||
| const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach); | ||
| return ( | ||
| <ContextMenuContext.Provider value={{ rootClass, setCoords, setIsOpen }} > | ||
| <MenuPrimitive.Root ref={ref} className={clsx(`${rootClass}-root`, className)} mainAxisOffset={-coords.y} crossAxisOffset={coords.x} open={isOpen} onOpenChange={setIsOpen} {...props}> |
There was a problem hiding this comment.
Detach is incomplete: generated class tokens are still emitted.
Line 28 builds ${rootClass}-root; when detached this becomes -root. The same pattern exists in ContextMenuContent, ContextMenuTrigger, and ContextMenuItem, producing -content/-trigger/-item. That violates the requested detach behavior.
Proposed root-level fix
- <MenuPrimitive.Root ref={ref} className={clsx(`${rootClass}-root`, className)} mainAxisOffset={-coords.y} crossAxisOffset={coords.x} open={isOpen} onOpenChange={setIsOpen} {...props}>
+ <MenuPrimitive.Root ref={ref} className={clsx(rootClass && `${rootClass}-root`, className)} mainAxisOffset={-coords.y} crossAxisOffset={coords.x} open={isOpen} onOpenChange={setIsOpen} {...props}>Please mirror this guard in the sibling fragments that append -${part} to rootClass.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/ui/ContextMenu/fragments/ContextMenuRoot.tsx` around lines 18
- 28, The string interpolation that appends part suffixes (e.g.,
`${rootClass}-root`) emits leading hyphens when rootClass is empty due to
detach; update ContextMenuRoot (and sibling fragments ContextMenuContent,
ContextMenuTrigger, ContextMenuItem) to only append
`-root`/`-content`/`-trigger`/`-item` when rootClass is non-empty—use the
existing customClassSwitcher/COMPONENT_NAME logic to compute rootClass and guard
the template concatenation so that when detach produces an empty rootClass you
render just `rootClass` (empty) plus any className instead of `-root` (i.e.,
conditionally join the suffix only if rootClass !== "").
| detach?: boolean; | ||
| } | ||
|
|
||
| const DataListRoot = forwardRef<DataListRootElement, DataListRootProps>(({ children, className = '', customRootClass = '', ...props }, ref) => { | ||
| const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME); | ||
| const DataListRoot = forwardRef<DataListRootElement, DataListRootProps>(({ children, className = '', customRootClass = '', detach = false, ...props }, ref) => { | ||
| const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for string interpolation patterns with rootClass in DataList
rg -n '\$\{rootClass\}' src/components/ui/DataList/Repository: rad-ui/ui
Length of output: 535
🏁 Script executed:
#!/bin/bash
# Find customClassSwitcher implementation to understand what it returns
rg -n 'customClassSwitcher' src/components/ui/DataList/ -A 3 -B 3Repository: rad-ui/ui
Length of output: 1360
🏁 Script executed:
#!/bin/bash
# Find the definition of customClassSwitcher function
fd -t f -e tsx -e ts . | xargs rg -l 'export.*customClassSwitcher|const customClassSwitcher|function customClassSwitcher'Repository: rad-ui/ui
Length of output: 108
🏁 Script executed:
#!/bin/bash
# Check DataListRoot to see what COMPONENT_NAME and customRootClass values might be
rg -n 'COMPONENT_NAME\|customRootClass' src/components/ui/DataList/fragments/DataListRoot.tsx -B 2 -A 2Repository: rad-ui/ui
Length of output: 35
🏁 Script executed:
#!/bin/bash
# Read the customClassSwitcher implementation
cat src/core/customClassSwitcher/index.tsRepository: rad-ui/ui
Length of output: 779
🏁 Script executed:
#!/bin/bash
# Also check what's exported from core to understand the function signature
head -50 src/core/index.tsRepository: rad-ui/ui
Length of output: 233
🏁 Script executed:
#!/bin/bash
# Search for usage of DataList component with detach prop
rg -n 'detach\s*=' src/components/ui/DataList/ -A 2 -B 2Repository: rad-ui/ui
Length of output: 622
🏁 Script executed:
#!/bin/bash
# Search for DataList usage in stories or examples
fd -t f -e tsx -e ts . | xargs rg -l 'DataList' | head -20Repository: rad-ui/ui
Length of output: 528
🏁 Script executed:
#!/bin/bash
# Check if there are tests for DataList detach behavior
fd -t f \( -name '*.test.tsx' -o -name '*.spec.tsx' -o -name '*.stories.tsx' \) | xargs rg -l 'DataList' 2>/dev/nullRepository: rad-ui/ui
Length of output: 899
🏁 Script executed:
#!/bin/bash
# Check test file for detach usage
rg -n 'detach' src/components/ui/DataList/tests/DataList.test.tsxRepository: rad-ui/ui
Length of output: 35
🏁 Script executed:
#!/bin/bash
# Check stories file for detach examples
rg -n 'detach' src/components/ui/DataList/stories/DataList.stories.tsxRepository: rad-ui/ui
Length of output: 35
🏁 Script executed:
#!/bin/bash
# Read the child component implementations to confirm the pattern
cat src/components/ui/DataList/fragments/DataListValue.tsxRepository: rad-ui/ui
Length of output: 706
🏁 Script executed:
#!/bin/bash
# Check the README or docs for DataList component purpose and detach usage
fd -t f \( -name 'README*' -o -name '*.md' \) . | xargs rg -l 'DataList' | head -5Repository: rad-ui/ui
Length of output: 573
🏁 Script executed:
#!/bin/bash
# Look at all child components to see the pattern consistency
cat src/components/ui/DataList/fragments/DataListLabel.tsx src/components/ui/DataList/fragments/DataListItem.tsxRepository: rad-ui/ui
Length of output: 1355
🏁 Script executed:
#!/bin/bash
# Check if detach is exported or documented in the public API
rg -n 'detach' src/components/ui/DataList/DataList.tsxRepository: rad-ui/ui
Length of output: 35
Malformed CSS class names when detach=true is used.
The implementation produces invalid class names. When detach=true, customClassSwitcher returns an empty string, causing child components to generate malformed classes like -value, -label, -item (with leading hyphens). This occurs because all child components use ${rootClass}-suffix interpolation without handling the empty rootClass case.
Since detach is not used in tests, stories, or the public API, either:
- Remove the unused
detachproperty entirely, or - Fix child components to handle empty
rootClass(e.g.,${rootClass ? rootClass + '-' : ''}value), or - Modify
customClassSwitcherto return a sensible default instead of empty string whendetach=true.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/ui/DataList/fragments/DataListRoot.tsx` around lines 11 - 15,
The DataListRoot component's detach prop causes customClassSwitcher to return an
empty rootClass which makes child components that build classes like
`${rootClass}-value`, `${rootClass}-label`, `${rootClass}-item` produce
malformed names (e.g., `-value`); fix by either removing the unused detach prop
from DataListRoot and its type, or updating one of two spots: (A) change
customClassSwitcher so when detach=true it returns a sensible default base class
(not an empty string), or (B) update child components that reference rootClass
(the templates producing `${rootClass}-...`) to guard against empty rootClass by
using a prefix expression such as `${rootClass ? rootClass + '-' : ''}value` so
no leading hyphen is produced. Ensure references include DataListRoot, detach,
customClassSwitcher and the child class interpolation sites.
| const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach); | ||
|
|
||
| return <StepsContext.Provider value={{ currentStep, setCurrentStep, rootClass, orientation }}> | ||
| <Primitive.div className={clsx(rootClass, className, `${rootClass}-${orientation}`)} data-orientation={orientation} {...props}>{children}</Primitive.div> |
There was a problem hiding this comment.
Empty rootClass produces malformed class names when detach=true.
When detach=true, rootClass becomes '', causing:
- Line 37:
`${rootClass}-${orientation}`→-horizontalor-vertical - Child
StepItem(from context snippet):`${rootClass}-item`→-item
These are unintended CSS class names rather than the "no class output" behavior requested in issue #1513. The detach flag or empty-rootClass check needs to propagate to child fragments and this template usage.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/ui/Steps/fragments/StepRoot.tsx` around lines 34 - 37,
rootClass can be an empty string when detach=true which produces malformed class
names like "-horizontal"; update the JSX and context so suffixes are only added
when rootClass is truthy and propagate the detach state to children: in the
component that computes rootClass (using customClassSwitcher and COMPONENT_NAME)
compute conditional classes (e.g., orientationClass = rootClass ?
`${rootClass}-${orientation}` : '' and itemClass = rootClass ?
`${rootClass}-item` : '') and pass either the boolean detach or the computed
rootClass/itemClass into StepsContext.Provider (value: { currentStep,
setCurrentStep, rootClass, orientation, detachOrItemClass }) so child fragments
(e.g., StepItem) can avoid rendering `${rootClass}-item` when rootClass is
empty. Ensure all template usages of `${rootClass}-...` check rootClass
truthiness before concatenation.
| value, ...props | ||
| }, ref) => { | ||
| const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME); | ||
| const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach); |
There was a problem hiding this comment.
TabNav detach is incomplete for child slots (-link leak).
Line 26 can produce empty rootClass, but children derive classes from context using ${rootClass}-link (src/components/ui/TabNav/fragments/TabNavLink.tsx, Line 45). That results in -link when detached.
Please propagate detach-aware slot class construction (e.g., pass detach in context or pass precomputed safe slot classes) so descendants output no generated classes in detach mode.
Also applies to: 48-52
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/ui/TabNav/fragments/TabNavRoot.tsx` at line 26, The child slot
class (`${rootClass}-link`) can become `-link` when rootClass is empty in detach
mode; fix by making TabNavRoot provide detach-aware class info to descendants:
compute safe slot classes (e.g., slotClasses.link = detach ? '' :
`${rootClass}-link`) in TabNavRoot (where rootClass is created via
customClassSwitcher and COMPONENT_NAME) and pass either the detach flag or the
precomputed slotClasses through the existing context/provider so TabNavLink (and
other fragments) use the context value instead of deriving `${rootClass}-link`
themselves; ensure the same change is applied to the other slot constructions
mentioned around the later block (48-52) so no generated classes are emitted in
detach mode.
| const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach); | ||
| const context = React.useMemo(() => ({ rootClass, orientation, dir }), [rootClass, orientation, dir]); |
There was a problem hiding this comment.
Toolbar detach currently produces leftover -button class names.
detach sets rootClass to empty, but child fragments suffix it (${rootClass}-button), which still emits generated-style classes and breaks the detach contract.
Suggested fix direction
// src/components/ui/Toolbar/fragments/ToolbarRoot.tsx
- const context = React.useMemo(() => ({ rootClass, orientation, dir }), [rootClass, orientation, dir]);
+ const context = React.useMemo(() => ({ rootClass, detach, orientation, dir }), [rootClass, detach, orientation, dir]);// src/components/ui/Toolbar/fragments/ToolbarButton.tsx
- className={clsx(`${rootClass}-button`, className)}
+ className={clsx(detach ? undefined : `${rootClass}-button`, className)}Use the same guard in other Toolbar subcomponents that suffix rootClass.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/ui/Toolbar/fragments/ToolbarRoot.tsx` around lines 35 - 36,
rootClass can be empty when detach is true, but child fragments that build
classes by suffixing `${rootClass}-button` still emit leftover "-button"
classes; update the child fragments that suffix rootClass to guard against falsy
rootClass (use the same detach check used in
customClassSwitcher/customRootClass/COMPONENT_NAME logic) so they only append
`-button` (or other suffixes) when rootClass is truthy—locate usages that
reference rootClass in toolbar subcomponents and change them to compute suffix
classes only if rootClass is non-empty.
| detach?: boolean; | ||
| 'aria-label'?: string; | ||
| 'aria-labelledby'?: string; | ||
| } & ComponentPropsWithoutRef<typeof Primitive.div>; | ||
|
|
||
| const TreeRoot = forwardRef<TreeRootElement, TreeRootProps>(({ children, className = '', customRootClass = '', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }, ref) => { | ||
| const TreeRoot = forwardRef<TreeRootElement, TreeRootProps>(({ children, className = '', customRootClass = '', detach = false, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }, ref) => { | ||
| const treeRef = useRef<TreeRootElement>(null); | ||
| useImperativeHandle(ref, () => treeRef.current as TreeRootElement); | ||
| const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME); | ||
| const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME, detach); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for string interpolation patterns with rootClass in Tree fragments
rg -n '\$\{rootClass\}' src/components/ui/Tree/Repository: rad-ui/ui
Length of output: 170
🏁 Script executed:
# Find customClassSwitcher function definition
rg -n 'customClassSwitcher' src/components/ui/Tree/ -B 2 -A 2Repository: rad-ui/ui
Length of output: 967
🏁 Script executed:
# Get full context of TreeItem.tsx around line 79
cat -n src/components/ui/Tree/fragments/TreeItem.tsx | sed -n '70,90p'Repository: rad-ui/ui
Length of output: 1083
🏁 Script executed:
# Find the customClassSwitcher function definition to understand its return value
fd -e ts -e tsx | xargs grep -l "customClassSwitcher" | head -5Repository: rad-ui/ui
Length of output: 324
🏁 Script executed:
# Find customClassSwitcher implementation in the core module
fd -e ts -e tsx -e js src/ | xargs grep -l "export.*customClassSwitcher" | head -1Repository: rad-ui/ui
Length of output: 374
🏁 Script executed:
# Also check the core directory structure
find src -type d -name "core" | head -3Repository: rad-ui/ui
Length of output: 61
🏁 Script executed:
# Find customClassSwitcher in src/core
rg -n "export.*customClassSwitcher" src/core/Repository: rad-ui/ui
Length of output: 301
🏁 Script executed:
# Or search for the function definition
rg -n "customClassSwitcher" src/core/ -A 10 | head -50Repository: rad-ui/ui
Length of output: 4389
🏁 Script executed:
# Check if there are other similar string interpolation patterns in Tree components
rg -n '\$\{rootClass\}-' src/components/ui/Tree/Repository: rad-ui/ui
Length of output: 170
Fix string interpolation pattern in TreeItem to handle empty rootClass when detached.
TreeItem.tsx:79 uses className={clsx(\${rootClass}-item`, className)}, which produces the unwanted class name -itemwhendetach=true(sincecustomClassSwitcherreturns an empty string). Use conditional logic instead:clsx(rootClass && `${rootClass}-item`, className)`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/ui/Tree/fragments/TreeRoot.tsx` around lines 16 - 24, The
TreeItem component currently interpolates rootClass into className as
clsx(`${rootClass}-item`, className), which yields "-item" when
customClassSwitcher returned an empty string (detach=true); update TreeItem
(component TreeItem) to guard the interpolation by using conditional logic so
the item class is only added when rootClass is truthy (e.g., use clsx(rootClass
&& `${rootClass}-item`, className)), referencing the rootClass produced in
TreeRoot via customClassSwitcher to avoid emitting a leading dash when detached.
| export const customClassSwitcher = (customRootClass: string = '', componentName: string = '', detach: boolean = false): string => { | ||
| if (detach) { | ||
| return ''; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find all child fragment components that use ${rootClass}- pattern
rg -n '\$\{rootClass\}-' --type=tsx --type=ts -g '!*.test.*' | head -50Repository: rad-ui/ui
Length of output: 80
🏁 Script executed:
#!/bin/bash
# First, let's look at the actual implementation
echo "=== customClassSwitcher implementation ==="
cat -n src/core/customClassSwitcher/index.ts
echo -e "\n=== Searching for ${rootClass}- pattern (corrected) ==="
# Search without specifying tsx type, just use ts and check all files
rg '\$\{rootClass\}-' --type=ts -g '!*.test.*' | head -60Repository: rad-ui/ui
Length of output: 9062
🏁 Script executed:
#!/bin/bash
echo "=== Examining AvatarGroupItem to see how rootClass is obtained ==="
cat -n src/components/ui/AvatarGroup/fragments/AvatarGroupItem.tsx
echo -e "\n=== Looking for context or hook that provides rootClass ==="
rg 'rootClass' src/components/ui/AvatarGroup/ --type=ts -B2 -A2 | head -80Repository: rad-ui/ui
Length of output: 5136
🏁 Script executed:
#!/bin/bash
echo "=== Checking AvatarGroupContext definition ==="
cat -n src/components/ui/AvatarGroup/contexts/AvatarGroupContext.tsx
echo -e "\n=== Verifying one more component type for consistency ==="
cat -n src/components/ui/Accordion/fragments/AccordionItem.tsxRepository: rad-ui/ui
Length of output: 3202
Returning empty string causes malformed class names in child components.
When detach=true, this returns '', but child components throughout the codebase use template literals like `${rootClass}-item`, `${rootClass}-trigger`, `${rootClass}-wrapper`, etc. This produces invalid class names such as -item, -trigger, -wrapper instead of no classes at all.
The detach parameter is not propagated through context to child components, so they cannot conditionally omit class assignments. Per issue #1513, the intent is to suppress class generation entirely. Consider either:
- Returning a sentinel value and updating child fragments to conditionally omit class attributes
- Propagating
detachthrough context so child components can skip class assignment entirely
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/core/customClassSwitcher/index.ts` around lines 7 - 10,
customClassSwitcher currently returns an empty string when detach=true which
leads to malformed child class names like `-item`; change the contract so
customClassSwitcher(customRootClass, componentName, detach) returns a clear
sentinel (e.g., null/undefined) when detach is requested and then update all
child fragments that use the returned value (places that build classes with
template literals such as `${rootClass}-item`, `${rootClass}-trigger`,
`${rootClass}-wrapper`) to conditionally omit the entire class attribute when
rootClass is the sentinel. Alternatively, if you prefer propagation, pass the
detach boolean through context (from where customClassSwitcher is called) so
child components can check detach and skip assigning class names instead of
relying on an empty string.
should close #1513
Summary by CodeRabbit
Release Notes
New Features
detachprop support across all UI components. When enabled, components will not apply Rad UI's generated CSS classes, allowing for complete styling customization.Tests
detachfunctionality.