Skip to content

fix(system-rsc): extendVariants rendering behavior with as#6215

Merged
wingkwong merged 3 commits into
heroui-inc:canaryfrom
ITBoomBKStudio:fix/extendVariants_as_rendering
Feb 13, 2026
Merged

fix(system-rsc): extendVariants rendering behavior with as#6215
wingkwong merged 3 commits into
heroui-inc:canaryfrom
ITBoomBKStudio:fix/extendVariants_as_rendering

Conversation

@ITBoomBKStudio
Copy link
Copy Markdown

⚽️ Current behavior (updates)

When using the as prop with components created via extendVariants, the component was rendered as the value passed to as instead of rendering as the original base component.
This caused components to lose the original rendering semantics and styles.

🚀 New behavior

Components created with extendVariants now always render as the original base component.
The as prop no longer changes the rendered element, restoring the expected rendering behavior.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 8, 2026

🦋 Changeset detected

Latest commit: 3c0c1a2

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 Patch
@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/button Patch
@heroui/select Patch
@heroui/table 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
Copy Markdown

vercel Bot commented Feb 8, 2026

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

A member of the Team first needs to authorize it.

@jrgarciadev
Copy link
Copy Markdown
Member

@wingkwong please check

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Feb 8, 2026

Open in StackBlitz

@heroui/accordion

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

@heroui/alert

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

@heroui/autocomplete

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

@heroui/avatar

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

@heroui/badge

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

@heroui/breadcrumbs

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

@heroui/button

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

@heroui/calendar

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

@heroui/card

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

@heroui/checkbox

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

@heroui/chip

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

@heroui/code

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

@heroui/date-input

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

@heroui/date-picker

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

@heroui/divider

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

@heroui/drawer

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

@heroui/dropdown

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

@heroui/form

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

@heroui/image

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

@heroui/input

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

@heroui/input-otp

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

@heroui/kbd

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

@heroui/link

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

@heroui/listbox

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

@heroui/menu

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

@heroui/modal

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

@heroui/navbar

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

@heroui/number-input

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

@heroui/pagination

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

@heroui/popover

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

@heroui/progress

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

@heroui/radio

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

@heroui/ripple

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

@heroui/scroll-shadow

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

@heroui/select

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

@heroui/skeleton

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

@heroui/slider

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

@heroui/snippet

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

@heroui/spacer

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

@heroui/spinner

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

@heroui/switch

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

@heroui/table

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

@heroui/tabs

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

@heroui/toast

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

@heroui/tooltip

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

@heroui/user

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

@heroui/react

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

@heroui/system

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

@heroui/system-rsc

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

@heroui/theme

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

@heroui/use-aria-accordion

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

@heroui/use-aria-accordion-item

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

@heroui/use-aria-button

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

@heroui/use-aria-link

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

@heroui/use-aria-modal-overlay

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

@heroui/use-aria-multiselect

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

@heroui/use-aria-overlay

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

@heroui/use-callback-ref

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

@heroui/use-clipboard

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

@heroui/use-data-scroll-overflow

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

@heroui/use-disclosure

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

@heroui/use-draggable

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

@heroui/use-form-reset

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

@heroui/use-image

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

@heroui/use-infinite-scroll

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

@heroui/use-intersection-observer

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

@heroui/use-is-mobile

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

@heroui/use-is-mounted

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

@heroui/use-measure

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

@heroui/use-pagination

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

@heroui/use-real-shape

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

@heroui/use-ref-state

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

@heroui/use-resize

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

@heroui/use-safe-layout-effect

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

@heroui/use-scroll-position

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

@heroui/use-ssr

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

@heroui/use-theme

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

@heroui/use-update-effect

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

@heroui/use-viewport-size

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

@heroui/aria-utils

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

@heroui/dom-animation

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

@heroui/framer-utils

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

@heroui/react-rsc-utils

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

@heroui/react-utils

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

@heroui/shared-icons

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

@heroui/shared-utils

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

@heroui/stories-utils

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

@heroui/test-utils

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

commit: 3c0c1a2

@ITBoomBKStudio
Copy link
Copy Markdown
Author

Hey @jrgarciadev and @wingkwong 👋

I ran into a confusing inconsistency with extendVariants and ForwardedComponent.

Depending on how the as prop is handled, the behavior differs between environments:

  • One implementation works correctly in production but fails in Jest tests
  • The other passes Jest tests but breaks rendering and styling in production

Below are the two versions that demonstrate the issue.

