Skip to content

Conversation

@adbjo
Copy link

@adbjo adbjo commented Oct 24, 2025

📝 Description

  • Make tab cursor shrink / grow along with selected tab
  • Do not render default size / position
  • Do not animate initial size / position

⛳️ Current behavior (updates)

  • Tab cursor does not shrink / grow along with selected tab (stays the same size until state update)
  • Renders default size before getting sized to match selected tab
  • initialization is animated (from default size to selected tab size)

🚀 New behavior

See description :)

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

No

📝 Additional Information

Summary by CodeRabbit

  • Bug Fixes

    • Cursor reliably repositions when the selected tab changes, when switching tab variants or orientation, and after layout or content changes.
  • Performance

    • More responsive cursor updates with reduced layout jank via optimized repositioning and resize observation.
  • UX

    • Cursor stays hidden until fully initialized; transitions start only after readiness to prevent flicker and improve visual stability.
  • Refactor

    • Rendering and update flow streamlined to ensure consistent cursor behavior across layouts.

@adbjo adbjo requested a review from jrgarciadev as a code owner October 24, 2025 20:17
@changeset-bot
Copy link

changeset-bot bot commented Oct 24, 2025

🦋 Changeset detected

Latest commit: f314565

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

This PR includes changesets to release 14 packages
Name Type
@heroui/tabs Patch
@heroui/theme Patch
@heroui/react Patch
@heroui/autocomplete Patch
@heroui/checkbox Patch
@heroui/date-input Patch
@heroui/date-picker Patch
@heroui/form Patch
@heroui/input-otp Patch
@heroui/input Patch
@heroui/number-input Patch
@heroui/radio Patch
@heroui/select Patch
@heroui/table 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

@adbjo 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 Tabs cursor to a DOM-ref-driven implementation: adds cursorRef, memoized getCursorStyles/updateCursorPosition, ResizeObserver to track selected tab, and an initialization/animation gate (data-[initialized] / data-[animated]) so CSS transitions start only after cursor placement. No public API/signature changes.

Changes

Cohort / File(s) Change Summary
Tabs component (cursor logic)
packages/components/tabs/src/tabs.tsx
Added cursorRef; added previousVariant and previousIsVertical refs; replaced inline cursor styling with memoized/useCallback getCursorStyles and updateCursorPosition; added withAnimationReset to coordinate animation gating; attached ResizeObserver to selected tab and added effects to observe/cleanup; render now conditionally mounts cursor span and attaches cursorRef, and uses data-[initialized]/data-[animated].
Theme cursor transitions
packages/core/theme/src/components/tabs.ts
Made cursor slot invisible by default; use data-[initialized=true] to show cursor and data-[animated=true] to enable transitions for left/top/width/height with defined duration/easing; removed unconditional transitions/visibility.
Changeset
.changeset/serious-eels-stare.md
Added patch changeset for @heroui/tabs and @heroui/theme with description "responsive tab cursor."

Sequence Diagram(s)

