Skip to content

Conversation

@ITBoomBKStudio
Copy link

@ITBoomBKStudio ITBoomBKStudio commented Oct 24, 2025

Closes #5778

📝 Description

Supersedes #5795.
Builds on the initial work by @IsDyh01 to address the incorrect return type in extendVariants, and additionally fixes long-standing typing issues with CompoundVariants (observed since early NextUI days).

This PR:

  • Ports and refines the extendVariants type fix.
  • Corrects type inference for CompoundVariants when composing/extending variants.

Credits:
Original approach by @IsDyh01 (commits preserved via cherry-pick / attribution).
Authored by @ITBoomBKStudio.

⚽️ Current behavior (updates)

  • extendVariants can yield an incorrect component return type, leading to type errors when using the extended variant API.
  • CompoundVariants types don’t consistently infer the resulting props when variants are composed or extended; edge cases mis-type or drop constraints, forcing manual casts.

🚀 New behavior

  • extendVariants now preserves and exposes the correct component/props type in the returned value.

  • CompoundVariants inference is stable when:

    • composing existing variants,
    • extending variant configs, and
    • combining multiple variant keys (no manual as needed).
  • Added unit tests covering:

    • return type of extendVariants,
    • inference across composed/extended compound variants,
    • representative edge cases seen in real projects.

No runtime changes; all changes are type-level and test-only.

💣 Is this a breaking change (Yes/No):

No.
The change improves type inference without removing or renaming public APIs. Existing correct usages continue to type-check. In cases where code previously relied on loose/incorrect inference, the compiler now catches mistakes earlier (desired tightening, not a breaking API change).