Version that works in production

const ForwardedComponent = React.forwardRef((originalProps = {}, ref) => {
  const newProps = React.useMemo(
    () =>
      getClassNamesWithProps(
        {
          slots,
          variants,
          compoundVariants,
          props: originalProps,
          defaultVariants,
          hasSlots,
          opts,
        },
        [originalProps],
      ),
    [originalProps],
  );

  return React.createElement(BaseComponent, {
    ...originalProps,
    ...newProps,
    ref,
  });
});

Version that works in Jest tests

const ForwardedComponent = React.forwardRef((originalProps = {}, ref) => {
  // Extract `as` prop if present
  const { as: Component = BaseComponent, ...restProps } = originalProps;

  const newProps = React.useMemo(
    () =>
      getClassNamesWithProps(
        {
          slots,
          variants,
          compoundVariants,
          props: restProps,
          defaultVariants,
          hasSlots,
          opts,
        },
        [restProps],
      ),
    [restProps],
  );

  return React.createElement(Component, {
    ...restProps,
    ...newProps,
    ref,
  });
});

In the second version, components render as the value passed to as instead of rendering as the original base component, which causes styling and rendering issues in production.

This PR restores consistent rendering behavior by ensuring components created with extendVariants always render as the original base component, while still allowing as to be used in the styling logic.

@ITBoomBKStudio
Copy link
Copy Markdown
Author

Summary

I’ve completely rewritten the test file to better reflect the actual behavior and responsibilities of extendVariants.

In the process, I also fixed a serious issue with chained extendVariants inheritance, where compound variants and slot styles could not reliably reference variants defined in base components. With this fix, variant matching and slot resolution now behave consistently across multiple extension levels.

As a result, production behavior and test expectations are now aligned and predictable.

Some additional cleanup may still be needed in related files, but the core logic and tests are now in a good and stable state.

@ITBoomBKStudio
Copy link
Copy Markdown
Author

@jrgarciadev @wingkwong
It would be really appreciated if this PR could be released as a hotfix.

At the moment, this issue causes broken behavior in one of my projects, and having this fix available sooner would help a lot.

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 9, 2026

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

Project Deployment Actions Updated (UTC)
heroui Ready Ready Preview, Comment Feb 12, 2026 2:33pm
heroui-sb Ready Ready Preview, Comment Feb 12, 2026 2:33pm

Request Review