sequenceDiagram
    participant Tabs as Tabs component
    participant TabEl as Selected tab element (DOM)
    participant Cursor as cursorRef (span)
    participant RO as ResizeObserver
    participant rAF as requestAnimationFrame

    Tabs->>Cursor: render and attach ref (conditionally)
    Tabs->>TabEl: locate selected tab element
    Tabs->>RO: observe TabEl (on mount/selection)
    Note over TabEl,RO: layout or selection changes
    RO->>Tabs: notify on resize
    Tabs->>Tabs: updateCursorPosition(selectedTab)
    Tabs->>TabEl: getBoundingClientRect()
    Tabs->>Tabs: getCursorStyles(tabRect, variant, isVertical)
    Tabs->>Cursor: apply inline styles (left/top/width/height)
    Tabs->>rAF: schedule setting data-[initialized]=true / data-[animated]
    rAF->>Cursor: set data attributes to enable CSS transitions
    Note over Cursor: CSS transitions run only after data attributes set
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Potential focus areas:

  • ResizeObserver lifecycle and cleanup in the effect
  • Geometry calculations for "underlined" vs other variants and vertical mode
  • useCallback/useEffect dependency correctness to avoid stale closures or extra updates
  • rAF timing and interaction between data-[initialized] / data-[animated] and CSS transitions

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 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "fix(tabs): responsive resize cursor to match selected tab" directly aligns with the main objectives described in the PR. The title clearly communicates that the cursor is being made responsive to resize and match the selected tab, which is the primary focus of the changes as evidenced by the modifications to the tabs component that add ResizeObserver-based positioning, animation state management, and dynamic cursor styling. The title is concise, specific, and uses clear language that avoids vague terms, allowing a teammate reviewing the commit history to immediately understand the core change.
Description Check ✅ Passed The pull request description covers most of the required template sections: the Description section clearly lists the three main changes, the Current behavior section explains the prior issues, the Breaking change status is explicitly stated as "No", and the New behavior section references the description. The description is mostly complete and directly relevant to the PR objectives, though the "Closes #" issue reference is absent and the "Additional Information" section is empty. These missing sections are non-critical, and the core content sections provide sufficient information about the changes being made.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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: 3

🧹 Nitpick comments (4)
packages/components/tabs/src/tabs.tsx (4)

53-72: Consider making magic numbers configurable or documented.

The hardcoded values for the underlined variant (10% left offset, 80% width, 2px bottom offset) work but could be fragile if the design evolves. Consider extracting these as constants with descriptive names or making them configurable through props/theme.

Example refactor:

+const UNDERLINED_CURSOR_INSET = 0.1; // 10% horizontal inset
+const UNDERLINED_CURSOR_BOTTOM_OFFSET = 2; // px from bottom
+
 const getCursorStyles = useCallback(
   (tabRect: DOMRect) => {
     if (variant === "underlined") {
       return {
-        left: `${tabRect.left + tabRect.width * 0.1}px`,
-        top: `${tabRect.top + tabRect.height - 2}px`,
-        width: `${tabRect.width * 0.8}px`,
+        left: `${tabRect.left + tabRect.width * UNDERLINED_CURSOR_INSET}px`,
+        top: `${tabRect.top + tabRect.height - UNDERLINED_CURSOR_BOTTOM_OFFSET}px`,
+        width: `${tabRect.width * (1 - 2 * UNDERLINED_CURSOR_INSET)}px`,
         height: "",
       };
     }

84-89: Type casting to DOMRect is misleading.

The manually constructed object is missing many DOMRect properties (x, y, right, bottom, toJSON, etc.). While it works because getCursorStyles only uses the provided properties, the as DOMRect cast is technically incorrect and could be confusing.

Consider using a more accurate type:

+  type TabRect = Pick<DOMRect, 'width' | 'height' | 'left' | 'top'>;
+  
   const tabRect = {
     width: selectedTab.offsetWidth,
     height: selectedTab.offsetHeight,
     left: selectedTab.offsetLeft,
     top: selectedTab.offsetTop,
-  } as DOMRect;
+  } as TabRect;

And update getCursorStyles signature:

-const getCursorStyles = useCallback(
-  (tabRect: DOMRect) => {
+const getCursorStyles = useCallback(
+  (tabRect: TabRect) => {

98-98: Add explanatory comment for requestAnimationFrame usage.

The requestAnimationFrame ensures the data-initialized attribute is set after the initial styles are applied, preventing unwanted animation on first render. This pattern could benefit from a brief comment for future maintainers.

+  // Set initialized flag after paint to prevent animating from default position
   requestAnimationFrame(() => cursorRef.current?.setAttribute("data-initialized", "true"));

108-112: Consider reusing ResizeObserver for better performance.

Creating a new ResizeObserver instance on every selectedKey change works but could be optimized. Consider maintaining a single observer and updating what it observes, or verify if the current approach causes any performance issues with rapid tab switching.

Example optimization (if needed):

useEffect(() => {
  const observer = new ResizeObserver(updateCursorPosition);
  
  return () => observer.disconnect();
}, [updateCursorPosition]);

useEffect(() => {
  const selectedTab = domRef.current?.querySelector(`[data-key="${selectedKey}"]`);
  if (!selectedTab || !observerRef.current) return;
  
  observerRef.current.disconnect();
  observerRef.current.observe(selectedTab);
}, [domRef, selectedKey]);
📜 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 1d99d04.

📒 Files selected for processing (2)
  • .changeset/swift-kiwis-knock.md (1 hunks)
  • packages/components/tabs/src/tabs.tsx (3 hunks)
🔇 Additional comments (5)
.changeset/swift-kiwis-knock.md (1)

1-5: LGTM!

The changeset correctly documents a minor version bump with an appropriate description that aligns with the feature enhancement.

packages/components/tabs/src/tabs.tsx (4)

4-4: LGTM!

The additional React hooks (useEffect, useCallback) are appropriately imported for the new cursor positioning logic.


47-47: LGTM!

The cursorRef is properly declared to manage the cursor element.


121-127: Excellent solution for preventing initial animation flash!

The className logic using [&:not([data-initialized])] modifiers effectively hides the cursor and disables transitions until it's properly positioned, preventing the visual issue of animating from a default position on initialization. This directly addresses one of the key objectives mentioned in the PR description.


103-113: No action required—data-key attribute is reliably set on Tab components.

The data-key attribute used in the selector is explicitly set in the Tab component (packages/components/tabs/src/tab.tsx: data-key={key}), confirming the querySelector will work correctly. The useEffect hook dependencies and ResizeObserver cleanup logic are sound.

@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@5846

@heroui/alert

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

@heroui/autocomplete

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

@heroui/avatar

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

@heroui/badge

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

@heroui/breadcrumbs

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

@heroui/button

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

@heroui/calendar

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

@heroui/card

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

@heroui/checkbox

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

@heroui/chip

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

@heroui/code

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

@heroui/date-input

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

@heroui/date-picker

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

@heroui/divider

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

@heroui/drawer

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

@heroui/dropdown

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

@heroui/form

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

@heroui/image

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

@heroui/input

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

@heroui/input-otp

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

@heroui/kbd

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

@heroui/link

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

@heroui/listbox

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

@heroui/menu

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

@heroui/modal

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

@heroui/navbar

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

@heroui/number-input

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

@heroui/pagination

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

@heroui/popover

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

@heroui/progress

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

@heroui/radio

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

@heroui/ripple

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

@heroui/scroll-shadow

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

@heroui/select

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

@heroui/skeleton

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

@heroui/slider

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

@heroui/snippet

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

@heroui/spacer

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

@heroui/spinner

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

@heroui/switch

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

@heroui/table

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

@heroui/tabs

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

@heroui/toast

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

@heroui/tooltip

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

@heroui/user

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

@heroui/react

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

@heroui/system

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

@heroui/system-rsc

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

@heroui/theme

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

@heroui/use-aria-accordion

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

@heroui/use-aria-accordion-item

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

@heroui/use-aria-button

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

@heroui/use-aria-link

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

@heroui/use-aria-modal-overlay

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

@heroui/use-aria-multiselect

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

@heroui/use-aria-overlay

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

@heroui/use-callback-ref

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

@heroui/use-clipboard

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

@heroui/use-data-scroll-overflow

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

@heroui/use-disclosure

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

@heroui/use-draggable

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

@heroui/use-form-reset

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

@heroui/use-image

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

@heroui/use-infinite-scroll

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

@heroui/use-intersection-observer

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

@heroui/use-is-mobile

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

@heroui/use-is-mounted

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

@heroui/use-measure

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

@heroui/use-pagination

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

@heroui/use-real-shape

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

@heroui/use-ref-state

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

@heroui/use-resize

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

@heroui/use-safe-layout-effect

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

@heroui/use-scroll-position

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

@heroui/use-ssr

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

@heroui/use-theme

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

@heroui/use-update-effect

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

@heroui/use-viewport-size

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

@heroui/aria-utils

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

@heroui/dom-animation

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

@heroui/framer-utils

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

@heroui/react-rsc-utils

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

@heroui/react-utils

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

@heroui/shared-icons

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

@heroui/shared-utils

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

@heroui/stories-utils

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

@heroui/test-utils

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

commit: f314565

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.

please also check coderabbitai comments.

@adbjo adbjo requested a review from wingkwong October 25, 2025 17:16
@adbjo
Copy link
Author

adbjo commented Oct 27, 2025

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 27, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@vercel
Copy link

vercel bot commented Oct 27, 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 3, 2025 4:45am
heroui-sb Ready Ready Preview Comment Nov 3, 2025 4:45am

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 share before & after videos for demonstration.
  2. In Vertical storybook, if you change the variant from solid to underlined, there is slight transition which is not expected. (you may compare with the production storybook).

@adbjo
Copy link
Author

adbjo commented Oct 27, 2025

  1. Please share before & after videos for demonstration.
  2. In Vertical storybook, if you change the variant from solid to underlined, there is slight transition which is not expected. (you may compare with the production storybook).

Before:
Screencast from 2025-10-27 18-55-28.webm

After:
Screencast from 2025-10-27 18-57-00.webm

  1. Fixed! I assume you meant disabling animation when changing variant

@adbjo adbjo requested a review from wingkwong October 27, 2025 19:11
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/components/tabs/src/tabs.tsx (1)

80-85: Consider defining a proper type for tab rectangle.

The object literal on lines 80-85 only has four properties but is cast as DOMRect. This type assertion bypasses type checking, since DOMRect includes many additional properties (x, y, right, bottom, toJSON, etc.). While this works because getCursorStyles only accesses these four properties, it's a type safety smell.

Consider defining a custom type:

+type TabRect = {
+  width: number;
+  height: number;
+  left: number;
+  top: number;
+};
+
-const getCursorStyles = useCallback(
-  (tabRect: DOMRect) => {
+const getCursorStyles = useCallback(
+  (tabRect: TabRect) => {
    // ...
  },
  [variant],
);

// ...

const updateCursorPosition = useCallback(
  (selectedTab: HTMLElement) => {
    if (!cursorRef.current) return;

    const tabRect = {
      width: selectedTab.offsetWidth,
      height: selectedTab.offsetHeight,
      left: selectedTab.offsetLeft,
      top: selectedTab.offsetTop,
-   } as DOMRect;
+   };
📜 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 c3783d4 and 3b7471a.

📒 Files selected for processing (1)
  • packages/components/tabs/src/tabs.tsx (3 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: adbjo
PR: heroui-inc/heroui#5846
File: packages/components/tabs/src/tabs.tsx:155-155
Timestamp: 2025-10-25T17:11:59.319Z
Learning: In packages/components/tabs/src/tabs.tsx, the renderTabs useMemo dependency array intentionally includes both `domRef` and `cursorRef` to maintain consistency in how ref objects are handled in dependency arrays, even though ref objects have stable identity across renders.
📚 Learning: 2025-10-25T17:11:59.319Z
Learnt from: adbjo
PR: heroui-inc/heroui#5846
File: packages/components/tabs/src/tabs.tsx:155-155
Timestamp: 2025-10-25T17:11:59.319Z
Learning: In packages/components/tabs/src/tabs.tsx, the renderTabs useMemo dependency array intentionally includes both `domRef` and `cursorRef` to maintain consistency in how ref objects are handled in dependency arrays, even though ref objects have stable identity across renders.

Applied to files:

  • packages/components/tabs/src/tabs.tsx
📚 Learning: 2025-10-25T17:08:46.274Z
Learnt from: adbjo
PR: heroui-inc/heroui#5846
File: packages/components/tabs/src/tabs.tsx:156-157
Timestamp: 2025-10-25T17:08:46.274Z
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/components/tabs/src/tabs.tsx
🔇 Additional comments (7)
packages/components/tabs/src/tabs.tsx (7)

4-4: LGTM!

The new imports and ref declarations are appropriate for the ref-driven cursor positioning approach.

Also applies to: 48-48, 51-53


55-74: LGTM!

The cursor positioning math is correct. For the underlined variant, the 10% offset and 80% width properly center the cursor with margins on each side.


103-113: LGTM!

The contentRect check correctly guards against positioning the cursor for tabs that haven't rendered content yet. Based on learnings, this approach was intentionally chosen to handle tabs with only padding/borders but no content.


115-125: LGTM!

The effect correctly wires the ResizeObserver to the selected tab, with proper cleanup to prevent memory leaks. When selectedKey changes, the observer is recreated for the new tab, and the initial observation fires immediately to position the cursor.


133-133: LGTM!

The cursorRef is correctly attached to the cursor span, enabling direct style updates without triggering React re-renders.


167-167: Dependency array consistency maintained.

As per learnings, cursorRef is intentionally included to maintain consistency with how domRef is handled in dependency arrays, even though ref objects have stable identity across renders.


168-169: Dependency array safety maintained.

As per learnings, variant and isVertical are intentionally included to prevent potential side-effects, even though static analysis might suggest they're redundant.

@adbjo adbjo force-pushed the fix/tabs_resize_cursor_with_selected_tab branch from 879c3ac to 8f16d30 Compare October 27, 2025 20:30
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: 3

📜 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 879c3ac and 8f16d30.

📒 Files selected for processing (1)
  • packages/components/tabs/src/tabs.tsx (3 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: adbjo
PR: heroui-inc/heroui#5846
File: packages/components/tabs/src/tabs.tsx:155-155
Timestamp: 2025-10-25T17:11:59.319Z
Learning: In packages/components/tabs/src/tabs.tsx, the renderTabs useMemo dependency array intentionally includes both `domRef` and `cursorRef` to maintain consistency in how ref objects are handled in dependency arrays, even though ref objects have stable identity across renders.
📚 Learning: 2025-10-25T17:11:59.319Z
Learnt from: adbjo
PR: heroui-inc/heroui#5846
File: packages/components/tabs/src/tabs.tsx:155-155
Timestamp: 2025-10-25T17:11:59.319Z
Learning: In packages/components/tabs/src/tabs.tsx, the renderTabs useMemo dependency array intentionally includes both `domRef` and `cursorRef` to maintain consistency in how ref objects are handled in dependency arrays, even though ref objects have stable identity across renders.

Applied to files:

  • packages/components/tabs/src/tabs.tsx
📚 Learning: 2025-10-25T17:08:46.274Z
Learnt from: adbjo
PR: heroui-inc/heroui#5846
File: packages/components/tabs/src/tabs.tsx:156-157
Timestamp: 2025-10-25T17:08:46.274Z
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/components/tabs/src/tabs.tsx
🔇 Additional comments (7)
packages/components/tabs/src/tabs.tsx (7)

4-4: LGTM!

The added imports are necessary for the new cursor positioning implementation.


48-53: LGTM!

The ref declarations and state extractions are clean and necessary for the cursor positioning logic.


55-74: LGTM!

The cursor style calculation correctly handles both underlined and other variants, with appropriate memoization.


103-113: LGTM!

The ResizeObserver callback correctly handles tab content resizing and is properly memoized.


133-133: LGTM!

Correctly attaches the cursor ref to enable the new positioning logic.


167-169: LGTM!

The dependency array correctly includes cursorRef for consistency with the existing domRef pattern, as well as variant and isVertical to guard against potential side-effects.

Based on learnings


98-98: Add null check for unmount safety.

The requestAnimationFrame callback doesn't verify that cursorRef.current still exists before calling setAttribute. If the component unmounts between scheduling and execution, this could cause an error.

Apply this diff:

-      requestAnimationFrame(() => cursorRef.current?.setAttribute("data-initialized", "true"));
+      requestAnimationFrame(() => {
+        if (cursorRef.current) {
+          cursorRef.current.setAttribute("data-initialized", "true");
+        }
+      });

Likely an incorrect or invalid review comment.

@adbjo
Copy link
Author

adbjo commented Oct 28, 2025

  1. Please share before & after videos for demonstration.
  2. In Vertical storybook, if you change the variant from solid to underlined, there is slight transition which is not expected. (you may compare with the production storybook).

Before: Screencast from 2025-10-27 18-55-28.webm

After: Screencast from 2025-10-27 18-57-00.webm

  1. Fixed! I assume you meant disabling animation when changing variant

Toggling isVertical had same issue as variant, fixed it now too

@adbjo
Copy link
Author

adbjo commented Oct 30, 2025

  1. Please share before & after videos for demonstration.
  2. In Vertical storybook, if you change the variant from solid to underlined, there is slight transition which is not expected. (you may compare with the production storybook).

Before: Screencast from 2025-10-27 18-55-28.webm
After: Screencast from 2025-10-27 18-57-00.webm

  1. Fixed! I assume you meant disabling animation when changing variant

Toggling isVertical had same issue as variant, fixed it now too

Also saw that there was unwanted blinking when toggling between variants and isVertical. Added another fix for this, final final fix

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 (2)
packages/components/tabs/src/tabs.tsx (2)

56-75: Consider extracting magic numbers for the underlined variant positioning.

The calculations on lines 60-62 use magic numbers (0.1, 0.8, -2) that could be more maintainable as named constants. This would make the positioning logic clearer and easier to adjust.

Example:

+  const UNDERLINED_INSET_RATIO = 0.1;
+  const UNDERLINED_WIDTH_RATIO = 0.8;
+  const UNDERLINED_VERTICAL_OFFSET = 2;
+
   const getCursorStyles = useCallback(
     (tabRect: DOMRect) => {
       if (variant === "underlined") {
         return {
-          left: `${tabRect.left + tabRect.width * 0.1}px`,
-          top: `${tabRect.top + tabRect.height - 2}px`,
-          width: `${tabRect.width * 0.8}px`,
+          left: `${tabRect.left + tabRect.width * UNDERLINED_INSET_RATIO}px`,
+          top: `${tabRect.top + tabRect.height - UNDERLINED_VERTICAL_OFFSET}px`,
+          width: `${tabRect.width * UNDERLINED_WIDTH_RATIO}px`,
           height: "",
         };
       }

93-114: Consider improving type safety for the tabRect object.

Line 102 asserts a plain object as DOMRect, which is not fully type-safe. While this works at runtime since only the four properties are used, it would be clearer to define a custom type or use a partial type.

+type TabRect = {
+  width: number;
+  height: number;
+  left: number;
+  top: number;
+};
+
 const updateCursorPosition = useCallback(
   (selectedTab: HTMLElement) => {
     if (!cursorRef.current) return;

     const tabRect = {
       width: selectedTab.offsetWidth,
       height: selectedTab.offsetHeight,
       left: selectedTab.offsetLeft,
       top: selectedTab.offsetTop,
-    } as DOMRect;
+    } as TabRect;

     const styles = getCursorStyles(tabRect);

Also update the getCursorStyles signature to accept TabRect instead of DOMRect.

Based on learnings

📜 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 d2f97e0 and f314565.

📒 Files selected for processing (2)
  • packages/components/tabs/src/tabs.tsx (3 hunks)
  • packages/core/theme/src/components/tabs.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/theme/src/components/tabs.ts
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: adbjo
PR: heroui-inc/heroui#5846
File: packages/components/tabs/src/tabs.tsx:76-101
Timestamp: 2025-10-27T21:48:35.272Z
Learning: In packages/components/tabs/src/tabs.tsx, the updateCursorPosition useCallback dependency array intentionally includes `cursorRef.current` to handle the case where the cursor span element is unmounted and remounted (e.g., when `disableAnimation` or `disableCursorAnimation` toggles). This ensures the callback is recreated when the ref points to a new element, triggering a dependency chain that re-establishes the ResizeObserver and initializes the new cursor element with the data-initialized attribute.
</learning]
Learnt from: adbjo
PR: heroui-inc/heroui#5846
File: packages/components/tabs/src/tabs.tsx:155-155
Timestamp: 2025-10-25T17:11:59.319Z
Learning: In packages/components/tabs/src/tabs.tsx, the renderTabs useMemo dependency array intentionally includes both `domRef` and `cursorRef` to maintain consistency in how ref objects are handled in dependency arrays, even though ref objects have stable identity across renders.
Learnt from: adbjo
PR: heroui-inc/heroui#5846
File: packages/components/tabs/src/tabs.tsx:115-125
Timestamp: 2025-10-27T21:52:33.304Z
Learning: In packages/components/tabs/src/tabs.tsx, the useEffect dependency array at line 125 intentionally uses `domRef.current` rather than `domRef` because domRef.current can change between renders (when React sets it during the commit phase), whereas domRef itself has stable identity and won't change.
📚 Learning: 2025-10-27T21:48:35.272Z
Learnt from: adbjo
PR: heroui-inc/heroui#5846
File: packages/components/tabs/src/tabs.tsx:76-101
Timestamp: 2025-10-27T21:48:35.272Z
Learning: In packages/components/tabs/src/tabs.tsx, the updateCursorPosition useCallback dependency array intentionally includes `cursorRef.current` to handle the case where the cursor span element is unmounted and remounted (e.g., when `disableAnimation` or `disableCursorAnimation` toggles). This ensures the callback is recreated when the ref points to a new element, triggering a dependency chain that re-establishes the ResizeObserver and initializes the new cursor element with the data-initialized attribute.
</learning]

Applied to files:

  • packages/components/tabs/src/tabs.tsx
📚 Learning: 2025-10-25T17:11:59.319Z
Learnt from: adbjo
PR: heroui-inc/heroui#5846
File: packages/components/tabs/src/tabs.tsx:155-155
Timestamp: 2025-10-25T17:11:59.319Z
Learning: In packages/components/tabs/src/tabs.tsx, the renderTabs useMemo dependency array intentionally includes both `domRef` and `cursorRef` to maintain consistency in how ref objects are handled in dependency arrays, even though ref objects have stable identity across renders.

Applied to files:

  • packages/components/tabs/src/tabs.tsx
📚 Learning: 2025-10-25T17:08:46.274Z
Learnt from: adbjo
PR: heroui-inc/heroui#5846
File: packages/components/tabs/src/tabs.tsx:156-157
Timestamp: 2025-10-25T17:08:46.274Z
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/components/tabs/src/tabs.tsx
📚 Learning: 2025-10-27T21:52:33.304Z
Learnt from: adbjo
PR: heroui-inc/heroui#5846
File: packages/components/tabs/src/tabs.tsx:115-125
Timestamp: 2025-10-27T21:52:33.304Z
Learning: In packages/components/tabs/src/tabs.tsx, the useEffect dependency array at line 125 intentionally uses `domRef.current` rather than `domRef` because domRef.current can change between renders (when React sets it during the commit phase), whereas domRef itself has stable identity and won't change.

Applied to files:

  • packages/components/tabs/src/tabs.tsx
🔇 Additional comments (5)
packages/components/tabs/src/tabs.tsx (5)

4-4: LGTM!

The additional React hooks imports are necessary and correctly added for the new cursor positioning and animation logic.


47-54: LGTM!

The ref declarations and state tracking setup are well-structured. The previousVariant and previousIsVertical refs enable proper animation reset detection, and the cursor ref enables direct DOM manipulation for performant positioning.


77-91: LGTM!

The animation reset wrapper correctly prevents transition animations when variant or orientation changes. Using requestAnimationFrame to re-add the attributes ensures proper timing after layout updates.


116-138: LGTM!

The ResizeObserver implementation correctly handles cursor repositioning when the selected tab's dimensions change. The effect properly observes the currently selected tab and cleans up the observer on unmount or when the selection changes.

The contentRect check on line 121 correctly guards against positioning updates before the tab has rendered content, as discussed in previous reviews.

Based on learnings


140-184: LGTM!

The cursor span correctly receives the cursorRef for DOM manipulation, and the dependency array includes all necessary values. The conditional rendering ensures the cursor only appears when animations are enabled and a tab is selected.

Based on learnings

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.

2 participants