📝 Additional Information

  • Motivation: This aligns Typescript inference with real-world usage patterns for extended/compound variants; it removes long-standing friction that required casts/workarounds.
  • Attribution: Built on @IsDyh01’s PR (fix(extendVariants): return component type error #5795). Happy to adjust structure (split PRs or tweak tests) per maintainer preference.

Summary by CodeRabbit

  • Refactor
    • Redesigned type-level variant system to be slot-aware and more expressive, with updated public signatures for variant helpers.
  • New Features
    • Exported slot-aware variant prop shape and expanded public API to accept richer default/compound variant forms.
  • Bug Fixes
    • More robust slot-detection logic for varied inputs.
  • Tests
    • Test helpers accept optional style input and cover compound-variant propagation to slots.
  • Chores
    • Added changesets preparing releases (major and minor).

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link

changeset-bot bot commented Oct 24, 2025

🦋 Changeset detected

Latest commit: 9f38de7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 37 packages
Name Type
@heroui/system-rsc Major
@heroui/code Patch
@heroui/divider Patch
@heroui/kbd Patch
@heroui/spacer Patch
@heroui/spinner Patch
@heroui/system Patch
@heroui/react Patch
@heroui/accordion Patch
@heroui/listbox Patch
@heroui/menu Patch
@heroui/table Patch
@heroui/button Patch
@heroui/select Patch
@heroui/toast Patch
@heroui/alert Patch
@heroui/autocomplete Patch
@heroui/calendar Patch
@heroui/checkbox Patch
@heroui/date-input Patch
@heroui/date-picker Patch
@heroui/drawer Patch
@heroui/dropdown Patch
@heroui/form Patch
@heroui/input-otp Patch
@heroui/input Patch
@heroui/modal Patch
@heroui/navbar Patch
@heroui/number-input Patch
@heroui/popover Patch
@heroui/radio Patch
@heroui/slider Patch
@heroui/snippet Patch
@heroui/tabs Patch
@heroui/tooltip Patch
@heroui/aria-utils Patch
@heroui/framer-utils Patch

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

@vercel
Copy link

vercel bot commented Oct 24, 2025

@ITBoomBKStudio is attempting to deploy a commit to the HeroUI Inc Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 24, 2025

Walkthrough

Reworks variant typings to add slot-awareness (new generic S) across Variants, DefaultVariants, CompoundVariants and ExtendVariants; updates extendVariants runtime getSlots to an explicit traversal; exports ExtendVariantWithSlotsProps and updates tests and changesets accordingly. (48 words)

Changes

Cohort / File(s) Summary
Type definition refinement
packages/core/system-rsc/src/extend-variants.d.ts
Introduces generic S across Variants, GetSuggestedValues, SuggestedVariants, DefaultVariants, CompoundVariants, and the ExtendVariants signature; changes keys to use GetSuggestedValues<S> and updates return render-function props mapping to combine CP keys and V keys via StringToBoolean.
Runtime slot extraction
packages/core/system-rsc/src/extend-variants.js
Replaces reduce/flatMap getSlots implementation with explicit nested for-of loops and stricter guards; builds a null-prototype accumulator of slot names, ignores invalid shapes (arrays, non-objects, String objects), and returns {} for invalid input.
Tests & exports
packages/core/system-rsc/__tests__/extend-variants.test.tsx
Exports ExtendVariantWithSlotsProps; createExtendSlotsComponent now accepts styles: ExtendVariantWithSlotsProps = {} and uses styles?.compoundVariants ?? [...] defaults; adds/adjusts tests to assert compound-variant styles for components with slots.
Release metadata
.changeset/nine-apes-remain.md, .changeset/four-guests-sneeze.md
Adds changesets documenting type-inference and slot-extraction fixes for @heroui/system-rsc (major and minor entries).

Sequence Diagram(s)

sequenceDiagram
  participant Dev as Developer (calls extendVariants)
  participant TS as Type system (compile-time)
  participant Runtime as extendVariants runtime
  participant Component as Extended component

  Note over Dev,TS: compile-time type flow (new S generic)
  Dev->>TS: supply CP, V, SV, CV, S
  TS-->>Dev: validate CV extends CompoundVariants<V,SV,ComponentSlots<CP>> and infer props using GetSuggestedValues<S>

  Note over Dev,Runtime: runtime slot extraction
  Dev->>Runtime: pass variant groups/configs
  Runtime-->>Dev: traverse groups/configs -> return null-proto map of slot names

  Dev->>Component: create extended component (typed + runtime slots)
  Component-->>Dev: component with slot-aware compoundVariants
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Pay special attention to inference interactions between GetSuggestedValues<S>, ClassProp, and CompoundVariants<V,SV,S>.
  • Review the new props mapping in ExtendVariants to ensure existing call sites still type-check.
  • Verify getSlots handles edge-case variant shapes (String objects, arrays, nested non-objects) covered by tests.

Possibly related PRs

Suggested reviewers

  • jrgarciadev
  • wingkwong

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: correcting type issues with extendVariants and compound variants, which aligns with the PR's primary objective.
Description check ✅ Passed The description includes required sections (closes issue, current behavior, new behavior, breaking changes, additional info) and provides comprehensive context about the type fixes and test coverage.
Linked Issues check ✅ Passed The PR addresses issue #5778 by fixing TypeScript inference so custom variants created with extendVariants are visible to the compiler, and corrects type inference for CompoundVariants.
Out of Scope Changes check ✅ Passed All changes directly support the stated objectives: TypeScript type fixes for extendVariants and CompoundVariants, related test file updates, and changeset documentation. No unrelated modifications detected.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5bbef15 and 9f38de7.

📒 Files selected for processing (2)
  • packages/core/system-rsc/src/extend-variants.d.ts (5 hunks)
  • packages/core/system-rsc/src/extend-variants.js (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-25T17:08:46.283Z
Learnt from: adbjo
Repo: heroui-inc/heroui PR: 5846
File: packages/components/tabs/src/tabs.tsx:156-157
Timestamp: 2025-10-25T17:08:46.283Z
Learning: In packages/components/tabs/src/tabs.tsx, the renderTabs useMemo dependency array intentionally includes `variant` and `isVertical` to prevent potential side-effects, even though they might appear redundant based on static analysis.

Applied to files:

  • packages/core/system-rsc/src/extend-variants.d.ts
🔇 Additional comments (3)
packages/core/system-rsc/src/extend-variants.js (1)

7-38: getSlots traversal is defensive and aligned with slot extraction needs

The new implementation cleanly walks the variant shape, skips non-object/array-ish configs, and uses a null-prototype accumulator. It should robustly derive slot names from variant configs without introducing runtime edge cases.

packages/core/system-rsc/src/extend-variants.d.ts (2)

19-50: Slots-aware value helpers and compound/default variants look consistent

GetSuggestedValues<S> and its use in Variants, SuggestedVariants, DefaultVariants, and CompoundVariants correctly model both plain class values and per-slot maps, while gating slot support on S being defined. Likewise, widening ExtendVariantWithSlotsProps["defaultVariants"] to string | Record<string, string> aligns with these helpers and matches how slot-based defaults and compound variants are authored in practice. I don’t see type-safety gaps here.

Also applies to: 72-76


85-113: ExtendVariants generics and mapped return props preserve base props while enriching variants

The new ExtendVariants signature cleanly threads CP, S, V, SV, DV, and CV, and returning _heroui_system.InternalForwardRefRenderFunction<C, …> ensures the extended component keeps the base component’s props/ref behavior. The mapped props type that unions CP[K] with StringToBoolean<keyof NonNullable<V[K]>> (excluding "ref" | "as") gives the extended component both original props and correctly inferred variant keys. This shape matches the stated goals around extendVariants/compoundVariants inference.


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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/core/system-rsc/src/extend-variants.d.ts (2)

51-54: CompoundVariants slots support looks good; consider a small readability tweak.

You can express the value type via the existing helper to reduce repetition.

Apply:

 type CompoundVariants<V, SV, S> = Array<
   VariantValue<V, SV> &
-    ClassProp<S extends undefined ? ClassValue : ClassValue | SlotsClassValue<S>>
+    ClassProp<ClassValue | GetSuggestedValues<S>>
 >;

98-99: Use the already-bound S instead of recomputing ComponentSlots.

Keeps generics consistent and may help inference.

-    CV extends CompoundVariants<V, SV, ComponentSlots<CP>>,
+    CV extends CompoundVariants<V, SV, S>,
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 328c57d and 3858ffd.

📒 Files selected for processing (1)
  • packages/core/system-rsc/src/extend-variants.d.ts (3 hunks)

Replaces the conditional ClassProp logic with a simpler,
consistent form to fix incorrect slot value inference.

Before:
  ClassProp<S extends undefined ? ClassValue : ClassValue | SlotsClassValue<S>>

After:
  ClassProp<ClassValue | GetSuggestedValues<S>>

This ensures GetSuggestedValues<S> is used for slot-aware variants
and avoids duplicated conditional branches for undefined slots.
@ITBoomBKStudio
Copy link
Author

Actionable comments posted: 1

🧹 Nitpick comments (2)

packages/core/system-rsc/src/extend-variants.d.ts (2)> 51-54: CompoundVariants slots support looks good; consider a small readability tweak.

You can express the value type via the existing helper to reduce repetition.
Apply:

 type CompoundVariants<V, SV, S> = Array<
   VariantValue<V, SV> &
-    ClassProp<S extends undefined ? ClassValue : ClassValue | SlotsClassValue<S>>
+    ClassProp<ClassValue | GetSuggestedValues<S>>
 >;

98-99: Use the already-bound S instead of recomputing ComponentSlots.
Keeps generics consistent and may help inference.

-    CV extends CompoundVariants<V, SV, ComponentSlots<CP>>,
+    CV extends CompoundVariants<V, SV, S>,

📜 Review details

I’m keeping CV extends CompoundVariants<V, SV, ComponentSlots<CP>> on purpose.
Using S here narrows slot suggestions and validation to only the user-provided slots, while the extended component should expose all base component slots. ComponentSlots reflects the full slot surface of the base component and keeps compoundVariants authoring ergonomic and type-safe.

@ITBoomBKStudio ITBoomBKStudio changed the title Fix/compound variants types [BUG] - extendVariants Fix/compound variants types Oct 24, 2025
@ITBoomBKStudio ITBoomBKStudio changed the title [BUG] - extendVariants Fix/compound variants types extendVariants Fix/compound variants types Oct 24, 2025
…NonNullable

Simplifies the return type of ExtendVariants to ensure no required props
are enforced at the HOC level. This aligns with the intended API contract
where extended components expose all props as optional.

- All keys (CP ∪ V) are optional
- Preserve CP type hints and booleanized V values
- Added NonNullable<V[key]> guard to prevent undefined indexing
@ITBoomBKStudio
Copy link
Author

Added the final compoundVariants test - all cases now covered and passing
image

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6336fa8 and b770b54.

📒 Files selected for processing (1)
  • packages/core/system-rsc/__tests__/extend-variants.test.tsx (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/core/system-rsc/__tests__/extend-variants.test.tsx (4)
packages/core/system-rsc/src/extend-variants.js (1)
  • styles (105-105)
packages/core/system-rsc/src/extend-variants.d.ts (1)
  • ExtendVariantWithSlotsProps (76-80)
packages/core/system/src/index.ts (1)
  • ExtendVariantWithSlotsProps (16-16)
packages/core/system-rsc/src/index.ts (1)
  • ExtendVariantWithSlotsProps (29-29)
🔇 Additional comments (3)
packages/core/system-rsc/__tests__/extend-variants.test.tsx (3)

1-1: LGTM!

The import of ExtendVariantWithSlotsProps is necessary to support the updated type signature for the slots-based test helper and aligns with the PR's objective to expose slots-aware compound variant types.


40-40: LGTM!

The parameter type change from ExtendVariantProps to ExtendVariantWithSlotsProps correctly reflects that slots-based components require compound variants to support per-slot class definitions (e.g., class: { header: "..." }), which is demonstrated in the new test at lines 274-276.


264-292: Well-structured test for slots-aware compound variants.

This test effectively validates both forms of compound variant classes:

  • String class applied to the base slot (line 270)
  • Record class applied to specific slots like header (lines 274-276)

The test aligns well with the PR objective to ensure proper type inference for slots-aware compound variants.

However, the same concern applies here: both compound variants reference radius: "md" (lines 269, 273), which is not defined in the extended radius variants. If this is intentional to test compound variants referencing base component variants not overridden in the extension, consider adding a comment explaining this edge case.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Oct 25, 2025

Open in StackBlitz

@heroui/accordion

npm i https://pkg.pr.new/@heroui/accordion@5847

@heroui/alert

npm i https://pkg.pr.new/@heroui/alert@5847

@heroui/autocomplete

npm i https://pkg.pr.new/@heroui/autocomplete@5847

@heroui/avatar

npm i https://pkg.pr.new/@heroui/avatar@5847

@heroui/badge

npm i https://pkg.pr.new/@heroui/badge@5847

@heroui/breadcrumbs

npm i https://pkg.pr.new/@heroui/breadcrumbs@5847

@heroui/button

npm i https://pkg.pr.new/@heroui/button@5847

@heroui/calendar

npm i https://pkg.pr.new/@heroui/calendar@5847

@heroui/card

npm i https://pkg.pr.new/@heroui/card@5847

@heroui/checkbox

npm i https://pkg.pr.new/@heroui/checkbox@5847

@heroui/chip

npm i https://pkg.pr.new/@heroui/chip@5847

@heroui/code

npm i https://pkg.pr.new/@heroui/code@5847

@heroui/date-input

npm i https://pkg.pr.new/@heroui/date-input@5847

@heroui/date-picker

npm i https://pkg.pr.new/@heroui/date-picker@5847

@heroui/divider

npm i https://pkg.pr.new/@heroui/divider@5847

@heroui/drawer

npm i https://pkg.pr.new/@heroui/drawer@5847

@heroui/dropdown

npm i https://pkg.pr.new/@heroui/dropdown@5847

@heroui/form

npm i https://pkg.pr.new/@heroui/form@5847

@heroui/image

npm i https://pkg.pr.new/@heroui/image@5847

@heroui/input

npm i https://pkg.pr.new/@heroui/input@5847

@heroui/input-otp

npm i https://pkg.pr.new/@heroui/input-otp@5847

@heroui/kbd

npm i https://pkg.pr.new/@heroui/kbd@5847

@heroui/link

npm i https://pkg.pr.new/@heroui/link@5847

@heroui/listbox

npm i https://pkg.pr.new/@heroui/listbox@5847

@heroui/menu

npm i https://pkg.pr.new/@heroui/menu@5847

@heroui/modal

npm i https://pkg.pr.new/@heroui/modal@5847

@heroui/navbar

npm i https://pkg.pr.new/@heroui/navbar@5847

@heroui/number-input

npm i https://pkg.pr.new/@heroui/number-input@5847

@heroui/pagination

npm i https://pkg.pr.new/@heroui/pagination@5847

@heroui/popover

npm i https://pkg.pr.new/@heroui/popover@5847

@heroui/progress

npm i https://pkg.pr.new/@heroui/progress@5847

@heroui/radio

npm i https://pkg.pr.new/@heroui/radio@5847

@heroui/ripple

npm i https://pkg.pr.new/@heroui/ripple@5847

@heroui/scroll-shadow

npm i https://pkg.pr.new/@heroui/scroll-shadow@5847

@heroui/select

npm i https://pkg.pr.new/@heroui/select@5847

@heroui/skeleton

npm i https://pkg.pr.new/@heroui/skeleton@5847

@heroui/slider

npm i https://pkg.pr.new/@heroui/slider@5847

@heroui/snippet

npm i https://pkg.pr.new/@heroui/snippet@5847

@heroui/spacer

npm i https://pkg.pr.new/@heroui/spacer@5847

@heroui/spinner

npm i https://pkg.pr.new/@heroui/spinner@5847

@heroui/switch

npm i https://pkg.pr.new/@heroui/switch@5847

@heroui/table

npm i https://pkg.pr.new/@heroui/table@5847

@heroui/tabs

npm i https://pkg.pr.new/@heroui/tabs@5847

@heroui/toast

npm i https://pkg.pr.new/@heroui/toast@5847

@heroui/tooltip

npm i https://pkg.pr.new/@heroui/tooltip@5847

@heroui/user

npm i https://pkg.pr.new/@heroui/user@5847

@heroui/react

npm i https://pkg.pr.new/@heroui/react@5847

@heroui/system

npm i https://pkg.pr.new/@heroui/system@5847

@heroui/system-rsc

npm i https://pkg.pr.new/@heroui/system-rsc@5847

@heroui/theme

npm i https://pkg.pr.new/@heroui/theme@5847

@heroui/use-aria-accordion

npm i https://pkg.pr.new/@heroui/use-aria-accordion@5847

@heroui/use-aria-accordion-item

npm i https://pkg.pr.new/@heroui/use-aria-accordion-item@5847

@heroui/use-aria-button

npm i https://pkg.pr.new/@heroui/use-aria-button@5847

@heroui/use-aria-link

npm i https://pkg.pr.new/@heroui/use-aria-link@5847

@heroui/use-aria-modal-overlay

npm i https://pkg.pr.new/@heroui/use-aria-modal-overlay@5847

@heroui/use-aria-multiselect

npm i https://pkg.pr.new/@heroui/use-aria-multiselect@5847

@heroui/use-aria-overlay

npm i https://pkg.pr.new/@heroui/use-aria-overlay@5847

@heroui/use-callback-ref

npm i https://pkg.pr.new/@heroui/use-callback-ref@5847

@heroui/use-clipboard

npm i https://pkg.pr.new/@heroui/use-clipboard@5847

@heroui/use-data-scroll-overflow

npm i https://pkg.pr.new/@heroui/use-data-scroll-overflow@5847

@heroui/use-disclosure

npm i https://pkg.pr.new/@heroui/use-disclosure@5847

@heroui/use-draggable

npm i https://pkg.pr.new/@heroui/use-draggable@5847

@heroui/use-form-reset

npm i https://pkg.pr.new/@heroui/use-form-reset@5847

@heroui/use-image

npm i https://pkg.pr.new/@heroui/use-image@5847

@heroui/use-infinite-scroll

npm i https://pkg.pr.new/@heroui/use-infinite-scroll@5847

@heroui/use-intersection-observer

npm i https://pkg.pr.new/@heroui/use-intersection-observer@5847

@heroui/use-is-mobile

npm i https://pkg.pr.new/@heroui/use-is-mobile@5847

@heroui/use-is-mounted

npm i https://pkg.pr.new/@heroui/use-is-mounted@5847

@heroui/use-measure

npm i https://pkg.pr.new/@heroui/use-measure@5847

@heroui/use-pagination

npm i https://pkg.pr.new/@heroui/use-pagination@5847

@heroui/use-real-shape

npm i https://pkg.pr.new/@heroui/use-real-shape@5847

@heroui/use-ref-state

npm i https://pkg.pr.new/@heroui/use-ref-state@5847

@heroui/use-resize

npm i https://pkg.pr.new/@heroui/use-resize@5847

@heroui/use-safe-layout-effect

npm i https://pkg.pr.new/@heroui/use-safe-layout-effect@5847

@heroui/use-scroll-position

npm i https://pkg.pr.new/@heroui/use-scroll-position@5847

@heroui/use-ssr

npm i https://pkg.pr.new/@heroui/use-ssr@5847

@heroui/use-theme

npm i https://pkg.pr.new/@heroui/use-theme@5847

@heroui/use-update-effect

npm i https://pkg.pr.new/@heroui/use-update-effect@5847

@heroui/use-viewport-size

npm i https://pkg.pr.new/@heroui/use-viewport-size@5847

@heroui/aria-utils

npm i https://pkg.pr.new/@heroui/aria-utils@5847

@heroui/dom-animation

npm i https://pkg.pr.new/@heroui/dom-animation@5847

@heroui/framer-utils

npm i https://pkg.pr.new/@heroui/framer-utils@5847

@heroui/react-rsc-utils

npm i https://pkg.pr.new/@heroui/react-rsc-utils@5847

@heroui/react-utils

npm i https://pkg.pr.new/@heroui/react-utils@5847

@heroui/shared-icons

npm i https://pkg.pr.new/@heroui/shared-icons@5847

@heroui/shared-utils

npm i https://pkg.pr.new/@heroui/shared-utils@5847

@heroui/stories-utils

npm i https://pkg.pr.new/@heroui/stories-utils@5847

@heroui/test-utils

npm i https://pkg.pr.new/@heroui/test-utils@5847

commit: 9f38de7

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/core/system-rsc/__tests__/extend-variants.test.tsx (1)

76-88: Default compound variant fix addresses previous review.

The change from radius: "md" to radius: "none" correctly uses a value from the extended variant set. The fallback logic with styles?.compoundVariants ?? [...] is clean and well-structured.

Optional: Consider adding test coverage for the first compound variant.

The default compound variant at lines 77-81 (shadow: "none" + radius: "none") is never explicitly tested. The test at line 270-284 uses radius: "sm", so only the second compound variant (lines 82-87) is exercised. Adding a test case with radius="none" shadow="none" would verify that the fix for the previous review comment works correctly.

Example test:

test("should apply compound variant when shadow and radius are none", () => {
  const Card2 = createExtendSlotsComponent();
  const {getByTestId} = render(
    <Card2 radius="none" shadow="none">
      Card Content
    </Card2>,
  );
  const baseEl = getByTestId("base");
  expect(baseEl).toHaveClass("rounded-sm"); // from compound variant, overriding radius="none"'s "rounded-xs"
});
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b770b54 and 0021c8f.

📒 Files selected for processing (1)
  • packages/core/system-rsc/__tests__/extend-variants.test.tsx (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/core/system-rsc/__tests__/extend-variants.test.tsx (2)
packages/core/system-rsc/src/extend-variants.js (1)
  • styles (105-105)
packages/core/system-rsc/src/extend-variants.d.ts (1)
  • ExtendVariantWithSlotsProps (76-80)
🔇 Additional comments (3)
packages/core/system-rsc/__tests__/extend-variants.test.tsx (3)

1-1: LGTM! Import addition aligns with slots-aware type improvements.

The new ExtendVariantWithSlotsProps type is correctly imported and used on line 40 to support compound variants with slot-specific styling.


40-40: Correct type change for slots-aware compound variants.

Using ExtendVariantWithSlotsProps enables the test helper to accept compound variants with slot-specific class values, matching the updated type system.


270-314: Excellent test coverage for compound variants.

The two new test cases correctly verify:

  1. Default compound variants work alongside regular variants (lines 270-284)
  2. Custom compound variants passed via styles parameter override defaults (lines 286-314)

The tests validate both base-level and slot-specific compound variant application, ensuring the slots-aware type improvements function correctly at runtime.

@ITBoomBKStudio ITBoomBKStudio changed the title extendVariants Fix/compound variants types fix(system-rsc): extendVariants Fix/compound variants types Oct 26, 2025
Replace PropsWithoutRef with explicit Exclude<'ref'> in mapped keys and
intersect with RefAttributes<InferRef<C>>. This prevents @types/react’s
internal UNDEFINED_VOID_ONLY from leaking into the public .d.ts and fixes
declaration emit for components like extended Autocomplete.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/core/system-rsc/src/extend-variants.d.ts (1)

103-107: Verify that making all keys optional is the intended behavior.

The current mapped type uses ?: on line 104, making all keys optional—including those that may be required in the base component props CP. A past review raised this concern and suggested preserving CP's original optionality by separating CP keys (which would retain their required/optional status) from V-only keys (which would be optional).

That review was marked as addressed by updating tests to provide required props. If the intention is for extended components to always have optional props, the current implementation is correct. However, if base component contracts should be preserved (required props remain required), consider this alternative:

{
  // CP keys preserve their original optionality
  [K in keyof CP]: K extends keyof V
    ? CP[K] | StringToBoolean<keyof NonNullable<V[K]>>
    : CP[K]
} & {
  // V-only keys are optional
  [K in Exclude<keyof V, keyof CP>]?: StringToBoolean<keyof NonNullable<V[K]>>
} & RefAttributes<InferRef<C>>

This would maintain stricter type-safety by preventing accidental omission of required base props while still allowing variant props to be optional.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 73032ce and 8baeab2.

📒 Files selected for processing (1)
  • packages/core/system-rsc/src/extend-variants.d.ts (4 hunks)
🔇 Additional comments (3)
packages/core/system-rsc/src/extend-variants.d.ts (3)

2-2: LGTM! Missing import added.

The JSXElementConstructor import is correctly added and used in the ComponentProps helper (line 12) and ExtendVariants constraint (line 86).


46-48: Slots-aware compound variants implemented correctly.

The addition of the S generic parameter and use of GetSuggestedValues<S> properly extends compound variants to support both plain class values and slot-keyed class values, maintaining backward compatibility when slots are undefined.


92-92: Appropriate use of ComponentSlots<CP> for broader type coverage.

Using ComponentSlots<CP> instead of the bound generic S ensures compound variants have access to the full slot surface of the base component, maintaining type-safety and authoring ergonomics as noted in the PR objectives.

@ITBoomBKStudio
Copy link
Author

Actionable comments posted: 0

🧹 Nitpick comments (1)

packages/core/system-rsc/src/extend-variants.d.ts (1)> 103-107: Verify that making all keys optional is the intended behavior.

The current mapped type uses ?: on line 104, making all keys optional—including those that may be required in the base component props CP. A past review raised this concern and suggested preserving CP's original optionality by separating CP keys (which would retain their required/optional status) from V-only keys (which would be optional).
That review was marked as addressed by updating tests to provide required props. If the intention is for extended components to always have optional props, the current implementation is correct. However, if base component contracts should be preserved (required props remain required), consider this alternative:

{
  // CP keys preserve their original optionality
  [K in keyof CP]: K extends keyof V
    ? CP[K] | StringToBoolean<keyof NonNullable<V[K]>>
    : CP[K]
} & {
  // V-only keys are optional
  [K in Exclude<keyof V, keyof CP>]?: StringToBoolean<keyof NonNullable<V[K]>>
} & RefAttributes<InferRef<C>>

This would maintain stricter type-safety by preventing accidental omission of required base props while still allowing variant props to be optional.

📜 Review details

Yes, this behavior is intentional for components created with extendVariants.
The goal is to make extended components easier to use - we don’t want developers to re-declare every required prop (like color, size, or variant for a Button) when composing or testing variants.

extendVariants is designed as a styling HOC, not a strict type-safe wrapper.
In this context, optional props provide flexibility and reduce boilerplate:
shorter JSX = better developer experience.

If we preserved CP’s required props, every extended component instance would force full prop specification, which defeats the purpose of variant composition.

@ITBoomBKStudio
Copy link
Author

Hey @jrgarciadev, both #5847 and #5848 are ready. They fix system-rsc type & slot inference issues. Would be great to get your review when you have a moment 👀

@jrgarciadev
Copy link
Member

@wingkwong please check this PR

@wingkwong wingkwong added this to the v2.8.6 milestone Oct 28, 2025
@wingkwong wingkwong self-assigned this Oct 29, 2025
@vercel
Copy link

vercel bot commented Oct 29, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
heroui Ready Ready Preview Comment Nov 30, 2025 2:48am
heroui-sb Ready Ready Preview Comment Nov 30, 2025 2:48am

Copy link
Member

@wingkwong wingkwong left a comment

Choose a reason for hiding this comment

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

  1. The changes you added in packages/core/system-rsc/__tests__/extend-variants.test.tsx don't really handle the reported issue. i.e If I just run those new tests in canary branch, they are all passed.
  2. Please merge your PR 5848 changes into this PR so that I could test them in one go.
  3. Please also handle the following cases.

The type error is on to.

export const CustomButton = extendVariants(Button, {});
<Button as={Link} to="/sign-in">
  Sign In
</Button>

The type error is on defaultVariants.classNames.tabList

const CustomTabs = extendVariants(Tabs, {
  slots: {
    tabList: 'bg-white p-0',
    base: 'w-full bg-white rounded-lg overflow-hidden',
    tab: 'py-6',
    tabContent: 'font-medium text-gray-800',
  },
  defaultVariants: {
    variant: 'underlined',
    color: 'primary',
    classNames: {
      tabList: 'bg-white p-0',
      base: 'w-full bg-white rounded-lg overflow-hidden',
      tab: 'py-6',
      tabContent: 'font-medium text-gray-800',
    },
  },
});

@@ -0,0 +1,5 @@
---
"@heroui/system-rsc": major
Copy link
Member

Choose a reason for hiding this comment

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

use patch instead of major

@wingkwong
Copy link
Member

@ITBoomBKStudio any updates?

@ITBoomBKStudio
Copy link
Author

@ITBoomBKStudio any updates?

Hey, I haven’t forgotten about this and I'm planning to wrap it up this week

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/core/system-rsc/src/extend-variants.js (1)

13-41: Consider simplifying the duplicate check.

The hasOwnProperty check on line 33 is redundant when using Object.create(null) since null-prototype objects have no inherited properties. You can simplify to:

       for (const slotName of Object.keys(config)) {
-        if (!Object.prototype.hasOwnProperty.call(acc, slotName)) {
+        if (!(slotName in acc)) {
           acc[slotName] = "";
         }
       }

Alternatively, since there's no prototype pollution risk, you could assign directly without checking:

acc[slotName] = "";

This would work correctly because reassigning the same key to "" is idempotent.


The overall traversal logic correctly implements the documented behavior: variants → groups → configs → slot names. The filtering for non-objects, arrays, and String objects ensures robust slot extraction.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d18110f and 5bbef15.

📒 Files selected for processing (2)
  • .changeset/four-guests-sneeze.md (1 hunks)
  • packages/core/system-rsc/src/extend-variants.js (1 hunks)
🔇 Additional comments (2)
packages/core/system-rsc/src/extend-variants.js (1)

7-12: LGTM: Clear JSDoc documentation.

The JSDoc accurately describes the traversal pattern and return type. The parameter and return descriptions provide helpful context for maintainers.

.changeset/four-guests-sneeze.md (1)

1-5: LGTM: Changeset correctly documents the fix.

The changeset appropriately marks this as a minor version bump and clearly describes the slot detection improvements. The description aligns with the implementation changes in getSlots().

@ITBoomBKStudio
Copy link
Author

Hey @wingkwong,

I just pushed an update:

  1. The reported issue was actually a type-level problem. The runtime behavior was already correct, so writing tests specifically for that case wouldn’t bring real value. The fix strictly addresses the incorrect type inference.

  2. I merged both PRs into a single one so the full set of related changes can be reviewed together.

  3. The edge cases are now handled. This part was genuinely tough and I explored several approaches and ended up with a compromise that works reliably, even though it means we lose some of the previous strictness in prop validation. Using _heroui_system.InternalForwardRefRenderFunction turned out to be the closest match to the intended behavior.

Happy to go through any part in detail if needed.

Copy link
Member

@wingkwong wingkwong left a comment

Choose a reason for hiding this comment

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

  1. please combine two changeset into one and use patch instead of minor or major
  2. those 2 cases mentioned in comment seems not resolved using this PR build.
  3. does this PR cover issue 4148? There's a few more related to extendVariants. Just wanna know which of them could be solved in this PR.

In case you need faster communication / discussion, please free feel to ping me at discord (same user id)

slots?: S;
},
opts?: Options,
) => _heroui_system.InternalForwardRefRenderFunction<
Copy link
Member

Choose a reason for hiding this comment

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

I think we may just do import type {InternalForwardRefRenderFunction} from "./types.js"; .

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.

[BUG] - extendVariants does not show variants

4 participants