@wingkwong wingkwong merged commit e1b3453 into heroui-inc:canary Feb 13, 2026
10 checks passed
jrgarciadev added a commit that referenced this pull request Feb 18, 2026
* fix(theme): hide password reveal button (#5990)

* fix(link): link overriding role (#5999)

* fix(link): allow overriding role

* chore(link): changeset

* chore(deps): upgrade react-aria (v1.14.0) (#5996)

* chore(deps): upgrade react-aria (v1.14.0)

* refactor(react): group client components

* fix(dropdown): keyDown test cases

* chore(react): rollback

* fix(theme): default transition-duration (#6011)

* fix(theme): default transitionDuration

* chore(deps): bump @heroui/theme peer dep

* ci(changesets): version packages (#5991)

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>

* fix(use-image): resolve race condition with cached images on Firefox/Safari (#6041)

* fix(use-image): resolve race condition with cached images on Firefox/Safari

The bug occurred because event handlers (onload/onerror) were attached
AFTER setting the image src. For cached images, browsers fire onload
synchronously when src is set, causing the event to be missed and
images to remain stuck at opacity-0.

Changes:
- Attach handlers BEFORE setting src to catch synchronous callbacks
- Check both naturalWidth AND naturalHeight (per CodeRabbit review on #4523)
- Handle synchronous error callbacks for failed cached images
- Add comprehensive test coverage (15 tests) including:
  - Synchronous cached image success (Firefox/Safari race condition)
  - Synchronous cached image error
  - Dynamic src changes
  - All props (crossOrigin, srcSet, sizes, loading)
  - Callback invocation verification

Reproduction and investigation performed using Claude Opus 4.5.

Fixes #4534, #2259

* fix(use-image): add ignoreFallback to useCallback dependencies

Address CodeRabbit review feedback: the `ignoreFallback` prop was used
inside the `load` callback (line 104) but was missing from the dependency
array, creating a stale closure bug.

Without this fix, if `ignoreFallback` changes from `true` to `false`
dynamically, the `load` callback would retain the stale `true` value,
preventing the image from ever loading.

Changes:
- Add `ignoreFallback` to useCallback dependency array
- Add tests for dynamic `ignoreFallback` changes (both directions)
- Update changeset to document this fix

Verification performed using Claude Opus 4.5.

---------

Co-authored-by: Brian Meek <brian@current.space>

* fix(docs): broken links in Form page (#6077)

* fix(pagination): improve layout for large page counts (#6034)

* fix(pagination): improve layout for large page counts/style of paagination compnents

* fix(pagination): refine item sizing to balance small and large page numbers

* ci(changesets): add pagination sizing changeset

* fix(pagination): ensure cursor fully covers button without changing radius

* chore(changeset): revise message and add issue numbers

---------

Co-authored-by: WK Wong <wingkwong.code@gmail.com>

* fix(date-picker): open date-picker when clicking border (#6084)

* fix(date-picker): add pointer interaction to open date picker on wrapper click

* chore: add changeset

* chore(changeset): add issue number

---------

Co-authored-by: WK <wingkwong.code@gmail.com>

* fix(accordion): click behaviour for dynamically generated accordion items (#6133)

* fix(accordion): add collection state

* chore: add changeset

* fix: update change set

* refactor(theme): remove flat dependency (#6157)

* chore(deps): remove flat library

* refactor(theme): replace flatten from flat

* chore: add changeset

* fix(listbox): prevent option focus from start/end content slots (#6060)

* fix(listbox): prevent option focus from start/end content slots

* test: add test code about when clicking
non-interactive slot content, and prevented focus leakage from interactive
start/end content such as buttons.

* docs: add changeset

* chore(changeset): add issue number

---------

Co-authored-by: WK <wingkwong.code@gmail.com>

* fix(system-rsc): extendVariants & compound variants types (#5847)

* fix(extendVariants): return component type error

* fix(CompoundVariants): correct type inference for extended/compound variants and composition

* test: cover compound/extend inference; enforce CP required props

* fix(types): correct CompoundVariants class value inference

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.

* fix(system-rsc): correct slot detection in getSlots()

* fix(types): make ExtendVariants props optional and guard V[key] with 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

* test(extendVariants): add compoundVariants integration test

* fix(system-rsc): getSlots() brief JSDoc comment added

* test(extendVariants):  new styles - extended & fixed styles - original tests for slots component

* test(extendVariants): fixed slot component variant styles extended test

* fix(types): avoid leaking React internals by removing PropsWithoutRef

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.

* chore(changeset): add patch for extendVariants and CompoundVariants type fix

* chore(system-rsc): add changeset for getSlots() slot detection fix

* refactor(types): unify slot value inference via GetSuggestedValues<S> for consistent variant typing

* fix(extendVariants): improved as-prop handling and exclude classNames from SuggestedVariants

* fix(system-rsc): add polymorphic 'as' prop support to extendVariants

* chore(system-rsc): add missing tests

---------

Co-authored-by: doki- <1335902682@qq.com>
Co-authored-by: WK Wong <wingkwong.code@gmail.com>

* chore(docs): update meta (#6168)

* ci(changesets): version packages (#6059)

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>

* fix(system-rsc): extendVariants rendering behavior with as (#6215)

* fix(system-rsc): fix components rendering with 'as' prop

* fix(system-rsc): fix compoundVariants and slots inheritance in extendVariants

* fix(system-rsc): extendVariants test file cleaned

* chore(deps): bump RA dependencies (#6221)

* chore(deps): bump RA dependencies

* chore(date-picker): revise test cases

* fix(button): correct disableRipple prop precedence (#6199)

* ci(changesets): version packages (#6227)

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>

---------

Co-authored-by: Hayato Hasegawa <hase1225hayato@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>
Co-authored-by: brianatdetections <brian@detections.ai>
Co-authored-by: Brian Meek <brian@current.space>
Co-authored-by: Dominik Hryshaiev <domhryshaiev@gmail.com>
Co-authored-by: creative-atish <149860680+atishkr25@users.noreply.github.com>
Co-authored-by: KyZy7 <29321162+KyZy7@users.noreply.github.com>
Co-authored-by: Deepansh Bhargava <deepansh940@gmail.com>
Co-authored-by: KumJungMin <37934668+KumJungMin@users.noreply.github.com>
Co-authored-by: Bohdan Kulinchenko <35272606+ITBoomBKStudio@users.noreply.github.com>
Co-authored-by: doki- <1335902682@qq.com>
Co-authored-by: Chris Nowak <krzysztofmareknowak@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants