Skip to content

fix(headless): correct panel measurement and tabs indicator timing - #8897

Merged
alexcarpenter merged 4 commits into
mainfrom
carp/headless-measurement-gaps
Jun 18, 2026
Merged

fix(headless): correct panel measurement and tabs indicator timing#8897
alexcarpenter merged 4 commits into
mainfrom
carp/headless-measurement-gaps

Conversation

@alexcarpenter

@alexcarpenteralexcarpenter commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

Three correctness fixes in @clerk/headless measurement primitives, identified by comparing against base-ui's useCollapsiblePanel.

1. Reset flex/grid alignment before measuring

Accordion.Panel and Collapsible.Panel read scrollHeight/scrollWidth directly. With display: flex/grid and non-default justify-content / align-items / align-content / justify-items, alignment can make the scroll size report smaller than the true content size. A new resetLayoutStyles util forces those properties to initial before measuring and restores them on the next frame.

2. Flow at auto when idle, re-pin on close

Once the open animation settles, the panels now drop the measured pixel size (CSS vars omitted → height: auto) so content flows naturally, and re-pin a measured value when the close transition starts so exit animations still have a concrete value to animate from.

3. Tabs.Indicator measures before paint

Switched the indicator measurement from useEffect to useLayoutEffect to remove a one-frame stale indicator position on tab change. This required tracking the tab-list element as context state (not a useRef): a child's layout effect runs before its parent's ref is attached, so the prior useEffect only worked by accident (passive effects run after all refs attach). Same pattern base-ui uses.

Testing

  • @clerk/headless unit suite passes (404 tests), typecheck clean.

Summary by CodeRabbit

Summary

  • Bug Fixes

    • Improved accordion and collapsible panel open/close animations by refining how sizes are measured and restored across transitions.
    • Updated tabs indicator to reposition synchronously during tab changes to prevent brief misalignment.
  • Tests

    • Strengthened accordion and collapsible animation lifecycle tests to verify pinned dimensions during active transitions and clearing after completion.
  • Refactor

    • Refactored tabs internals to track the tabs list element via state and update indicator measurements accordingly.

- Accordion.Panel / Collapsible.Panel: reset flex/grid alignment before
reading scrollHeight/scrollWidth so non-default alignment can't truncate
the measured content size (new resetLayoutStyles util).
- Accordion.Panel / Collapsible.Panel: drop the pinned pixel size once the
open animation settles (panel flows at auto) and re-pin a measured value
when closing starts so exit transitions still animate.
- Tabs.Indicator: measure in useLayoutEffect (before paint) to remove a
one-frame stale position on tab change. Track the list element as context
state so the indicator's layout effect can read it (a child layout effect
runs before the parent ref attaches).
@changeset-bot

changeset-botBot commented Jun 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b33c1db

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

vercelBot commented Jun 17, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreview, CommentJun 18, 2026 1:57pm
swingsetReadyReadyPreview, CommentJun 18, 2026 1:57pm

Request Review

@coderabbitai

coderabbitaiBot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 99c52d0b-49ba-4306-a940-de48b05be979

📥 Commits

Reviewing files that changed from the base of the PR and between 17090d3 and a989491.

📒 Files selected for processing (1)
  • packages/headless/src/utils/reset-layout-styles.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/headless/src/utils/reset-layout-styles.ts

📝 Walkthrough

Walkthrough

Adds a resetLayoutStyles utility that temporarily forces flex/grid alignment properties to initial !important before scroll-dimension measurement and restores them on the next animation frame. Wires this into AccordionPanel and CollapsiblePanel to pin pixel heights during close transitions and clear them after open animations settle. Separately migrates the Tabs list element from a useRef to useState so TabsIndicator can measure synchronously with useLayoutEffect.

Changes

Panel Height Measurement & Animation Lifecycle

Layer / File(s)Summary
resetLayoutStyles utility
packages/headless/src/utils/reset-layout-styles.ts, packages/headless/src/utils/index.ts
New resetLayoutStyles(element) helper captures original inline alignment styles (justify-content, align-items, align-content, justify-items), forces them to initial !important for accurate scrollHeight/scrollWidth reads, restores originals on the next animation frame, and returns an immediate-cleanup callback. Re-exported from the utils index.
AccordionPanel measurement & transition lifecycle
packages/headless/src/primitives/accordion/accordion-panel.tsx
Integrates resetLayoutStyles into the ResizeObserver path to normalize flex/grid alignment before each height measurement. Captures transitionStatus and runOnAnimationsFinished to orchestrate clearing the pinned pixel height after open animations finish and re-pinning it when a close transition enters the 'ending' status. ResizeObserver cleanup now restores layout styles.
AccordionPanel animation lifecycle tests
packages/headless/src/primitives/accordion/accordion.test.tsx
Replaces the prior single assertion with two tests: one mocks Element.prototype.getAnimations to simulate an in-progress open animation and asserts --cl-accordion-panel-height is present; the other renders open without mocked animations and asserts the variable is absent after settling.
CollapsiblePanel measurement & transition lifecycle
packages/headless/src/primitives/collapsible/collapsible-panel.tsx
Same resetLayoutStyles + useAnimationsFinished integration as accordion, extended to both height and width dimensions. Clears pinned values after open animations settle, re-pins both axes when closing begins, and restores layout styles in ResizeObserver cleanup.
CollapsiblePanel animation lifecycle tests
packages/headless/src/primitives/collapsible/collapsible.test.tsx
Replaces prior assertions with two tests: one verifies both --collapsible-panel-height and --collapsible-panel-width are set while an open animation is mocked as pending; the other verifies both are absent from inline style once settled open.

Tabs List Ref → State Migration

Layer / File(s)Summary
TabsContextValue contract: ref → state
packages/headless/src/primitives/tabs/tabs-context.ts
TabsContextValue drops listRef: React.RefObject<HTMLElement> and adds listElement: HTMLElement | null plus setListElement: (element: HTMLElement | null) => void, with comments explaining why state (rather than a ref) is needed for measurement timing.
TabsRoot state + TabsList ref wiring
packages/headless/src/primitives/tabs/tabs-root.tsx, packages/headless/src/primitives/tabs/tabs-list.tsx
TabsRoot switches from useRef to useState for the list element, stores it in state, and provides both listElement and setListElement through the context provider with updated dependencies. TabsList passes setListElement as the Composite ref callback.
TabsIndicator synchronous measurement
packages/headless/src/primitives/tabs/tabs-indicator.tsx
Converts the indicator positioning effect from useEffect to useLayoutEffect and reads listElement directly from context instead of accessing listRef.current, eliminating the ref indirection and updating the dependency array to trigger on listElement changes.

Sequence Diagram(s)

sequenceDiagram
participant ResizeObserver
participant AccordionPanel
participant resetLayoutStyles
participant useAnimationsFinished
ResizeObserver->>AccordionPanel: observe size changes
AccordionPanel->>resetLayoutStyles: call with panel element
resetLayoutStyles->>resetLayoutStyles: capture align styles + priorities
resetLayoutStyles->>AccordionPanel: force align to initial !important
AccordionPanel->>AccordionPanel: read scrollHeight
AccordionPanel->>resetLayoutStyles: schedule restore on next frame
AccordionPanel->>AccordionPanel: set height = px value
Note over AccordionPanel,useAnimationsFinished: Open animation in progress
useAnimationsFinished->>AccordionPanel: animation finished
AccordionPanel->>AccordionPanel: clear height (undefined)
Note over AccordionPanel,useAnimationsFinished: User closes panel
AccordionPanel->>resetLayoutStyles: call with panel element (re-pin)
resetLayoutStyles->>AccordionPanel: force align to initial !important
AccordionPanel->>AccordionPanel: set height = px value
AccordionPanel->>AccordionPanel: play close animation
Loading
sequenceDiagram
participant TabsRoot
participant TabsContext
participant TabsList
participant TabsIndicator
TabsRoot->>TabsRoot: useState(listElement)
TabsRoot->>TabsContext: provide listElement + setListElement
TabsList->>TabsContext: consume setListElement
TabsList->>TabsList: pass setListElement to Composite ref
Note over TabsList,TabsIndicator: Tab list element mounted
TabsList->>TabsContext: setListElement(composite element)
TabsContext->>TabsIndicator: listElement updated
TabsIndicator->>TabsIndicator: useLayoutEffect observes listElement
TabsIndicator->>TabsIndicator: measure active tab bounding rect
TabsIndicator->>TabsIndicator: update indicator position sync
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • clerk/javascript#8475: Modifies accordion-panel.tsx and the --cl-accordion-panel-height CSS variable/testing, directly overlapping with the accordion animation lifecycle changes in this PR.
  • clerk/javascript#8474: Main PR updates to accordion-panel.tsx and collapsible-panel.tsx integrate the animation lifecycle via the useAnimationsFinished/useTransition APIs (including transitionStatus) that are foundational to this PR's implementation.

Suggested reviewers

  • wobsoriano
  • austincalvelage
  • Ephem

Poem

🐇 Hop, hop! The panel heights now know their place,
No frozen flex tricks left to leave a trace.
I measure true with resetLayoutStyles neat,
Then auto flows once open—oh so sweet!
The tabs list lives in state for sync delight,
And indicators snap in perfect sight. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 20.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the three main fixes: correcting panel measurement and improving tabs indicator timing, which are the primary changes across accordion, collapsible, and tabs components.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

@pkg-pr-new

pkg-pr-newBot commented Jun 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8897

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8897

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8897

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8897

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@8897

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8897

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8897

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8897

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8897

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8897

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8897

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8897

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8897

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8897

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8897

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8897

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8897

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8897

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8897

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8897

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8897

commit: b33c1db

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/headless/src/primitives/accordion/accordion.test.tsx (1)

194-238: ⚡ Quick win

Add a close-transition test for re-pinning height.

The updated suite validates open-path pin/clear behavior, but it still doesn’t assert the new close-path contract (re-pin measured height when closing starts). A focused test here would lock down the exact behavior this PR adds.

💡 Suggested test shape
+ it('re-pins --cl-accordion-panel-height when close transition starts', async () => {+ const user = userEvent.setup();+ renderAccordion({ defaultValue: ['item1'] });++ const original = (Element.prototype as { getAnimations?: unknown }).getAnimations;+ (Element.prototype as { getAnimations?: unknown }).getAnimations = () => [+ { finished: new Promise<void>(() => {}) },+ ];+ try {+ await user.click(screen.getByRole('button', { name: 'Section 1' }));+ const panel = document.querySelector('[data-cl-slot="accordion-panel"]') as HTMLElement;+ expect(panel.getAttribute('style') ?? '').toContain('--cl-accordion-panel-height');+ } finally {+ if (original) {+ (Element.prototype as { getAnimations?: unknown }).getAnimations = original;+ } else {+ delete (Element.prototype as { getAnimations?: unknown }).getAnimations;+ }+ }+ });

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx} should “Implement comprehensive testing including unit, integration, and E2E tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/headless/src/primitives/accordion/accordion.test.tsx` around lines
194 - 238, The animation lifecycle test suite validates the open-path behavior
for height pinning and clearing, but is missing a test case for the close-path
behavior. Add a new test within the 'animation lifecycle' describe block that
validates the height re-pinning behavior when closing starts. This test should
open an accordion panel (using defaultValue), then simulate closing it and
verify that the --cl-accordion-panel-height CSS variable is re-pinned to the
measured pixel height during the close animation phase, similar to the pattern
used in the existing 'pins --cl-accordion-panel-height while the open animation
is in progress' test case.

Source: Coding guidelines

packages/headless/src/primitives/collapsible/collapsible.test.tsx (1)

142-189: ⚡ Quick win

Add a close-transition test that asserts both dimensions are re-pinned.

This suite now covers open-path pin/clear behavior, but it doesn’t directly test the new close-path re-pin logic (--collapsible-panel-height/--collapsible-panel-width when closing starts). Adding that case will better protect this PR’s core behavior.

💡 Suggested test shape
+ it('re-pins height/width vars when close transition starts', async () => {+ const user = userEvent.setup();+ renderCollapsible({ defaultOpen: true });++ const original = (Element.prototype as { getAnimations?: unknown }).getAnimations;+ (Element.prototype as { getAnimations?: unknown }).getAnimations = () => [+ { finished: new Promise<void>(() => {}) },+ ];+ try {+ await user.click(screen.getByRole('button', { name: 'Toggle' }));+ const panel = document.querySelector('[data-cl-slot="collapsible-panel"]') as HTMLElement;+ const style = panel.getAttribute('style') ?? '';+ expect(style).toContain('--collapsible-panel-height');+ expect(style).toContain('--collapsible-panel-width');+ } finally {+ if (original) {+ (Element.prototype as { getAnimations?: unknown }).getAnimations = original;+ } else {+ delete (Element.prototype as { getAnimations?: unknown }).getAnimations;+ }+ }+ });

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx} should “Implement comprehensive testing including unit, integration, and E2E tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/headless/src/primitives/collapsible/collapsible.test.tsx` around
lines 142 - 189, Add a new test case in the 'animation lifecycle' describe block
that verifies the close-transition re-pin behavior. The test should render the
collapsible in an open state (using defaultOpen: true), then trigger the close
action, and assert that the --collapsible-panel-height and
--collapsible-panel-width CSS variables are re-pinned on the panel element (with
the data-cl-slot="collapsible-panel" selector) during the closing animation.
This mirrors the existing "pins --collapsible-panel-height/width while the open
animation is in progress" test but for the close path, ensuring that both
dimensions are preserved while the close animation is in progress before being
cleared once the animation completes.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/headless/src/utils/reset-layout-styles.ts`:
- Around line 22-40: The originalLayoutStyles object only captures the property
values but not their priority flags (the !important declaration). When restoring
in the restore() function, this causes properties that originally had !important
to be re-applied without it, changing layout behavior. Modify the code to
capture both the value and priority using element.style.getPropertyPriority(key)
for each key in originalLayoutStyles, then update the restore() function to pass
the captured priority as the third parameter to element.style.setProperty() when
re-applying the values, ensuring the !important flag is preserved if it was
originally present.
---
Nitpick comments:
In `@packages/headless/src/primitives/accordion/accordion.test.tsx`:
- Around line 194-238: The animation lifecycle test suite validates the
open-path behavior for height pinning and clearing, but is missing a test case
for the close-path behavior. Add a new test within the 'animation lifecycle'
describe block that validates the height re-pinning behavior when closing
starts. This test should open an accordion panel (using defaultValue), then
simulate closing it and verify that the --cl-accordion-panel-height CSS variable
is re-pinned to the measured pixel height during the close animation phase,
similar to the pattern used in the existing 'pins --cl-accordion-panel-height
while the open animation is in progress' test case.
In `@packages/headless/src/primitives/collapsible/collapsible.test.tsx`:
- Around line 142-189: Add a new test case in the 'animation lifecycle' describe
block that verifies the close-transition re-pin behavior. The test should render
the collapsible in an open state (using defaultOpen: true), then trigger the
close action, and assert that the --collapsible-panel-height and
--collapsible-panel-width CSS variables are re-pinned on the panel element (with
the data-cl-slot="collapsible-panel" selector) during the closing animation.
This mirrors the existing "pins --collapsible-panel-height/width while the open
animation is in progress" test but for the close path, ensuring that both
dimensions are preserved while the close animation is in progress before being
cleared once the animation completes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 3c30d6de-3161-4e3c-a8d1-3ac274415bf5

📥 Commits

Reviewing files that changed from the base of the PR and between f4ecc13 and e810695.

📒 Files selected for processing (10)
  • packages/headless/src/primitives/accordion/accordion-panel.tsx
  • packages/headless/src/primitives/accordion/accordion.test.tsx
  • packages/headless/src/primitives/collapsible/collapsible-panel.tsx
  • packages/headless/src/primitives/collapsible/collapsible.test.tsx
  • packages/headless/src/primitives/tabs/tabs-context.ts
  • packages/headless/src/primitives/tabs/tabs-indicator.tsx
  • packages/headless/src/primitives/tabs/tabs-list.tsx
  • packages/headless/src/primitives/tabs/tabs-root.tsx
  • packages/headless/src/utils/index.ts
  • packages/headless/src/utils/reset-layout-styles.ts

Comment threadpackages/headless/src/utils/reset-layout-styles.ts Outdated
@alexcarpenter
alexcarpenter merged commit fc578e3 into mainJun 18, 2026
45 checks passed
@alexcarpenter
alexcarpenter deleted the carp/headless-measurement-gaps branch June 18, 2026 15:25
Sign up for freeto 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

@alexcarpenter@kylemac
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(headless): correct panel measurement and tabs indicator timing by alexcarpenter · Pull Request #8897 · clerk/javascript · GitHub
Skip to content

fix(headless): correct panel measurement and tabs indicator timing - #8897

Merged
alexcarpenter merged 4 commits into
mainfrom
carp/headless-measurement-gaps
Jun 18, 2026
Merged

fix(headless): correct panel measurement and tabs indicator timing#8897
alexcarpenter merged 4 commits into
mainfrom
carp/headless-measurement-gaps

Conversation

@alexcarpenter

@alexcarpenteralexcarpenter commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

Three correctness fixes in @clerk/headless measurement primitives, identified by comparing against base-ui's useCollapsiblePanel.

1. Reset flex/grid alignment before measuring

Accordion.Panel and Collapsible.Panel read scrollHeight/scrollWidth directly. With display: flex/grid and non-default justify-content / align-items / align-content / justify-items, alignment can make the scroll size report smaller than the true content size. A new resetLayoutStyles util forces those properties to initial before measuring and restores them on the next frame.

2. Flow at auto when idle, re-pin on close

Once the open animation settles, the panels now drop the measured pixel size (CSS vars omitted → height: auto) so content flows naturally, and re-pin a measured value when the close transition starts so exit animations still have a concrete value to animate from.

3. Tabs.Indicator measures before paint

Switched the indicator measurement from useEffect to useLayoutEffect to remove a one-frame stale indicator position on tab change. This required tracking the tab-list element as context state (not a useRef): a child's layout effect runs before its parent's ref is attached, so the prior useEffect only worked by accident (passive effects run after all refs attach). Same pattern base-ui uses.

Testing

  • @clerk/headless unit suite passes (404 tests), typecheck clean.

Summary by CodeRabbit

Summary

  • Bug Fixes

    • Improved accordion and collapsible panel open/close animations by refining how sizes are measured and restored across transitions.
    • Updated tabs indicator to reposition synchronously during tab changes to prevent brief misalignment.
  • Tests

    • Strengthened accordion and collapsible animation lifecycle tests to verify pinned dimensions during active transitions and clearing after completion.
  • Refactor

    • Refactored tabs internals to track the tabs list element via state and update indicator measurements accordingly.

- Accordion.Panel / Collapsible.Panel: reset flex/grid alignment before
reading scrollHeight/scrollWidth so non-default alignment can't truncate
the measured content size (new resetLayoutStyles util).
- Accordion.Panel / Collapsible.Panel: drop the pinned pixel size once the
open animation settles (panel flows at auto) and re-pin a measured value
when closing starts so exit transitions still animate.
- Tabs.Indicator: measure in useLayoutEffect (before paint) to remove a
one-frame stale position on tab change. Track the list element as context
state so the indicator's layout effect can read it (a child layout effect
runs before the parent ref attaches).
@changeset-bot

changeset-botBot commented Jun 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b33c1db

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

vercelBot commented Jun 17, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreview, CommentJun 18, 2026 1:57pm
swingsetReadyReadyPreview, CommentJun 18, 2026 1:57pm

Request Review

@coderabbitai

coderabbitaiBot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 99c52d0b-49ba-4306-a940-de48b05be979

📥 Commits

Reviewing files that changed from the base of the PR and between 17090d3 and a989491.

📒 Files selected for processing (1)
  • packages/headless/src/utils/reset-layout-styles.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/headless/src/utils/reset-layout-styles.ts

📝 Walkthrough

Walkthrough

Adds a resetLayoutStyles utility that temporarily forces flex/grid alignment properties to initial !important before scroll-dimension measurement and restores them on the next animation frame. Wires this into AccordionPanel and CollapsiblePanel to pin pixel heights during close transitions and clear them after open animations settle. Separately migrates the Tabs list element from a useRef to useState so TabsIndicator can measure synchronously with useLayoutEffect.

Changes

Panel Height Measurement & Animation Lifecycle

Layer / File(s)Summary
resetLayoutStyles utility
packages/headless/src/utils/reset-layout-styles.ts, packages/headless/src/utils/index.ts
New resetLayoutStyles(element) helper captures original inline alignment styles (justify-content, align-items, align-content, justify-items), forces them to initial !important for accurate scrollHeight/scrollWidth reads, restores originals on the next animation frame, and returns an immediate-cleanup callback. Re-exported from the utils index.
AccordionPanel measurement & transition lifecycle
packages/headless/src/primitives/accordion/accordion-panel.tsx
Integrates resetLayoutStyles into the ResizeObserver path to normalize flex/grid alignment before each height measurement. Captures transitionStatus and runOnAnimationsFinished to orchestrate clearing the pinned pixel height after open animations finish and re-pinning it when a close transition enters the 'ending' status. ResizeObserver cleanup now restores layout styles.
AccordionPanel animation lifecycle tests
packages/headless/src/primitives/accordion/accordion.test.tsx
Replaces the prior single assertion with two tests: one mocks Element.prototype.getAnimations to simulate an in-progress open animation and asserts --cl-accordion-panel-height is present; the other renders open without mocked animations and asserts the variable is absent after settling.
CollapsiblePanel measurement & transition lifecycle
packages/headless/src/primitives/collapsible/collapsible-panel.tsx
Same resetLayoutStyles + useAnimationsFinished integration as accordion, extended to both height and width dimensions. Clears pinned values after open animations settle, re-pins both axes when closing begins, and restores layout styles in ResizeObserver cleanup.
CollapsiblePanel animation lifecycle tests
packages/headless/src/primitives/collapsible/collapsible.test.tsx
Replaces prior assertions with two tests: one verifies both --collapsible-panel-height and --collapsible-panel-width are set while an open animation is mocked as pending; the other verifies both are absent from inline style once settled open.

Tabs List Ref → State Migration

Layer / File(s)Summary
TabsContextValue contract: ref → state
packages/headless/src/primitives/tabs/tabs-context.ts
TabsContextValue drops listRef: React.RefObject<HTMLElement> and adds listElement: HTMLElement | null plus setListElement: (element: HTMLElement | null) => void, with comments explaining why state (rather than a ref) is needed for measurement timing.
TabsRoot state + TabsList ref wiring
packages/headless/src/primitives/tabs/tabs-root.tsx, packages/headless/src/primitives/tabs/tabs-list.tsx
TabsRoot switches from useRef to useState for the list element, stores it in state, and provides both listElement and setListElement through the context provider with updated dependencies. TabsList passes setListElement as the Composite ref callback.
TabsIndicator synchronous measurement
packages/headless/src/primitives/tabs/tabs-indicator.tsx
Converts the indicator positioning effect from useEffect to useLayoutEffect and reads listElement directly from context instead of accessing listRef.current, eliminating the ref indirection and updating the dependency array to trigger on listElement changes.

Sequence Diagram(s)

sequenceDiagram
participant ResizeObserver
participant AccordionPanel
participant resetLayoutStyles
participant useAnimationsFinished
ResizeObserver->>AccordionPanel: observe size changes
AccordionPanel->>resetLayoutStyles: call with panel element
resetLayoutStyles->>resetLayoutStyles: capture align styles + priorities
resetLayoutStyles->>AccordionPanel: force align to initial !important
AccordionPanel->>AccordionPanel: read scrollHeight
AccordionPanel->>resetLayoutStyles: schedule restore on next frame
AccordionPanel->>AccordionPanel: set height = px value
Note over AccordionPanel,useAnimationsFinished: Open animation in progress
useAnimationsFinished->>AccordionPanel: animation finished
AccordionPanel->>AccordionPanel: clear height (undefined)
Note over AccordionPanel,useAnimationsFinished: User closes panel
AccordionPanel->>resetLayoutStyles: call with panel element (re-pin)
resetLayoutStyles->>AccordionPanel: force align to initial !important
AccordionPanel->>AccordionPanel: set height = px value
AccordionPanel->>AccordionPanel: play close animation
Loading
sequenceDiagram
participant TabsRoot
participant TabsContext
participant TabsList
participant TabsIndicator
TabsRoot->>TabsRoot: useState(listElement)
TabsRoot->>TabsContext: provide listElement + setListElement
TabsList->>TabsContext: consume setListElement
TabsList->>TabsList: pass setListElement to Composite ref
Note over TabsList,TabsIndicator: Tab list element mounted
TabsList->>TabsContext: setListElement(composite element)
TabsContext->>TabsIndicator: listElement updated
TabsIndicator->>TabsIndicator: useLayoutEffect observes listElement
TabsIndicator->>TabsIndicator: measure active tab bounding rect
TabsIndicator->>TabsIndicator: update indicator position sync
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • clerk/javascript#8475: Modifies accordion-panel.tsx and the --cl-accordion-panel-height CSS variable/testing, directly overlapping with the accordion animation lifecycle changes in this PR.
  • clerk/javascript#8474: Main PR updates to accordion-panel.tsx and collapsible-panel.tsx integrate the animation lifecycle via the useAnimationsFinished/useTransition APIs (including transitionStatus) that are foundational to this PR's implementation.

Suggested reviewers

  • wobsoriano
  • austincalvelage
  • Ephem

Poem

🐇 Hop, hop! The panel heights now know their place,
No frozen flex tricks left to leave a trace.
I measure true with resetLayoutStyles neat,
Then auto flows once open—oh so sweet!
The tabs list lives in state for sync delight,
And indicators snap in perfect sight. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 20.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the three main fixes: correcting panel measurement and improving tabs indicator timing, which are the primary changes across accordion, collapsible, and tabs components.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

@pkg-pr-new

pkg-pr-newBot commented Jun 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8897

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8897

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8897

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8897

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@8897

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8897

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8897

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8897

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8897

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8897

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8897

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8897

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8897

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8897

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8897

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8897

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8897

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8897

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8897

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8897

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8897

commit: b33c1db

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/headless/src/primitives/accordion/accordion.test.tsx (1)

194-238: ⚡ Quick win

Add a close-transition test for re-pinning height.

The updated suite validates open-path pin/clear behavior, but it still doesn’t assert the new close-path contract (re-pin measured height when closing starts). A focused test here would lock down the exact behavior this PR adds.

💡 Suggested test shape
+ it('re-pins --cl-accordion-panel-height when close transition starts', async () => {+ const user = userEvent.setup();+ renderAccordion({ defaultValue: ['item1'] });++ const original = (Element.prototype as { getAnimations?: unknown }).getAnimations;+ (Element.prototype as { getAnimations?: unknown }).getAnimations = () => [+ { finished: new Promise<void>(() => {}) },+ ];+ try {+ await user.click(screen.getByRole('button', { name: 'Section 1' }));+ const panel = document.querySelector('[data-cl-slot="accordion-panel"]') as HTMLElement;+ expect(panel.getAttribute('style') ?? '').toContain('--cl-accordion-panel-height');+ } finally {+ if (original) {+ (Element.prototype as { getAnimations?: unknown }).getAnimations = original;+ } else {+ delete (Element.prototype as { getAnimations?: unknown }).getAnimations;+ }+ }+ });

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx} should “Implement comprehensive testing including unit, integration, and E2E tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/headless/src/primitives/accordion/accordion.test.tsx` around lines
194 - 238, The animation lifecycle test suite validates the open-path behavior
for height pinning and clearing, but is missing a test case for the close-path
behavior. Add a new test within the 'animation lifecycle' describe block that
validates the height re-pinning behavior when closing starts. This test should
open an accordion panel (using defaultValue), then simulate closing it and
verify that the --cl-accordion-panel-height CSS variable is re-pinned to the
measured pixel height during the close animation phase, similar to the pattern
used in the existing 'pins --cl-accordion-panel-height while the open animation
is in progress' test case.

Source: Coding guidelines

packages/headless/src/primitives/collapsible/collapsible.test.tsx (1)

142-189: ⚡ Quick win

Add a close-transition test that asserts both dimensions are re-pinned.

This suite now covers open-path pin/clear behavior, but it doesn’t directly test the new close-path re-pin logic (--collapsible-panel-height/--collapsible-panel-width when closing starts). Adding that case will better protect this PR’s core behavior.

💡 Suggested test shape
+ it('re-pins height/width vars when close transition starts', async () => {+ const user = userEvent.setup();+ renderCollapsible({ defaultOpen: true });++ const original = (Element.prototype as { getAnimations?: unknown }).getAnimations;+ (Element.prototype as { getAnimations?: unknown }).getAnimations = () => [+ { finished: new Promise<void>(() => {}) },+ ];+ try {+ await user.click(screen.getByRole('button', { name: 'Toggle' }));+ const panel = document.querySelector('[data-cl-slot="collapsible-panel"]') as HTMLElement;+ const style = panel.getAttribute('style') ?? '';+ expect(style).toContain('--collapsible-panel-height');+ expect(style).toContain('--collapsible-panel-width');+ } finally {+ if (original) {+ (Element.prototype as { getAnimations?: unknown }).getAnimations = original;+ } else {+ delete (Element.prototype as { getAnimations?: unknown }).getAnimations;+ }+ }+ });

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx} should “Implement comprehensive testing including unit, integration, and E2E tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/headless/src/primitives/collapsible/collapsible.test.tsx` around
lines 142 - 189, Add a new test case in the 'animation lifecycle' describe block
that verifies the close-transition re-pin behavior. The test should render the
collapsible in an open state (using defaultOpen: true), then trigger the close
action, and assert that the --collapsible-panel-height and
--collapsible-panel-width CSS variables are re-pinned on the panel element (with
the data-cl-slot="collapsible-panel" selector) during the closing animation.
This mirrors the existing "pins --collapsible-panel-height/width while the open
animation is in progress" test but for the close path, ensuring that both
dimensions are preserved while the close animation is in progress before being
cleared once the animation completes.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/headless/src/utils/reset-layout-styles.ts`:
- Around line 22-40: The originalLayoutStyles object only captures the property
values but not their priority flags (the !important declaration). When restoring
in the restore() function, this causes properties that originally had !important
to be re-applied without it, changing layout behavior. Modify the code to
capture both the value and priority using element.style.getPropertyPriority(key)
for each key in originalLayoutStyles, then update the restore() function to pass
the captured priority as the third parameter to element.style.setProperty() when
re-applying the values, ensuring the !important flag is preserved if it was
originally present.
---
Nitpick comments:
In `@packages/headless/src/primitives/accordion/accordion.test.tsx`:
- Around line 194-238: The animation lifecycle test suite validates the
open-path behavior for height pinning and clearing, but is missing a test case
for the close-path behavior. Add a new test within the 'animation lifecycle'
describe block that validates the height re-pinning behavior when closing
starts. This test should open an accordion panel (using defaultValue), then
simulate closing it and verify that the --cl-accordion-panel-height CSS variable
is re-pinned to the measured pixel height during the close animation phase,
similar to the pattern used in the existing 'pins --cl-accordion-panel-height
while the open animation is in progress' test case.
In `@packages/headless/src/primitives/collapsible/collapsible.test.tsx`:
- Around line 142-189: Add a new test case in the 'animation lifecycle' describe
block that verifies the close-transition re-pin behavior. The test should render
the collapsible in an open state (using defaultOpen: true), then trigger the
close action, and assert that the --collapsible-panel-height and
--collapsible-panel-width CSS variables are re-pinned on the panel element (with
the data-cl-slot="collapsible-panel" selector) during the closing animation.
This mirrors the existing "pins --collapsible-panel-height/width while the open
animation is in progress" test but for the close path, ensuring that both
dimensions are preserved while the close animation is in progress before being
cleared once the animation completes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 3c30d6de-3161-4e3c-a8d1-3ac274415bf5

📥 Commits

Reviewing files that changed from the base of the PR and between f4ecc13 and e810695.

📒 Files selected for processing (10)
  • packages/headless/src/primitives/accordion/accordion-panel.tsx
  • packages/headless/src/primitives/accordion/accordion.test.tsx
  • packages/headless/src/primitives/collapsible/collapsible-panel.tsx
  • packages/headless/src/primitives/collapsible/collapsible.test.tsx
  • packages/headless/src/primitives/tabs/tabs-context.ts
  • packages/headless/src/primitives/tabs/tabs-indicator.tsx
  • packages/headless/src/primitives/tabs/tabs-list.tsx
  • packages/headless/src/primitives/tabs/tabs-root.tsx
  • packages/headless/src/utils/index.ts
  • packages/headless/src/utils/reset-layout-styles.ts

Comment threadpackages/headless/src/utils/reset-layout-styles.ts Outdated
@alexcarpenter
alexcarpenter merged commit fc578e3 into mainJun 18, 2026
45 checks passed
@alexcarpenter
alexcarpenter deleted the carp/headless-measurement-gaps branch June 18, 2026 15:25
Sign up for freeto 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

@alexcarpenter@kylemac
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(headless): correct panel measurement and tabs indicator timing by alexcarpenter · Pull Request #8897 · clerk/javascript · GitHub
Skip to content

fix(headless): correct panel measurement and tabs indicator timing - #8897

Merged
alexcarpenter merged 4 commits into
mainfrom
carp/headless-measurement-gaps
Jun 18, 2026
Merged

fix(headless): correct panel measurement and tabs indicator timing#8897
alexcarpenter merged 4 commits into
mainfrom
carp/headless-measurement-gaps

Conversation

@alexcarpenter

@alexcarpenteralexcarpenter commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

Three correctness fixes in @clerk/headless measurement primitives, identified by comparing against base-ui's useCollapsiblePanel.

1. Reset flex/grid alignment before measuring

Accordion.Panel and Collapsible.Panel read scrollHeight/scrollWidth directly. With display: flex/grid and non-default justify-content / align-items / align-content / justify-items, alignment can make the scroll size report smaller than the true content size. A new resetLayoutStyles util forces those properties to initial before measuring and restores them on the next frame.

2. Flow at auto when idle, re-pin on close

Once the open animation settles, the panels now drop the measured pixel size (CSS vars omitted → height: auto) so content flows naturally, and re-pin a measured value when the close transition starts so exit animations still have a concrete value to animate from.

3. Tabs.Indicator measures before paint

Switched the indicator measurement from useEffect to useLayoutEffect to remove a one-frame stale indicator position on tab change. This required tracking the tab-list element as context state (not a useRef): a child's layout effect runs before its parent's ref is attached, so the prior useEffect only worked by accident (passive effects run after all refs attach). Same pattern base-ui uses.

Testing

  • @clerk/headless unit suite passes (404 tests), typecheck clean.

Summary by CodeRabbit

Summary

  • Bug Fixes

    • Improved accordion and collapsible panel open/close animations by refining how sizes are measured and restored across transitions.
    • Updated tabs indicator to reposition synchronously during tab changes to prevent brief misalignment.
  • Tests

    • Strengthened accordion and collapsible animation lifecycle tests to verify pinned dimensions during active transitions and clearing after completion.
  • Refactor

    • Refactored tabs internals to track the tabs list element via state and update indicator measurements accordingly.

- Accordion.Panel / Collapsible.Panel: reset flex/grid alignment before
reading scrollHeight/scrollWidth so non-default alignment can't truncate
the measured content size (new resetLayoutStyles util).
- Accordion.Panel / Collapsible.Panel: drop the pinned pixel size once the
open animation settles (panel flows at auto) and re-pin a measured value
when closing starts so exit transitions still animate.
- Tabs.Indicator: measure in useLayoutEffect (before paint) to remove a
one-frame stale position on tab change. Track the list element as context
state so the indicator's layout effect can read it (a child layout effect
runs before the parent ref attaches).
@changeset-bot

changeset-botBot commented Jun 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b33c1db

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

vercelBot commented Jun 17, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreview, CommentJun 18, 2026 1:57pm
swingsetReadyReadyPreview, CommentJun 18, 2026 1:57pm

Request Review

@coderabbitai

coderabbitaiBot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 99c52d0b-49ba-4306-a940-de48b05be979

📥 Commits

Reviewing files that changed from the base of the PR and between 17090d3 and a989491.

📒 Files selected for processing (1)
  • packages/headless/src/utils/reset-layout-styles.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/headless/src/utils/reset-layout-styles.ts

📝 Walkthrough

Walkthrough

Adds a resetLayoutStyles utility that temporarily forces flex/grid alignment properties to initial !important before scroll-dimension measurement and restores them on the next animation frame. Wires this into AccordionPanel and CollapsiblePanel to pin pixel heights during close transitions and clear them after open animations settle. Separately migrates the Tabs list element from a useRef to useState so TabsIndicator can measure synchronously with useLayoutEffect.

Changes

Panel Height Measurement & Animation Lifecycle

Layer / File(s)Summary
resetLayoutStyles utility
packages/headless/src/utils/reset-layout-styles.ts, packages/headless/src/utils/index.ts
New resetLayoutStyles(element) helper captures original inline alignment styles (justify-content, align-items, align-content, justify-items), forces them to initial !important for accurate scrollHeight/scrollWidth reads, restores originals on the next animation frame, and returns an immediate-cleanup callback. Re-exported from the utils index.
AccordionPanel measurement & transition lifecycle
packages/headless/src/primitives/accordion/accordion-panel.tsx
Integrates resetLayoutStyles into the ResizeObserver path to normalize flex/grid alignment before each height measurement. Captures transitionStatus and runOnAnimationsFinished to orchestrate clearing the pinned pixel height after open animations finish and re-pinning it when a close transition enters the 'ending' status. ResizeObserver cleanup now restores layout styles.
AccordionPanel animation lifecycle tests
packages/headless/src/primitives/accordion/accordion.test.tsx
Replaces the prior single assertion with two tests: one mocks Element.prototype.getAnimations to simulate an in-progress open animation and asserts --cl-accordion-panel-height is present; the other renders open without mocked animations and asserts the variable is absent after settling.
CollapsiblePanel measurement & transition lifecycle
packages/headless/src/primitives/collapsible/collapsible-panel.tsx
Same resetLayoutStyles + useAnimationsFinished integration as accordion, extended to both height and width dimensions. Clears pinned values after open animations settle, re-pins both axes when closing begins, and restores layout styles in ResizeObserver cleanup.
CollapsiblePanel animation lifecycle tests
packages/headless/src/primitives/collapsible/collapsible.test.tsx
Replaces prior assertions with two tests: one verifies both --collapsible-panel-height and --collapsible-panel-width are set while an open animation is mocked as pending; the other verifies both are absent from inline style once settled open.

Tabs List Ref → State Migration

Layer / File(s)Summary
TabsContextValue contract: ref → state
packages/headless/src/primitives/tabs/tabs-context.ts
TabsContextValue drops listRef: React.RefObject<HTMLElement> and adds listElement: HTMLElement | null plus setListElement: (element: HTMLElement | null) => void, with comments explaining why state (rather than a ref) is needed for measurement timing.
TabsRoot state + TabsList ref wiring
packages/headless/src/primitives/tabs/tabs-root.tsx, packages/headless/src/primitives/tabs/tabs-list.tsx
TabsRoot switches from useRef to useState for the list element, stores it in state, and provides both listElement and setListElement through the context provider with updated dependencies. TabsList passes setListElement as the Composite ref callback.
TabsIndicator synchronous measurement
packages/headless/src/primitives/tabs/tabs-indicator.tsx
Converts the indicator positioning effect from useEffect to useLayoutEffect and reads listElement directly from context instead of accessing listRef.current, eliminating the ref indirection and updating the dependency array to trigger on listElement changes.

Sequence Diagram(s)

sequenceDiagram
participant ResizeObserver
participant AccordionPanel
participant resetLayoutStyles
participant useAnimationsFinished
ResizeObserver->>AccordionPanel: observe size changes
AccordionPanel->>resetLayoutStyles: call with panel element
resetLayoutStyles->>resetLayoutStyles: capture align styles + priorities
resetLayoutStyles->>AccordionPanel: force align to initial !important
AccordionPanel->>AccordionPanel: read scrollHeight
AccordionPanel->>resetLayoutStyles: schedule restore on next frame
AccordionPanel->>AccordionPanel: set height = px value
Note over AccordionPanel,useAnimationsFinished: Open animation in progress
useAnimationsFinished->>AccordionPanel: animation finished
AccordionPanel->>AccordionPanel: clear height (undefined)
Note over AccordionPanel,useAnimationsFinished: User closes panel
AccordionPanel->>resetLayoutStyles: call with panel element (re-pin)
resetLayoutStyles->>AccordionPanel: force align to initial !important
AccordionPanel->>AccordionPanel: set height = px value
AccordionPanel->>AccordionPanel: play close animation
Loading
sequenceDiagram
participant TabsRoot
participant TabsContext
participant TabsList
participant TabsIndicator
TabsRoot->>TabsRoot: useState(listElement)
TabsRoot->>TabsContext: provide listElement + setListElement
TabsList->>TabsContext: consume setListElement
TabsList->>TabsList: pass setListElement to Composite ref
Note over TabsList,TabsIndicator: Tab list element mounted
TabsList->>TabsContext: setListElement(composite element)
TabsContext->>TabsIndicator: listElement updated
TabsIndicator->>TabsIndicator: useLayoutEffect observes listElement
TabsIndicator->>TabsIndicator: measure active tab bounding rect
TabsIndicator->>TabsIndicator: update indicator position sync
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • clerk/javascript#8475: Modifies accordion-panel.tsx and the --cl-accordion-panel-height CSS variable/testing, directly overlapping with the accordion animation lifecycle changes in this PR.
  • clerk/javascript#8474: Main PR updates to accordion-panel.tsx and collapsible-panel.tsx integrate the animation lifecycle via the useAnimationsFinished/useTransition APIs (including transitionStatus) that are foundational to this PR's implementation.

Suggested reviewers

  • wobsoriano
  • austincalvelage
  • Ephem

Poem

🐇 Hop, hop! The panel heights now know their place,
No frozen flex tricks left to leave a trace.
I measure true with resetLayoutStyles neat,
Then auto flows once open—oh so sweet!
The tabs list lives in state for sync delight,
And indicators snap in perfect sight. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 20.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the three main fixes: correcting panel measurement and improving tabs indicator timing, which are the primary changes across accordion, collapsible, and tabs components.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

@pkg-pr-new

pkg-pr-newBot commented Jun 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8897

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8897

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8897

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8897

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@8897

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8897

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8897

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8897

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8897

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8897

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8897

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8897

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8897

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8897

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8897

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8897

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8897

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8897

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8897

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8897

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8897

commit: b33c1db

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/headless/src/primitives/accordion/accordion.test.tsx (1)

194-238: ⚡ Quick win

Add a close-transition test for re-pinning height.

The updated suite validates open-path pin/clear behavior, but it still doesn’t assert the new close-path contract (re-pin measured height when closing starts). A focused test here would lock down the exact behavior this PR adds.

💡 Suggested test shape
+ it('re-pins --cl-accordion-panel-height when close transition starts', async () => {+ const user = userEvent.setup();+ renderAccordion({ defaultValue: ['item1'] });++ const original = (Element.prototype as { getAnimations?: unknown }).getAnimations;+ (Element.prototype as { getAnimations?: unknown }).getAnimations = () => [+ { finished: new Promise<void>(() => {}) },+ ];+ try {+ await user.click(screen.getByRole('button', { name: 'Section 1' }));+ const panel = document.querySelector('[data-cl-slot="accordion-panel"]') as HTMLElement;+ expect(panel.getAttribute('style') ?? '').toContain('--cl-accordion-panel-height');+ } finally {+ if (original) {+ (Element.prototype as { getAnimations?: unknown }).getAnimations = original;+ } else {+ delete (Element.prototype as { getAnimations?: unknown }).getAnimations;+ }+ }+ });

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx} should “Implement comprehensive testing including unit, integration, and E2E tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/headless/src/primitives/accordion/accordion.test.tsx` around lines
194 - 238, The animation lifecycle test suite validates the open-path behavior
for height pinning and clearing, but is missing a test case for the close-path
behavior. Add a new test within the 'animation lifecycle' describe block that
validates the height re-pinning behavior when closing starts. This test should
open an accordion panel (using defaultValue), then simulate closing it and
verify that the --cl-accordion-panel-height CSS variable is re-pinned to the
measured pixel height during the close animation phase, similar to the pattern
used in the existing 'pins --cl-accordion-panel-height while the open animation
is in progress' test case.

Source: Coding guidelines

packages/headless/src/primitives/collapsible/collapsible.test.tsx (1)

142-189: ⚡ Quick win

Add a close-transition test that asserts both dimensions are re-pinned.

This suite now covers open-path pin/clear behavior, but it doesn’t directly test the new close-path re-pin logic (--collapsible-panel-height/--collapsible-panel-width when closing starts). Adding that case will better protect this PR’s core behavior.

💡 Suggested test shape
+ it('re-pins height/width vars when close transition starts', async () => {+ const user = userEvent.setup();+ renderCollapsible({ defaultOpen: true });++ const original = (Element.prototype as { getAnimations?: unknown }).getAnimations;+ (Element.prototype as { getAnimations?: unknown }).getAnimations = () => [+ { finished: new Promise<void>(() => {}) },+ ];+ try {+ await user.click(screen.getByRole('button', { name: 'Toggle' }));+ const panel = document.querySelector('[data-cl-slot="collapsible-panel"]') as HTMLElement;+ const style = panel.getAttribute('style') ?? '';+ expect(style).toContain('--collapsible-panel-height');+ expect(style).toContain('--collapsible-panel-width');+ } finally {+ if (original) {+ (Element.prototype as { getAnimations?: unknown }).getAnimations = original;+ } else {+ delete (Element.prototype as { getAnimations?: unknown }).getAnimations;+ }+ }+ });

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx} should “Implement comprehensive testing including unit, integration, and E2E tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/headless/src/primitives/collapsible/collapsible.test.tsx` around
lines 142 - 189, Add a new test case in the 'animation lifecycle' describe block
that verifies the close-transition re-pin behavior. The test should render the
collapsible in an open state (using defaultOpen: true), then trigger the close
action, and assert that the --collapsible-panel-height and
--collapsible-panel-width CSS variables are re-pinned on the panel element (with
the data-cl-slot="collapsible-panel" selector) during the closing animation.
This mirrors the existing "pins --collapsible-panel-height/width while the open
animation is in progress" test but for the close path, ensuring that both
dimensions are preserved while the close animation is in progress before being
cleared once the animation completes.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/headless/src/utils/reset-layout-styles.ts`:
- Around line 22-40: The originalLayoutStyles object only captures the property
values but not their priority flags (the !important declaration). When restoring
in the restore() function, this causes properties that originally had !important
to be re-applied without it, changing layout behavior. Modify the code to
capture both the value and priority using element.style.getPropertyPriority(key)
for each key in originalLayoutStyles, then update the restore() function to pass
the captured priority as the third parameter to element.style.setProperty() when
re-applying the values, ensuring the !important flag is preserved if it was
originally present.
---
Nitpick comments:
In `@packages/headless/src/primitives/accordion/accordion.test.tsx`:
- Around line 194-238: The animation lifecycle test suite validates the
open-path behavior for height pinning and clearing, but is missing a test case
for the close-path behavior. Add a new test within the 'animation lifecycle'
describe block that validates the height re-pinning behavior when closing
starts. This test should open an accordion panel (using defaultValue), then
simulate closing it and verify that the --cl-accordion-panel-height CSS variable
is re-pinned to the measured pixel height during the close animation phase,
similar to the pattern used in the existing 'pins --cl-accordion-panel-height
while the open animation is in progress' test case.
In `@packages/headless/src/primitives/collapsible/collapsible.test.tsx`:
- Around line 142-189: Add a new test case in the 'animation lifecycle' describe
block that verifies the close-transition re-pin behavior. The test should render
the collapsible in an open state (using defaultOpen: true), then trigger the
close action, and assert that the --collapsible-panel-height and
--collapsible-panel-width CSS variables are re-pinned on the panel element (with
the data-cl-slot="collapsible-panel" selector) during the closing animation.
This mirrors the existing "pins --collapsible-panel-height/width while the open
animation is in progress" test but for the close path, ensuring that both
dimensions are preserved while the close animation is in progress before being
cleared once the animation completes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 3c30d6de-3161-4e3c-a8d1-3ac274415bf5

📥 Commits

Reviewing files that changed from the base of the PR and between f4ecc13 and e810695.

📒 Files selected for processing (10)
  • packages/headless/src/primitives/accordion/accordion-panel.tsx
  • packages/headless/src/primitives/accordion/accordion.test.tsx
  • packages/headless/src/primitives/collapsible/collapsible-panel.tsx
  • packages/headless/src/primitives/collapsible/collapsible.test.tsx
  • packages/headless/src/primitives/tabs/tabs-context.ts
  • packages/headless/src/primitives/tabs/tabs-indicator.tsx
  • packages/headless/src/primitives/tabs/tabs-list.tsx
  • packages/headless/src/primitives/tabs/tabs-root.tsx
  • packages/headless/src/utils/index.ts
  • packages/headless/src/utils/reset-layout-styles.ts

Comment threadpackages/headless/src/utils/reset-layout-styles.ts Outdated
@alexcarpenter
alexcarpenter merged commit fc578e3 into mainJun 18, 2026
45 checks passed
@alexcarpenter
alexcarpenter deleted the carp/headless-measurement-gaps branch June 18, 2026 15:25
Sign up for freeto 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

@alexcarpenter@kylemac
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(headless): correct panel measurement and tabs indicator timing by alexcarpenter · Pull Request #8897 · clerk/javascript · GitHub
Skip to content

fix(headless): correct panel measurement and tabs indicator timing - #8897

Merged
alexcarpenter merged 4 commits into
mainfrom
carp/headless-measurement-gaps
Jun 18, 2026
Merged

fix(headless): correct panel measurement and tabs indicator timing#8897
alexcarpenter merged 4 commits into
mainfrom
carp/headless-measurement-gaps

Conversation

@alexcarpenter

@alexcarpenteralexcarpenter commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

Three correctness fixes in @clerk/headless measurement primitives, identified by comparing against base-ui's useCollapsiblePanel.

1. Reset flex/grid alignment before measuring

Accordion.Panel and Collapsible.Panel read scrollHeight/scrollWidth directly. With display: flex/grid and non-default justify-content / align-items / align-content / justify-items, alignment can make the scroll size report smaller than the true content size. A new resetLayoutStyles util forces those properties to initial before measuring and restores them on the next frame.

2. Flow at auto when idle, re-pin on close

Once the open animation settles, the panels now drop the measured pixel size (CSS vars omitted → height: auto) so content flows naturally, and re-pin a measured value when the close transition starts so exit animations still have a concrete value to animate from.

3. Tabs.Indicator measures before paint

Switched the indicator measurement from useEffect to useLayoutEffect to remove a one-frame stale indicator position on tab change. This required tracking the tab-list element as context state (not a useRef): a child's layout effect runs before its parent's ref is attached, so the prior useEffect only worked by accident (passive effects run after all refs attach). Same pattern base-ui uses.

Testing

  • @clerk/headless unit suite passes (404 tests), typecheck clean.

Summary by CodeRabbit

Summary

  • Bug Fixes

    • Improved accordion and collapsible panel open/close animations by refining how sizes are measured and restored across transitions.
    • Updated tabs indicator to reposition synchronously during tab changes to prevent brief misalignment.
  • Tests

    • Strengthened accordion and collapsible animation lifecycle tests to verify pinned dimensions during active transitions and clearing after completion.
  • Refactor

    • Refactored tabs internals to track the tabs list element via state and update indicator measurements accordingly.

- Accordion.Panel / Collapsible.Panel: reset flex/grid alignment before
reading scrollHeight/scrollWidth so non-default alignment can't truncate
the measured content size (new resetLayoutStyles util).
- Accordion.Panel / Collapsible.Panel: drop the pinned pixel size once the
open animation settles (panel flows at auto) and re-pin a measured value
when closing starts so exit transitions still animate.
- Tabs.Indicator: measure in useLayoutEffect (before paint) to remove a
one-frame stale position on tab change. Track the list element as context
state so the indicator's layout effect can read it (a child layout effect
runs before the parent ref attaches).
@changeset-bot

changeset-botBot commented Jun 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b33c1db

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

vercelBot commented Jun 17, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreview, CommentJun 18, 2026 1:57pm
swingsetReadyReadyPreview, CommentJun 18, 2026 1:57pm

Request Review

@coderabbitai

coderabbitaiBot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 99c52d0b-49ba-4306-a940-de48b05be979

📥 Commits

Reviewing files that changed from the base of the PR and between 17090d3 and a989491.

📒 Files selected for processing (1)
  • packages/headless/src/utils/reset-layout-styles.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/headless/src/utils/reset-layout-styles.ts

📝 Walkthrough

Walkthrough

Adds a resetLayoutStyles utility that temporarily forces flex/grid alignment properties to initial !important before scroll-dimension measurement and restores them on the next animation frame. Wires this into AccordionPanel and CollapsiblePanel to pin pixel heights during close transitions and clear them after open animations settle. Separately migrates the Tabs list element from a useRef to useState so TabsIndicator can measure synchronously with useLayoutEffect.

Changes

Panel Height Measurement & Animation Lifecycle

Layer / File(s)Summary
resetLayoutStyles utility
packages/headless/src/utils/reset-layout-styles.ts, packages/headless/src/utils/index.ts
New resetLayoutStyles(element) helper captures original inline alignment styles (justify-content, align-items, align-content, justify-items), forces them to initial !important for accurate scrollHeight/scrollWidth reads, restores originals on the next animation frame, and returns an immediate-cleanup callback. Re-exported from the utils index.
AccordionPanel measurement & transition lifecycle
packages/headless/src/primitives/accordion/accordion-panel.tsx
Integrates resetLayoutStyles into the ResizeObserver path to normalize flex/grid alignment before each height measurement. Captures transitionStatus and runOnAnimationsFinished to orchestrate clearing the pinned pixel height after open animations finish and re-pinning it when a close transition enters the 'ending' status. ResizeObserver cleanup now restores layout styles.
AccordionPanel animation lifecycle tests
packages/headless/src/primitives/accordion/accordion.test.tsx
Replaces the prior single assertion with two tests: one mocks Element.prototype.getAnimations to simulate an in-progress open animation and asserts --cl-accordion-panel-height is present; the other renders open without mocked animations and asserts the variable is absent after settling.
CollapsiblePanel measurement & transition lifecycle
packages/headless/src/primitives/collapsible/collapsible-panel.tsx
Same resetLayoutStyles + useAnimationsFinished integration as accordion, extended to both height and width dimensions. Clears pinned values after open animations settle, re-pins both axes when closing begins, and restores layout styles in ResizeObserver cleanup.
CollapsiblePanel animation lifecycle tests
packages/headless/src/primitives/collapsible/collapsible.test.tsx
Replaces prior assertions with two tests: one verifies both --collapsible-panel-height and --collapsible-panel-width are set while an open animation is mocked as pending; the other verifies both are absent from inline style once settled open.

Tabs List Ref → State Migration

Layer / File(s)Summary
TabsContextValue contract: ref → state
packages/headless/src/primitives/tabs/tabs-context.ts
TabsContextValue drops listRef: React.RefObject<HTMLElement> and adds listElement: HTMLElement | null plus setListElement: (element: HTMLElement | null) => void, with comments explaining why state (rather than a ref) is needed for measurement timing.
TabsRoot state + TabsList ref wiring
packages/headless/src/primitives/tabs/tabs-root.tsx, packages/headless/src/primitives/tabs/tabs-list.tsx
TabsRoot switches from useRef to useState for the list element, stores it in state, and provides both listElement and setListElement through the context provider with updated dependencies. TabsList passes setListElement as the Composite ref callback.
TabsIndicator synchronous measurement
packages/headless/src/primitives/tabs/tabs-indicator.tsx
Converts the indicator positioning effect from useEffect to useLayoutEffect and reads listElement directly from context instead of accessing listRef.current, eliminating the ref indirection and updating the dependency array to trigger on listElement changes.

Sequence Diagram(s)

sequenceDiagram
participant ResizeObserver
participant AccordionPanel
participant resetLayoutStyles
participant useAnimationsFinished
ResizeObserver->>AccordionPanel: observe size changes
AccordionPanel->>resetLayoutStyles: call with panel element
resetLayoutStyles->>resetLayoutStyles: capture align styles + priorities
resetLayoutStyles->>AccordionPanel: force align to initial !important
AccordionPanel->>AccordionPanel: read scrollHeight
AccordionPanel->>resetLayoutStyles: schedule restore on next frame
AccordionPanel->>AccordionPanel: set height = px value
Note over AccordionPanel,useAnimationsFinished: Open animation in progress
useAnimationsFinished->>AccordionPanel: animation finished
AccordionPanel->>AccordionPanel: clear height (undefined)
Note over AccordionPanel,useAnimationsFinished: User closes panel
AccordionPanel->>resetLayoutStyles: call with panel element (re-pin)
resetLayoutStyles->>AccordionPanel: force align to initial !important
AccordionPanel->>AccordionPanel: set height = px value
AccordionPanel->>AccordionPanel: play close animation
Loading
sequenceDiagram
participant TabsRoot
participant TabsContext
participant TabsList
participant TabsIndicator
TabsRoot->>TabsRoot: useState(listElement)
TabsRoot->>TabsContext: provide listElement + setListElement
TabsList->>TabsContext: consume setListElement
TabsList->>TabsList: pass setListElement to Composite ref
Note over TabsList,TabsIndicator: Tab list element mounted
TabsList->>TabsContext: setListElement(composite element)
TabsContext->>TabsIndicator: listElement updated
TabsIndicator->>TabsIndicator: useLayoutEffect observes listElement
TabsIndicator->>TabsIndicator: measure active tab bounding rect
TabsIndicator->>TabsIndicator: update indicator position sync
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • clerk/javascript#8475: Modifies accordion-panel.tsx and the --cl-accordion-panel-height CSS variable/testing, directly overlapping with the accordion animation lifecycle changes in this PR.
  • clerk/javascript#8474: Main PR updates to accordion-panel.tsx and collapsible-panel.tsx integrate the animation lifecycle via the useAnimationsFinished/useTransition APIs (including transitionStatus) that are foundational to this PR's implementation.

Suggested reviewers

  • wobsoriano
  • austincalvelage
  • Ephem

Poem

🐇 Hop, hop! The panel heights now know their place,
No frozen flex tricks left to leave a trace.
I measure true with resetLayoutStyles neat,
Then auto flows once open—oh so sweet!
The tabs list lives in state for sync delight,
And indicators snap in perfect sight. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 20.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the three main fixes: correcting panel measurement and improving tabs indicator timing, which are the primary changes across accordion, collapsible, and tabs components.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

@pkg-pr-new

pkg-pr-newBot commented Jun 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8897

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8897

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8897

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8897

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@8897

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8897

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8897

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8897

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8897

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8897

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8897

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8897

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8897

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8897

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8897

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8897

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8897

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8897

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8897

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8897

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8897

commit: b33c1db

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/headless/src/primitives/accordion/accordion.test.tsx (1)

194-238: ⚡ Quick win

Add a close-transition test for re-pinning height.

The updated suite validates open-path pin/clear behavior, but it still doesn’t assert the new close-path contract (re-pin measured height when closing starts). A focused test here would lock down the exact behavior this PR adds.

💡 Suggested test shape
+ it('re-pins --cl-accordion-panel-height when close transition starts', async () => {+ const user = userEvent.setup();+ renderAccordion({ defaultValue: ['item1'] });++ const original = (Element.prototype as { getAnimations?: unknown }).getAnimations;+ (Element.prototype as { getAnimations?: unknown }).getAnimations = () => [+ { finished: new Promise<void>(() => {}) },+ ];+ try {+ await user.click(screen.getByRole('button', { name: 'Section 1' }));+ const panel = document.querySelector('[data-cl-slot="accordion-panel"]') as HTMLElement;+ expect(panel.getAttribute('style') ?? '').toContain('--cl-accordion-panel-height');+ } finally {+ if (original) {+ (Element.prototype as { getAnimations?: unknown }).getAnimations = original;+ } else {+ delete (Element.prototype as { getAnimations?: unknown }).getAnimations;+ }+ }+ });

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx} should “Implement comprehensive testing including unit, integration, and E2E tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/headless/src/primitives/accordion/accordion.test.tsx` around lines
194 - 238, The animation lifecycle test suite validates the open-path behavior
for height pinning and clearing, but is missing a test case for the close-path
behavior. Add a new test within the 'animation lifecycle' describe block that
validates the height re-pinning behavior when closing starts. This test should
open an accordion panel (using defaultValue), then simulate closing it and
verify that the --cl-accordion-panel-height CSS variable is re-pinned to the
measured pixel height during the close animation phase, similar to the pattern
used in the existing 'pins --cl-accordion-panel-height while the open animation
is in progress' test case.

Source: Coding guidelines

packages/headless/src/primitives/collapsible/collapsible.test.tsx (1)

142-189: ⚡ Quick win

Add a close-transition test that asserts both dimensions are re-pinned.

This suite now covers open-path pin/clear behavior, but it doesn’t directly test the new close-path re-pin logic (--collapsible-panel-height/--collapsible-panel-width when closing starts). Adding that case will better protect this PR’s core behavior.

💡 Suggested test shape
+ it('re-pins height/width vars when close transition starts', async () => {+ const user = userEvent.setup();+ renderCollapsible({ defaultOpen: true });++ const original = (Element.prototype as { getAnimations?: unknown }).getAnimations;+ (Element.prototype as { getAnimations?: unknown }).getAnimations = () => [+ { finished: new Promise<void>(() => {}) },+ ];+ try {+ await user.click(screen.getByRole('button', { name: 'Toggle' }));+ const panel = document.querySelector('[data-cl-slot="collapsible-panel"]') as HTMLElement;+ const style = panel.getAttribute('style') ?? '';+ expect(style).toContain('--collapsible-panel-height');+ expect(style).toContain('--collapsible-panel-width');+ } finally {+ if (original) {+ (Element.prototype as { getAnimations?: unknown }).getAnimations = original;+ } else {+ delete (Element.prototype as { getAnimations?: unknown }).getAnimations;+ }+ }+ });

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx} should “Implement comprehensive testing including unit, integration, and E2E tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/headless/src/primitives/collapsible/collapsible.test.tsx` around
lines 142 - 189, Add a new test case in the 'animation lifecycle' describe block
that verifies the close-transition re-pin behavior. The test should render the
collapsible in an open state (using defaultOpen: true), then trigger the close
action, and assert that the --collapsible-panel-height and
--collapsible-panel-width CSS variables are re-pinned on the panel element (with
the data-cl-slot="collapsible-panel" selector) during the closing animation.
This mirrors the existing "pins --collapsible-panel-height/width while the open
animation is in progress" test but for the close path, ensuring that both
dimensions are preserved while the close animation is in progress before being
cleared once the animation completes.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/headless/src/utils/reset-layout-styles.ts`:
- Around line 22-40: The originalLayoutStyles object only captures the property
values but not their priority flags (the !important declaration). When restoring
in the restore() function, this causes properties that originally had !important
to be re-applied without it, changing layout behavior. Modify the code to
capture both the value and priority using element.style.getPropertyPriority(key)
for each key in originalLayoutStyles, then update the restore() function to pass
the captured priority as the third parameter to element.style.setProperty() when
re-applying the values, ensuring the !important flag is preserved if it was
originally present.
---
Nitpick comments:
In `@packages/headless/src/primitives/accordion/accordion.test.tsx`:
- Around line 194-238: The animation lifecycle test suite validates the
open-path behavior for height pinning and clearing, but is missing a test case
for the close-path behavior. Add a new test within the 'animation lifecycle'
describe block that validates the height re-pinning behavior when closing
starts. This test should open an accordion panel (using defaultValue), then
simulate closing it and verify that the --cl-accordion-panel-height CSS variable
is re-pinned to the measured pixel height during the close animation phase,
similar to the pattern used in the existing 'pins --cl-accordion-panel-height
while the open animation is in progress' test case.
In `@packages/headless/src/primitives/collapsible/collapsible.test.tsx`:
- Around line 142-189: Add a new test case in the 'animation lifecycle' describe
block that verifies the close-transition re-pin behavior. The test should render
the collapsible in an open state (using defaultOpen: true), then trigger the
close action, and assert that the --collapsible-panel-height and
--collapsible-panel-width CSS variables are re-pinned on the panel element (with
the data-cl-slot="collapsible-panel" selector) during the closing animation.
This mirrors the existing "pins --collapsible-panel-height/width while the open
animation is in progress" test but for the close path, ensuring that both
dimensions are preserved while the close animation is in progress before being
cleared once the animation completes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 3c30d6de-3161-4e3c-a8d1-3ac274415bf5

📥 Commits

Reviewing files that changed from the base of the PR and between f4ecc13 and e810695.

📒 Files selected for processing (10)
  • packages/headless/src/primitives/accordion/accordion-panel.tsx
  • packages/headless/src/primitives/accordion/accordion.test.tsx
  • packages/headless/src/primitives/collapsible/collapsible-panel.tsx
  • packages/headless/src/primitives/collapsible/collapsible.test.tsx
  • packages/headless/src/primitives/tabs/tabs-context.ts
  • packages/headless/src/primitives/tabs/tabs-indicator.tsx
  • packages/headless/src/primitives/tabs/tabs-list.tsx
  • packages/headless/src/primitives/tabs/tabs-root.tsx
  • packages/headless/src/utils/index.ts
  • packages/headless/src/utils/reset-layout-styles.ts

Comment threadpackages/headless/src/utils/reset-layout-styles.ts Outdated
@alexcarpenter
alexcarpenter merged commit fc578e3 into mainJun 18, 2026
45 checks passed
@alexcarpenter
alexcarpenter deleted the carp/headless-measurement-gaps branch June 18, 2026 15:25
Sign up for freeto 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

@alexcarpenter@kylemac
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(headless): correct panel measurement and tabs indicator timing by alexcarpenter · Pull Request #8897 · clerk/javascript · GitHub
Skip to content

fix(headless): correct panel measurement and tabs indicator timing - #8897

Merged
alexcarpenter merged 4 commits into
mainfrom
carp/headless-measurement-gaps
Jun 18, 2026
Merged

fix(headless): correct panel measurement and tabs indicator timing#8897
alexcarpenter merged 4 commits into
mainfrom
carp/headless-measurement-gaps

Conversation

@alexcarpenter

@alexcarpenteralexcarpenter commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

Three correctness fixes in @clerk/headless measurement primitives, identified by comparing against base-ui's useCollapsiblePanel.

1. Reset flex/grid alignment before measuring

Accordion.Panel and Collapsible.Panel read scrollHeight/scrollWidth directly. With display: flex/grid and non-default justify-content / align-items / align-content / justify-items, alignment can make the scroll size report smaller than the true content size. A new resetLayoutStyles util forces those properties to initial before measuring and restores them on the next frame.

2. Flow at auto when idle, re-pin on close

Once the open animation settles, the panels now drop the measured pixel size (CSS vars omitted → height: auto) so content flows naturally, and re-pin a measured value when the close transition starts so exit animations still have a concrete value to animate from.

3. Tabs.Indicator measures before paint

Switched the indicator measurement from useEffect to useLayoutEffect to remove a one-frame stale indicator position on tab change. This required tracking the tab-list element as context state (not a useRef): a child's layout effect runs before its parent's ref is attached, so the prior useEffect only worked by accident (passive effects run after all refs attach). Same pattern base-ui uses.

Testing

  • @clerk/headless unit suite passes (404 tests), typecheck clean.

Summary by CodeRabbit

Summary

  • Bug Fixes

    • Improved accordion and collapsible panel open/close animations by refining how sizes are measured and restored across transitions.
    • Updated tabs indicator to reposition synchronously during tab changes to prevent brief misalignment.
  • Tests

    • Strengthened accordion and collapsible animation lifecycle tests to verify pinned dimensions during active transitions and clearing after completion.
  • Refactor

    • Refactored tabs internals to track the tabs list element via state and update indicator measurements accordingly.

- Accordion.Panel / Collapsible.Panel: reset flex/grid alignment before
reading scrollHeight/scrollWidth so non-default alignment can't truncate
the measured content size (new resetLayoutStyles util).
- Accordion.Panel / Collapsible.Panel: drop the pinned pixel size once the
open animation settles (panel flows at auto) and re-pin a measured value
when closing starts so exit transitions still animate.
- Tabs.Indicator: measure in useLayoutEffect (before paint) to remove a
one-frame stale position on tab change. Track the list element as context
state so the indicator's layout effect can read it (a child layout effect
runs before the parent ref attaches).
@changeset-bot

changeset-botBot commented Jun 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b33c1db

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

vercelBot commented Jun 17, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreview, CommentJun 18, 2026 1:57pm
swingsetReadyReadyPreview, CommentJun 18, 2026 1:57pm

Request Review

@coderabbitai

coderabbitaiBot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 99c52d0b-49ba-4306-a940-de48b05be979

📥 Commits

Reviewing files that changed from the base of the PR and between 17090d3 and a989491.

📒 Files selected for processing (1)
  • packages/headless/src/utils/reset-layout-styles.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/headless/src/utils/reset-layout-styles.ts

📝 Walkthrough

Walkthrough

Adds a resetLayoutStyles utility that temporarily forces flex/grid alignment properties to initial !important before scroll-dimension measurement and restores them on the next animation frame. Wires this into AccordionPanel and CollapsiblePanel to pin pixel heights during close transitions and clear them after open animations settle. Separately migrates the Tabs list element from a useRef to useState so TabsIndicator can measure synchronously with useLayoutEffect.

Changes

Panel Height Measurement & Animation Lifecycle

Layer / File(s)Summary
resetLayoutStyles utility
packages/headless/src/utils/reset-layout-styles.ts, packages/headless/src/utils/index.ts
New resetLayoutStyles(element) helper captures original inline alignment styles (justify-content, align-items, align-content, justify-items), forces them to initial !important for accurate scrollHeight/scrollWidth reads, restores originals on the next animation frame, and returns an immediate-cleanup callback. Re-exported from the utils index.
AccordionPanel measurement & transition lifecycle
packages/headless/src/primitives/accordion/accordion-panel.tsx
Integrates resetLayoutStyles into the ResizeObserver path to normalize flex/grid alignment before each height measurement. Captures transitionStatus and runOnAnimationsFinished to orchestrate clearing the pinned pixel height after open animations finish and re-pinning it when a close transition enters the 'ending' status. ResizeObserver cleanup now restores layout styles.
AccordionPanel animation lifecycle tests
packages/headless/src/primitives/accordion/accordion.test.tsx
Replaces the prior single assertion with two tests: one mocks Element.prototype.getAnimations to simulate an in-progress open animation and asserts --cl-accordion-panel-height is present; the other renders open without mocked animations and asserts the variable is absent after settling.
CollapsiblePanel measurement & transition lifecycle
packages/headless/src/primitives/collapsible/collapsible-panel.tsx
Same resetLayoutStyles + useAnimationsFinished integration as accordion, extended to both height and width dimensions. Clears pinned values after open animations settle, re-pins both axes when closing begins, and restores layout styles in ResizeObserver cleanup.
CollapsiblePanel animation lifecycle tests
packages/headless/src/primitives/collapsible/collapsible.test.tsx
Replaces prior assertions with two tests: one verifies both --collapsible-panel-height and --collapsible-panel-width are set while an open animation is mocked as pending; the other verifies both are absent from inline style once settled open.

Tabs List Ref → State Migration

Layer / File(s)Summary
TabsContextValue contract: ref → state
packages/headless/src/primitives/tabs/tabs-context.ts
TabsContextValue drops listRef: React.RefObject<HTMLElement> and adds listElement: HTMLElement | null plus setListElement: (element: HTMLElement | null) => void, with comments explaining why state (rather than a ref) is needed for measurement timing.
TabsRoot state + TabsList ref wiring
packages/headless/src/primitives/tabs/tabs-root.tsx, packages/headless/src/primitives/tabs/tabs-list.tsx
TabsRoot switches from useRef to useState for the list element, stores it in state, and provides both listElement and setListElement through the context provider with updated dependencies. TabsList passes setListElement as the Composite ref callback.
TabsIndicator synchronous measurement
packages/headless/src/primitives/tabs/tabs-indicator.tsx
Converts the indicator positioning effect from useEffect to useLayoutEffect and reads listElement directly from context instead of accessing listRef.current, eliminating the ref indirection and updating the dependency array to trigger on listElement changes.

Sequence Diagram(s)

sequenceDiagram
participant ResizeObserver
participant AccordionPanel
participant resetLayoutStyles
participant useAnimationsFinished
ResizeObserver->>AccordionPanel: observe size changes
AccordionPanel->>resetLayoutStyles: call with panel element
resetLayoutStyles->>resetLayoutStyles: capture align styles + priorities
resetLayoutStyles->>AccordionPanel: force align to initial !important
AccordionPanel->>AccordionPanel: read scrollHeight
AccordionPanel->>resetLayoutStyles: schedule restore on next frame
AccordionPanel->>AccordionPanel: set height = px value
Note over AccordionPanel,useAnimationsFinished: Open animation in progress
useAnimationsFinished->>AccordionPanel: animation finished
AccordionPanel->>AccordionPanel: clear height (undefined)
Note over AccordionPanel,useAnimationsFinished: User closes panel
AccordionPanel->>resetLayoutStyles: call with panel element (re-pin)
resetLayoutStyles->>AccordionPanel: force align to initial !important
AccordionPanel->>AccordionPanel: set height = px value
AccordionPanel->>AccordionPanel: play close animation
Loading
sequenceDiagram
participant TabsRoot
participant TabsContext
participant TabsList
participant TabsIndicator
TabsRoot->>TabsRoot: useState(listElement)
TabsRoot->>TabsContext: provide listElement + setListElement
TabsList->>TabsContext: consume setListElement
TabsList->>TabsList: pass setListElement to Composite ref
Note over TabsList,TabsIndicator: Tab list element mounted
TabsList->>TabsContext: setListElement(composite element)
TabsContext->>TabsIndicator: listElement updated
TabsIndicator->>TabsIndicator: useLayoutEffect observes listElement
TabsIndicator->>TabsIndicator: measure active tab bounding rect
TabsIndicator->>TabsIndicator: update indicator position sync
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • clerk/javascript#8475: Modifies accordion-panel.tsx and the --cl-accordion-panel-height CSS variable/testing, directly overlapping with the accordion animation lifecycle changes in this PR.
  • clerk/javascript#8474: Main PR updates to accordion-panel.tsx and collapsible-panel.tsx integrate the animation lifecycle via the useAnimationsFinished/useTransition APIs (including transitionStatus) that are foundational to this PR's implementation.

Suggested reviewers

  • wobsoriano
  • austincalvelage
  • Ephem

Poem

🐇 Hop, hop! The panel heights now know their place,
No frozen flex tricks left to leave a trace.
I measure true with resetLayoutStyles neat,
Then auto flows once open—oh so sweet!
The tabs list lives in state for sync delight,
And indicators snap in perfect sight. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 20.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the three main fixes: correcting panel measurement and improving tabs indicator timing, which are the primary changes across accordion, collapsible, and tabs components.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

@pkg-pr-new

pkg-pr-newBot commented Jun 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8897

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8897

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8897

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8897

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@8897

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8897

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8897

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8897

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8897

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8897

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8897

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8897

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8897

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8897

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8897

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8897

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8897

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8897

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8897

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8897

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8897

commit: b33c1db

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/headless/src/primitives/accordion/accordion.test.tsx (1)

194-238: ⚡ Quick win

Add a close-transition test for re-pinning height.

The updated suite validates open-path pin/clear behavior, but it still doesn’t assert the new close-path contract (re-pin measured height when closing starts). A focused test here would lock down the exact behavior this PR adds.

💡 Suggested test shape
+ it('re-pins --cl-accordion-panel-height when close transition starts', async () => {+ const user = userEvent.setup();+ renderAccordion({ defaultValue: ['item1'] });++ const original = (Element.prototype as { getAnimations?: unknown }).getAnimations;+ (Element.prototype as { getAnimations?: unknown }).getAnimations = () => [+ { finished: new Promise<void>(() => {}) },+ ];+ try {+ await user.click(screen.getByRole('button', { name: 'Section 1' }));+ const panel = document.querySelector('[data-cl-slot="accordion-panel"]') as HTMLElement;+ expect(panel.getAttribute('style') ?? '').toContain('--cl-accordion-panel-height');+ } finally {+ if (original) {+ (Element.prototype as { getAnimations?: unknown }).getAnimations = original;+ } else {+ delete (Element.prototype as { getAnimations?: unknown }).getAnimations;+ }+ }+ });

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx} should “Implement comprehensive testing including unit, integration, and E2E tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/headless/src/primitives/accordion/accordion.test.tsx` around lines
194 - 238, The animation lifecycle test suite validates the open-path behavior
for height pinning and clearing, but is missing a test case for the close-path
behavior. Add a new test within the 'animation lifecycle' describe block that
validates the height re-pinning behavior when closing starts. This test should
open an accordion panel (using defaultValue), then simulate closing it and
verify that the --cl-accordion-panel-height CSS variable is re-pinned to the
measured pixel height during the close animation phase, similar to the pattern
used in the existing 'pins --cl-accordion-panel-height while the open animation
is in progress' test case.

Source: Coding guidelines

packages/headless/src/primitives/collapsible/collapsible.test.tsx (1)

142-189: ⚡ Quick win

Add a close-transition test that asserts both dimensions are re-pinned.

This suite now covers open-path pin/clear behavior, but it doesn’t directly test the new close-path re-pin logic (--collapsible-panel-height/--collapsible-panel-width when closing starts). Adding that case will better protect this PR’s core behavior.

💡 Suggested test shape
+ it('re-pins height/width vars when close transition starts', async () => {+ const user = userEvent.setup();+ renderCollapsible({ defaultOpen: true });++ const original = (Element.prototype as { getAnimations?: unknown }).getAnimations;+ (Element.prototype as { getAnimations?: unknown }).getAnimations = () => [+ { finished: new Promise<void>(() => {}) },+ ];+ try {+ await user.click(screen.getByRole('button', { name: 'Toggle' }));+ const panel = document.querySelector('[data-cl-slot="collapsible-panel"]') as HTMLElement;+ const style = panel.getAttribute('style') ?? '';+ expect(style).toContain('--collapsible-panel-height');+ expect(style).toContain('--collapsible-panel-width');+ } finally {+ if (original) {+ (Element.prototype as { getAnimations?: unknown }).getAnimations = original;+ } else {+ delete (Element.prototype as { getAnimations?: unknown }).getAnimations;+ }+ }+ });

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx} should “Implement comprehensive testing including unit, integration, and E2E tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/headless/src/primitives/collapsible/collapsible.test.tsx` around
lines 142 - 189, Add a new test case in the 'animation lifecycle' describe block
that verifies the close-transition re-pin behavior. The test should render the
collapsible in an open state (using defaultOpen: true), then trigger the close
action, and assert that the --collapsible-panel-height and
--collapsible-panel-width CSS variables are re-pinned on the panel element (with
the data-cl-slot="collapsible-panel" selector) during the closing animation.
This mirrors the existing "pins --collapsible-panel-height/width while the open
animation is in progress" test but for the close path, ensuring that both
dimensions are preserved while the close animation is in progress before being
cleared once the animation completes.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/headless/src/utils/reset-layout-styles.ts`:
- Around line 22-40: The originalLayoutStyles object only captures the property
values but not their priority flags (the !important declaration). When restoring
in the restore() function, this causes properties that originally had !important
to be re-applied without it, changing layout behavior. Modify the code to
capture both the value and priority using element.style.getPropertyPriority(key)
for each key in originalLayoutStyles, then update the restore() function to pass
the captured priority as the third parameter to element.style.setProperty() when
re-applying the values, ensuring the !important flag is preserved if it was
originally present.
---
Nitpick comments:
In `@packages/headless/src/primitives/accordion/accordion.test.tsx`:
- Around line 194-238: The animation lifecycle test suite validates the
open-path behavior for height pinning and clearing, but is missing a test case
for the close-path behavior. Add a new test within the 'animation lifecycle'
describe block that validates the height re-pinning behavior when closing
starts. This test should open an accordion panel (using defaultValue), then
simulate closing it and verify that the --cl-accordion-panel-height CSS variable
is re-pinned to the measured pixel height during the close animation phase,
similar to the pattern used in the existing 'pins --cl-accordion-panel-height
while the open animation is in progress' test case.
In `@packages/headless/src/primitives/collapsible/collapsible.test.tsx`:
- Around line 142-189: Add a new test case in the 'animation lifecycle' describe
block that verifies the close-transition re-pin behavior. The test should render
the collapsible in an open state (using defaultOpen: true), then trigger the
close action, and assert that the --collapsible-panel-height and
--collapsible-panel-width CSS variables are re-pinned on the panel element (with
the data-cl-slot="collapsible-panel" selector) during the closing animation.
This mirrors the existing "pins --collapsible-panel-height/width while the open
animation is in progress" test but for the close path, ensuring that both
dimensions are preserved while the close animation is in progress before being
cleared once the animation completes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 3c30d6de-3161-4e3c-a8d1-3ac274415bf5

📥 Commits

Reviewing files that changed from the base of the PR and between f4ecc13 and e810695.

📒 Files selected for processing (10)
  • packages/headless/src/primitives/accordion/accordion-panel.tsx
  • packages/headless/src/primitives/accordion/accordion.test.tsx
  • packages/headless/src/primitives/collapsible/collapsible-panel.tsx
  • packages/headless/src/primitives/collapsible/collapsible.test.tsx
  • packages/headless/src/primitives/tabs/tabs-context.ts
  • packages/headless/src/primitives/tabs/tabs-indicator.tsx
  • packages/headless/src/primitives/tabs/tabs-list.tsx
  • packages/headless/src/primitives/tabs/tabs-root.tsx
  • packages/headless/src/utils/index.ts
  • packages/headless/src/utils/reset-layout-styles.ts

Comment threadpackages/headless/src/utils/reset-layout-styles.ts Outdated
@alexcarpenter
alexcarpenter merged commit fc578e3 into mainJun 18, 2026
45 checks passed
@alexcarpenter
alexcarpenter deleted the carp/headless-measurement-gaps branch June 18, 2026 15:25
Sign up for freeto 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

@alexcarpenter@kylemac
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(headless): correct panel measurement and tabs indicator timing by alexcarpenter · Pull Request #8897 · clerk/javascript · GitHub
Skip to content

fix(headless): correct panel measurement and tabs indicator timing - #8897

Merged
alexcarpenter merged 4 commits into
mainfrom
carp/headless-measurement-gaps
Jun 18, 2026
Merged

fix(headless): correct panel measurement and tabs indicator timing#8897
alexcarpenter merged 4 commits into
mainfrom
carp/headless-measurement-gaps

Conversation

@alexcarpenter

@alexcarpenteralexcarpenter commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

Three correctness fixes in @clerk/headless measurement primitives, identified by comparing against base-ui's useCollapsiblePanel.

1. Reset flex/grid alignment before measuring

Accordion.Panel and Collapsible.Panel read scrollHeight/scrollWidth directly. With display: flex/grid and non-default justify-content / align-items / align-content / justify-items, alignment can make the scroll size report smaller than the true content size. A new resetLayoutStyles util forces those properties to initial before measuring and restores them on the next frame.

2. Flow at auto when idle, re-pin on close

Once the open animation settles, the panels now drop the measured pixel size (CSS vars omitted → height: auto) so content flows naturally, and re-pin a measured value when the close transition starts so exit animations still have a concrete value to animate from.

3. Tabs.Indicator measures before paint

Switched the indicator measurement from useEffect to useLayoutEffect to remove a one-frame stale indicator position on tab change. This required tracking the tab-list element as context state (not a useRef): a child's layout effect runs before its parent's ref is attached, so the prior useEffect only worked by accident (passive effects run after all refs attach). Same pattern base-ui uses.

Testing

  • @clerk/headless unit suite passes (404 tests), typecheck clean.

Summary by CodeRabbit

Summary

  • Bug Fixes

    • Improved accordion and collapsible panel open/close animations by refining how sizes are measured and restored across transitions.
    • Updated tabs indicator to reposition synchronously during tab changes to prevent brief misalignment.
  • Tests

    • Strengthened accordion and collapsible animation lifecycle tests to verify pinned dimensions during active transitions and clearing after completion.
  • Refactor

    • Refactored tabs internals to track the tabs list element via state and update indicator measurements accordingly.

- Accordion.Panel / Collapsible.Panel: reset flex/grid alignment before
reading scrollHeight/scrollWidth so non-default alignment can't truncate
the measured content size (new resetLayoutStyles util).
- Accordion.Panel / Collapsible.Panel: drop the pinned pixel size once the
open animation settles (panel flows at auto) and re-pin a measured value
when closing starts so exit transitions still animate.
- Tabs.Indicator: measure in useLayoutEffect (before paint) to remove a
one-frame stale position on tab change. Track the list element as context
state so the indicator's layout effect can read it (a child layout effect
runs before the parent ref attaches).
@changeset-bot

changeset-botBot commented Jun 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b33c1db

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

vercelBot commented Jun 17, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreview, CommentJun 18, 2026 1:57pm
swingsetReadyReadyPreview, CommentJun 18, 2026 1:57pm

Request Review

@coderabbitai

coderabbitaiBot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 99c52d0b-49ba-4306-a940-de48b05be979

📥 Commits

Reviewing files that changed from the base of the PR and between 17090d3 and a989491.

📒 Files selected for processing (1)
  • packages/headless/src/utils/reset-layout-styles.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/headless/src/utils/reset-layout-styles.ts

📝 Walkthrough

Walkthrough

Adds a resetLayoutStyles utility that temporarily forces flex/grid alignment properties to initial !important before scroll-dimension measurement and restores them on the next animation frame. Wires this into AccordionPanel and CollapsiblePanel to pin pixel heights during close transitions and clear them after open animations settle. Separately migrates the Tabs list element from a useRef to useState so TabsIndicator can measure synchronously with useLayoutEffect.

Changes

Panel Height Measurement & Animation Lifecycle

Layer / File(s)Summary
resetLayoutStyles utility
packages/headless/src/utils/reset-layout-styles.ts, packages/headless/src/utils/index.ts
New resetLayoutStyles(element) helper captures original inline alignment styles (justify-content, align-items, align-content, justify-items), forces them to initial !important for accurate scrollHeight/scrollWidth reads, restores originals on the next animation frame, and returns an immediate-cleanup callback. Re-exported from the utils index.
AccordionPanel measurement & transition lifecycle
packages/headless/src/primitives/accordion/accordion-panel.tsx
Integrates resetLayoutStyles into the ResizeObserver path to normalize flex/grid alignment before each height measurement. Captures transitionStatus and runOnAnimationsFinished to orchestrate clearing the pinned pixel height after open animations finish and re-pinning it when a close transition enters the 'ending' status. ResizeObserver cleanup now restores layout styles.
AccordionPanel animation lifecycle tests
packages/headless/src/primitives/accordion/accordion.test.tsx
Replaces the prior single assertion with two tests: one mocks Element.prototype.getAnimations to simulate an in-progress open animation and asserts --cl-accordion-panel-height is present; the other renders open without mocked animations and asserts the variable is absent after settling.
CollapsiblePanel measurement & transition lifecycle
packages/headless/src/primitives/collapsible/collapsible-panel.tsx
Same resetLayoutStyles + useAnimationsFinished integration as accordion, extended to both height and width dimensions. Clears pinned values after open animations settle, re-pins both axes when closing begins, and restores layout styles in ResizeObserver cleanup.
CollapsiblePanel animation lifecycle tests
packages/headless/src/primitives/collapsible/collapsible.test.tsx
Replaces prior assertions with two tests: one verifies both --collapsible-panel-height and --collapsible-panel-width are set while an open animation is mocked as pending; the other verifies both are absent from inline style once settled open.

Tabs List Ref → State Migration

Layer / File(s)Summary
TabsContextValue contract: ref → state
packages/headless/src/primitives/tabs/tabs-context.ts
TabsContextValue drops listRef: React.RefObject<HTMLElement> and adds listElement: HTMLElement | null plus setListElement: (element: HTMLElement | null) => void, with comments explaining why state (rather than a ref) is needed for measurement timing.
TabsRoot state + TabsList ref wiring
packages/headless/src/primitives/tabs/tabs-root.tsx, packages/headless/src/primitives/tabs/tabs-list.tsx
TabsRoot switches from useRef to useState for the list element, stores it in state, and provides both listElement and setListElement through the context provider with updated dependencies. TabsList passes setListElement as the Composite ref callback.
TabsIndicator synchronous measurement
packages/headless/src/primitives/tabs/tabs-indicator.tsx
Converts the indicator positioning effect from useEffect to useLayoutEffect and reads listElement directly from context instead of accessing listRef.current, eliminating the ref indirection and updating the dependency array to trigger on listElement changes.

Sequence Diagram(s)

sequenceDiagram
participant ResizeObserver
participant AccordionPanel
participant resetLayoutStyles
participant useAnimationsFinished
ResizeObserver->>AccordionPanel: observe size changes
AccordionPanel->>resetLayoutStyles: call with panel element
resetLayoutStyles->>resetLayoutStyles: capture align styles + priorities
resetLayoutStyles->>AccordionPanel: force align to initial !important
AccordionPanel->>AccordionPanel: read scrollHeight
AccordionPanel->>resetLayoutStyles: schedule restore on next frame
AccordionPanel->>AccordionPanel: set height = px value
Note over AccordionPanel,useAnimationsFinished: Open animation in progress
useAnimationsFinished->>AccordionPanel: animation finished
AccordionPanel->>AccordionPanel: clear height (undefined)
Note over AccordionPanel,useAnimationsFinished: User closes panel
AccordionPanel->>resetLayoutStyles: call with panel element (re-pin)
resetLayoutStyles->>AccordionPanel: force align to initial !important
AccordionPanel->>AccordionPanel: set height = px value
AccordionPanel->>AccordionPanel: play close animation
Loading
sequenceDiagram
participant TabsRoot
participant TabsContext
participant TabsList
participant TabsIndicator
TabsRoot->>TabsRoot: useState(listElement)
TabsRoot->>TabsContext: provide listElement + setListElement
TabsList->>TabsContext: consume setListElement
TabsList->>TabsList: pass setListElement to Composite ref
Note over TabsList,TabsIndicator: Tab list element mounted
TabsList->>TabsContext: setListElement(composite element)
TabsContext->>TabsIndicator: listElement updated
TabsIndicator->>TabsIndicator: useLayoutEffect observes listElement
TabsIndicator->>TabsIndicator: measure active tab bounding rect
TabsIndicator->>TabsIndicator: update indicator position sync
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • clerk/javascript#8475: Modifies accordion-panel.tsx and the --cl-accordion-panel-height CSS variable/testing, directly overlapping with the accordion animation lifecycle changes in this PR.
  • clerk/javascript#8474: Main PR updates to accordion-panel.tsx and collapsible-panel.tsx integrate the animation lifecycle via the useAnimationsFinished/useTransition APIs (including transitionStatus) that are foundational to this PR's implementation.

Suggested reviewers

  • wobsoriano
  • austincalvelage
  • Ephem

Poem

🐇 Hop, hop! The panel heights now know their place,
No frozen flex tricks left to leave a trace.
I measure true with resetLayoutStyles neat,
Then auto flows once open—oh so sweet!
The tabs list lives in state for sync delight,
And indicators snap in perfect sight. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 20.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the three main fixes: correcting panel measurement and improving tabs indicator timing, which are the primary changes across accordion, collapsible, and tabs components.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

@pkg-pr-new

pkg-pr-newBot commented Jun 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8897

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8897

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8897

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8897

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@8897

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8897

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8897

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8897

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8897

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8897

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8897

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8897

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8897

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8897

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8897

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8897

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8897

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8897

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8897

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8897

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8897

commit: b33c1db

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/headless/src/primitives/accordion/accordion.test.tsx (1)

194-238: ⚡ Quick win

Add a close-transition test for re-pinning height.

The updated suite validates open-path pin/clear behavior, but it still doesn’t assert the new close-path contract (re-pin measured height when closing starts). A focused test here would lock down the exact behavior this PR adds.

💡 Suggested test shape
+ it('re-pins --cl-accordion-panel-height when close transition starts', async () => {+ const user = userEvent.setup();+ renderAccordion({ defaultValue: ['item1'] });++ const original = (Element.prototype as { getAnimations?: unknown }).getAnimations;+ (Element.prototype as { getAnimations?: unknown }).getAnimations = () => [+ { finished: new Promise<void>(() => {}) },+ ];+ try {+ await user.click(screen.getByRole('button', { name: 'Section 1' }));+ const panel = document.querySelector('[data-cl-slot="accordion-panel"]') as HTMLElement;+ expect(panel.getAttribute('style') ?? '').toContain('--cl-accordion-panel-height');+ } finally {+ if (original) {+ (Element.prototype as { getAnimations?: unknown }).getAnimations = original;+ } else {+ delete (Element.prototype as { getAnimations?: unknown }).getAnimations;+ }+ }+ });

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx} should “Implement comprehensive testing including unit, integration, and E2E tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/headless/src/primitives/accordion/accordion.test.tsx` around lines
194 - 238, The animation lifecycle test suite validates the open-path behavior
for height pinning and clearing, but is missing a test case for the close-path
behavior. Add a new test within the 'animation lifecycle' describe block that
validates the height re-pinning behavior when closing starts. This test should
open an accordion panel (using defaultValue), then simulate closing it and
verify that the --cl-accordion-panel-height CSS variable is re-pinned to the
measured pixel height during the close animation phase, similar to the pattern
used in the existing 'pins --cl-accordion-panel-height while the open animation
is in progress' test case.

Source: Coding guidelines

packages/headless/src/primitives/collapsible/collapsible.test.tsx (1)

142-189: ⚡ Quick win

Add a close-transition test that asserts both dimensions are re-pinned.

This suite now covers open-path pin/clear behavior, but it doesn’t directly test the new close-path re-pin logic (--collapsible-panel-height/--collapsible-panel-width when closing starts). Adding that case will better protect this PR’s core behavior.

💡 Suggested test shape
+ it('re-pins height/width vars when close transition starts', async () => {+ const user = userEvent.setup();+ renderCollapsible({ defaultOpen: true });++ const original = (Element.prototype as { getAnimations?: unknown }).getAnimations;+ (Element.prototype as { getAnimations?: unknown }).getAnimations = () => [+ { finished: new Promise<void>(() => {}) },+ ];+ try {+ await user.click(screen.getByRole('button', { name: 'Toggle' }));+ const panel = document.querySelector('[data-cl-slot="collapsible-panel"]') as HTMLElement;+ const style = panel.getAttribute('style') ?? '';+ expect(style).toContain('--collapsible-panel-height');+ expect(style).toContain('--collapsible-panel-width');+ } finally {+ if (original) {+ (Element.prototype as { getAnimations?: unknown }).getAnimations = original;+ } else {+ delete (Element.prototype as { getAnimations?: unknown }).getAnimations;+ }+ }+ });

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx} should “Implement comprehensive testing including unit, integration, and E2E tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/headless/src/primitives/collapsible/collapsible.test.tsx` around
lines 142 - 189, Add a new test case in the 'animation lifecycle' describe block
that verifies the close-transition re-pin behavior. The test should render the
collapsible in an open state (using defaultOpen: true), then trigger the close
action, and assert that the --collapsible-panel-height and
--collapsible-panel-width CSS variables are re-pinned on the panel element (with
the data-cl-slot="collapsible-panel" selector) during the closing animation.
This mirrors the existing "pins --collapsible-panel-height/width while the open
animation is in progress" test but for the close path, ensuring that both
dimensions are preserved while the close animation is in progress before being
cleared once the animation completes.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/headless/src/utils/reset-layout-styles.ts`:
- Around line 22-40: The originalLayoutStyles object only captures the property
values but not their priority flags (the !important declaration). When restoring
in the restore() function, this causes properties that originally had !important
to be re-applied without it, changing layout behavior. Modify the code to
capture both the value and priority using element.style.getPropertyPriority(key)
for each key in originalLayoutStyles, then update the restore() function to pass
the captured priority as the third parameter to element.style.setProperty() when
re-applying the values, ensuring the !important flag is preserved if it was
originally present.
---
Nitpick comments:
In `@packages/headless/src/primitives/accordion/accordion.test.tsx`:
- Around line 194-238: The animation lifecycle test suite validates the
open-path behavior for height pinning and clearing, but is missing a test case
for the close-path behavior. Add a new test within the 'animation lifecycle'
describe block that validates the height re-pinning behavior when closing
starts. This test should open an accordion panel (using defaultValue), then
simulate closing it and verify that the --cl-accordion-panel-height CSS variable
is re-pinned to the measured pixel height during the close animation phase,
similar to the pattern used in the existing 'pins --cl-accordion-panel-height
while the open animation is in progress' test case.
In `@packages/headless/src/primitives/collapsible/collapsible.test.tsx`:
- Around line 142-189: Add a new test case in the 'animation lifecycle' describe
block that verifies the close-transition re-pin behavior. The test should render
the collapsible in an open state (using defaultOpen: true), then trigger the
close action, and assert that the --collapsible-panel-height and
--collapsible-panel-width CSS variables are re-pinned on the panel element (with
the data-cl-slot="collapsible-panel" selector) during the closing animation.
This mirrors the existing "pins --collapsible-panel-height/width while the open
animation is in progress" test but for the close path, ensuring that both
dimensions are preserved while the close animation is in progress before being
cleared once the animation completes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 3c30d6de-3161-4e3c-a8d1-3ac274415bf5

📥 Commits

Reviewing files that changed from the base of the PR and between f4ecc13 and e810695.

📒 Files selected for processing (10)
  • packages/headless/src/primitives/accordion/accordion-panel.tsx
  • packages/headless/src/primitives/accordion/accordion.test.tsx
  • packages/headless/src/primitives/collapsible/collapsible-panel.tsx
  • packages/headless/src/primitives/collapsible/collapsible.test.tsx
  • packages/headless/src/primitives/tabs/tabs-context.ts
  • packages/headless/src/primitives/tabs/tabs-indicator.tsx
  • packages/headless/src/primitives/tabs/tabs-list.tsx
  • packages/headless/src/primitives/tabs/tabs-root.tsx
  • packages/headless/src/utils/index.ts
  • packages/headless/src/utils/reset-layout-styles.ts

Comment threadpackages/headless/src/utils/reset-layout-styles.ts Outdated
@alexcarpenter
alexcarpenter merged commit fc578e3 into mainJun 18, 2026
45 checks passed
@alexcarpenter
alexcarpenter deleted the carp/headless-measurement-gaps branch June 18, 2026 15:25
Sign up for freeto 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

@alexcarpenter@kylemac
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(headless): correct panel measurement and tabs indicator timing by alexcarpenter · Pull Request #8897 · clerk/javascript · GitHub
Skip to content

fix(headless): correct panel measurement and tabs indicator timing - #8897

Merged
alexcarpenter merged 4 commits into
mainfrom
carp/headless-measurement-gaps
Jun 18, 2026
Merged

fix(headless): correct panel measurement and tabs indicator timing#8897
alexcarpenter merged 4 commits into
mainfrom
carp/headless-measurement-gaps

Conversation

@alexcarpenter

@alexcarpenteralexcarpenter commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

Three correctness fixes in @clerk/headless measurement primitives, identified by comparing against base-ui's useCollapsiblePanel.

1. Reset flex/grid alignment before measuring

Accordion.Panel and Collapsible.Panel read scrollHeight/scrollWidth directly. With display: flex/grid and non-default justify-content / align-items / align-content / justify-items, alignment can make the scroll size report smaller than the true content size. A new resetLayoutStyles util forces those properties to initial before measuring and restores them on the next frame.

2. Flow at auto when idle, re-pin on close

Once the open animation settles, the panels now drop the measured pixel size (CSS vars omitted → height: auto) so content flows naturally, and re-pin a measured value when the close transition starts so exit animations still have a concrete value to animate from.

3. Tabs.Indicator measures before paint

Switched the indicator measurement from useEffect to useLayoutEffect to remove a one-frame stale indicator position on tab change. This required tracking the tab-list element as context state (not a useRef): a child's layout effect runs before its parent's ref is attached, so the prior useEffect only worked by accident (passive effects run after all refs attach). Same pattern base-ui uses.

Testing

  • @clerk/headless unit suite passes (404 tests), typecheck clean.

Summary by CodeRabbit

Summary

  • Bug Fixes

    • Improved accordion and collapsible panel open/close animations by refining how sizes are measured and restored across transitions.
    • Updated tabs indicator to reposition synchronously during tab changes to prevent brief misalignment.
  • Tests

    • Strengthened accordion and collapsible animation lifecycle tests to verify pinned dimensions during active transitions and clearing after completion.
  • Refactor

    • Refactored tabs internals to track the tabs list element via state and update indicator measurements accordingly.

- Accordion.Panel / Collapsible.Panel: reset flex/grid alignment before
reading scrollHeight/scrollWidth so non-default alignment can't truncate
the measured content size (new resetLayoutStyles util).
- Accordion.Panel / Collapsible.Panel: drop the pinned pixel size once the
open animation settles (panel flows at auto) and re-pin a measured value
when closing starts so exit transitions still animate.
- Tabs.Indicator: measure in useLayoutEffect (before paint) to remove a
one-frame stale position on tab change. Track the list element as context
state so the indicator's layout effect can read it (a child layout effect
runs before the parent ref attaches).
@changeset-bot

changeset-botBot commented Jun 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b33c1db

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

vercelBot commented Jun 17, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreview, CommentJun 18, 2026 1:57pm
swingsetReadyReadyPreview, CommentJun 18, 2026 1:57pm

Request Review

@coderabbitai

coderabbitaiBot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 99c52d0b-49ba-4306-a940-de48b05be979

📥 Commits

Reviewing files that changed from the base of the PR and between 17090d3 and a989491.

📒 Files selected for processing (1)
  • packages/headless/src/utils/reset-layout-styles.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/headless/src/utils/reset-layout-styles.ts

📝 Walkthrough

Walkthrough

Adds a resetLayoutStyles utility that temporarily forces flex/grid alignment properties to initial !important before scroll-dimension measurement and restores them on the next animation frame. Wires this into AccordionPanel and CollapsiblePanel to pin pixel heights during close transitions and clear them after open animations settle. Separately migrates the Tabs list element from a useRef to useState so TabsIndicator can measure synchronously with useLayoutEffect.

Changes

Panel Height Measurement & Animation Lifecycle

Layer / File(s)Summary
resetLayoutStyles utility
packages/headless/src/utils/reset-layout-styles.ts, packages/headless/src/utils/index.ts
New resetLayoutStyles(element) helper captures original inline alignment styles (justify-content, align-items, align-content, justify-items), forces them to initial !important for accurate scrollHeight/scrollWidth reads, restores originals on the next animation frame, and returns an immediate-cleanup callback. Re-exported from the utils index.
AccordionPanel measurement & transition lifecycle
packages/headless/src/primitives/accordion/accordion-panel.tsx
Integrates resetLayoutStyles into the ResizeObserver path to normalize flex/grid alignment before each height measurement. Captures transitionStatus and runOnAnimationsFinished to orchestrate clearing the pinned pixel height after open animations finish and re-pinning it when a close transition enters the 'ending' status. ResizeObserver cleanup now restores layout styles.
AccordionPanel animation lifecycle tests
packages/headless/src/primitives/accordion/accordion.test.tsx
Replaces the prior single assertion with two tests: one mocks Element.prototype.getAnimations to simulate an in-progress open animation and asserts --cl-accordion-panel-height is present; the other renders open without mocked animations and asserts the variable is absent after settling.
CollapsiblePanel measurement & transition lifecycle
packages/headless/src/primitives/collapsible/collapsible-panel.tsx
Same resetLayoutStyles + useAnimationsFinished integration as accordion, extended to both height and width dimensions. Clears pinned values after open animations settle, re-pins both axes when closing begins, and restores layout styles in ResizeObserver cleanup.
CollapsiblePanel animation lifecycle tests
packages/headless/src/primitives/collapsible/collapsible.test.tsx
Replaces prior assertions with two tests: one verifies both --collapsible-panel-height and --collapsible-panel-width are set while an open animation is mocked as pending; the other verifies both are absent from inline style once settled open.

Tabs List Ref → State Migration

Layer / File(s)Summary
TabsContextValue contract: ref → state
packages/headless/src/primitives/tabs/tabs-context.ts
TabsContextValue drops listRef: React.RefObject<HTMLElement> and adds listElement: HTMLElement | null plus setListElement: (element: HTMLElement | null) => void, with comments explaining why state (rather than a ref) is needed for measurement timing.
TabsRoot state + TabsList ref wiring
packages/headless/src/primitives/tabs/tabs-root.tsx, packages/headless/src/primitives/tabs/tabs-list.tsx
TabsRoot switches from useRef to useState for the list element, stores it in state, and provides both listElement and setListElement through the context provider with updated dependencies. TabsList passes setListElement as the Composite ref callback.
TabsIndicator synchronous measurement
packages/headless/src/primitives/tabs/tabs-indicator.tsx
Converts the indicator positioning effect from useEffect to useLayoutEffect and reads listElement directly from context instead of accessing listRef.current, eliminating the ref indirection and updating the dependency array to trigger on listElement changes.

Sequence Diagram(s)

sequenceDiagram
participant ResizeObserver
participant AccordionPanel
participant resetLayoutStyles
participant useAnimationsFinished
ResizeObserver->>AccordionPanel: observe size changes
AccordionPanel->>resetLayoutStyles: call with panel element
resetLayoutStyles->>resetLayoutStyles: capture align styles + priorities
resetLayoutStyles->>AccordionPanel: force align to initial !important
AccordionPanel->>AccordionPanel: read scrollHeight
AccordionPanel->>resetLayoutStyles: schedule restore on next frame
AccordionPanel->>AccordionPanel: set height = px value
Note over AccordionPanel,useAnimationsFinished: Open animation in progress
useAnimationsFinished->>AccordionPanel: animation finished
AccordionPanel->>AccordionPanel: clear height (undefined)
Note over AccordionPanel,useAnimationsFinished: User closes panel
AccordionPanel->>resetLayoutStyles: call with panel element (re-pin)
resetLayoutStyles->>AccordionPanel: force align to initial !important
AccordionPanel->>AccordionPanel: set height = px value
AccordionPanel->>AccordionPanel: play close animation
Loading
sequenceDiagram
participant TabsRoot
participant TabsContext
participant TabsList
participant TabsIndicator
TabsRoot->>TabsRoot: useState(listElement)
TabsRoot->>TabsContext: provide listElement + setListElement
TabsList->>TabsContext: consume setListElement
TabsList->>TabsList: pass setListElement to Composite ref
Note over TabsList,TabsIndicator: Tab list element mounted
TabsList->>TabsContext: setListElement(composite element)
TabsContext->>TabsIndicator: listElement updated
TabsIndicator->>TabsIndicator: useLayoutEffect observes listElement
TabsIndicator->>TabsIndicator: measure active tab bounding rect
TabsIndicator->>TabsIndicator: update indicator position sync
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • clerk/javascript#8475: Modifies accordion-panel.tsx and the --cl-accordion-panel-height CSS variable/testing, directly overlapping with the accordion animation lifecycle changes in this PR.
  • clerk/javascript#8474: Main PR updates to accordion-panel.tsx and collapsible-panel.tsx integrate the animation lifecycle via the useAnimationsFinished/useTransition APIs (including transitionStatus) that are foundational to this PR's implementation.

Suggested reviewers

  • wobsoriano
  • austincalvelage
  • Ephem

Poem

🐇 Hop, hop! The panel heights now know their place,
No frozen flex tricks left to leave a trace.
I measure true with resetLayoutStyles neat,
Then auto flows once open—oh so sweet!
The tabs list lives in state for sync delight,
And indicators snap in perfect sight. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 20.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the three main fixes: correcting panel measurement and improving tabs indicator timing, which are the primary changes across accordion, collapsible, and tabs components.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

@pkg-pr-new

pkg-pr-newBot commented Jun 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8897

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8897

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8897

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8897

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@8897

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8897

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8897

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8897

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8897

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8897

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8897

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8897

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8897

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8897

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8897

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8897

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8897

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8897

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8897

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8897

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8897

commit: b33c1db

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/headless/src/primitives/accordion/accordion.test.tsx (1)

194-238: ⚡ Quick win

Add a close-transition test for re-pinning height.

The updated suite validates open-path pin/clear behavior, but it still doesn’t assert the new close-path contract (re-pin measured height when closing starts). A focused test here would lock down the exact behavior this PR adds.

💡 Suggested test shape
+ it('re-pins --cl-accordion-panel-height when close transition starts', async () => {+ const user = userEvent.setup();+ renderAccordion({ defaultValue: ['item1'] });++ const original = (Element.prototype as { getAnimations?: unknown }).getAnimations;+ (Element.prototype as { getAnimations?: unknown }).getAnimations = () => [+ { finished: new Promise<void>(() => {}) },+ ];+ try {+ await user.click(screen.getByRole('button', { name: 'Section 1' }));+ const panel = document.querySelector('[data-cl-slot="accordion-panel"]') as HTMLElement;+ expect(panel.getAttribute('style') ?? '').toContain('--cl-accordion-panel-height');+ } finally {+ if (original) {+ (Element.prototype as { getAnimations?: unknown }).getAnimations = original;+ } else {+ delete (Element.prototype as { getAnimations?: unknown }).getAnimations;+ }+ }+ });

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx} should “Implement comprehensive testing including unit, integration, and E2E tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/headless/src/primitives/accordion/accordion.test.tsx` around lines
194 - 238, The animation lifecycle test suite validates the open-path behavior
for height pinning and clearing, but is missing a test case for the close-path
behavior. Add a new test within the 'animation lifecycle' describe block that
validates the height re-pinning behavior when closing starts. This test should
open an accordion panel (using defaultValue), then simulate closing it and
verify that the --cl-accordion-panel-height CSS variable is re-pinned to the
measured pixel height during the close animation phase, similar to the pattern
used in the existing 'pins --cl-accordion-panel-height while the open animation
is in progress' test case.

Source: Coding guidelines

packages/headless/src/primitives/collapsible/collapsible.test.tsx (1)

142-189: ⚡ Quick win

Add a close-transition test that asserts both dimensions are re-pinned.

This suite now covers open-path pin/clear behavior, but it doesn’t directly test the new close-path re-pin logic (--collapsible-panel-height/--collapsible-panel-width when closing starts). Adding that case will better protect this PR’s core behavior.

💡 Suggested test shape
+ it('re-pins height/width vars when close transition starts', async () => {+ const user = userEvent.setup();+ renderCollapsible({ defaultOpen: true });++ const original = (Element.prototype as { getAnimations?: unknown }).getAnimations;+ (Element.prototype as { getAnimations?: unknown }).getAnimations = () => [+ { finished: new Promise<void>(() => {}) },+ ];+ try {+ await user.click(screen.getByRole('button', { name: 'Toggle' }));+ const panel = document.querySelector('[data-cl-slot="collapsible-panel"]') as HTMLElement;+ const style = panel.getAttribute('style') ?? '';+ expect(style).toContain('--collapsible-panel-height');+ expect(style).toContain('--collapsible-panel-width');+ } finally {+ if (original) {+ (Element.prototype as { getAnimations?: unknown }).getAnimations = original;+ } else {+ delete (Element.prototype as { getAnimations?: unknown }).getAnimations;+ }+ }+ });

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx} should “Implement comprehensive testing including unit, integration, and E2E tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/headless/src/primitives/collapsible/collapsible.test.tsx` around
lines 142 - 189, Add a new test case in the 'animation lifecycle' describe block
that verifies the close-transition re-pin behavior. The test should render the
collapsible in an open state (using defaultOpen: true), then trigger the close
action, and assert that the --collapsible-panel-height and
--collapsible-panel-width CSS variables are re-pinned on the panel element (with
the data-cl-slot="collapsible-panel" selector) during the closing animation.
This mirrors the existing "pins --collapsible-panel-height/width while the open
animation is in progress" test but for the close path, ensuring that both
dimensions are preserved while the close animation is in progress before being
cleared once the animation completes.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/headless/src/utils/reset-layout-styles.ts`:
- Around line 22-40: The originalLayoutStyles object only captures the property
values but not their priority flags (the !important declaration). When restoring
in the restore() function, this causes properties that originally had !important
to be re-applied without it, changing layout behavior. Modify the code to
capture both the value and priority using element.style.getPropertyPriority(key)
for each key in originalLayoutStyles, then update the restore() function to pass
the captured priority as the third parameter to element.style.setProperty() when
re-applying the values, ensuring the !important flag is preserved if it was
originally present.
---
Nitpick comments:
In `@packages/headless/src/primitives/accordion/accordion.test.tsx`:
- Around line 194-238: The animation lifecycle test suite validates the
open-path behavior for height pinning and clearing, but is missing a test case
for the close-path behavior. Add a new test within the 'animation lifecycle'
describe block that validates the height re-pinning behavior when closing
starts. This test should open an accordion panel (using defaultValue), then
simulate closing it and verify that the --cl-accordion-panel-height CSS variable
is re-pinned to the measured pixel height during the close animation phase,
similar to the pattern used in the existing 'pins --cl-accordion-panel-height
while the open animation is in progress' test case.
In `@packages/headless/src/primitives/collapsible/collapsible.test.tsx`:
- Around line 142-189: Add a new test case in the 'animation lifecycle' describe
block that verifies the close-transition re-pin behavior. The test should render
the collapsible in an open state (using defaultOpen: true), then trigger the
close action, and assert that the --collapsible-panel-height and
--collapsible-panel-width CSS variables are re-pinned on the panel element (with
the data-cl-slot="collapsible-panel" selector) during the closing animation.
This mirrors the existing "pins --collapsible-panel-height/width while the open
animation is in progress" test but for the close path, ensuring that both
dimensions are preserved while the close animation is in progress before being
cleared once the animation completes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 3c30d6de-3161-4e3c-a8d1-3ac274415bf5

📥 Commits

Reviewing files that changed from the base of the PR and between f4ecc13 and e810695.

📒 Files selected for processing (10)
  • packages/headless/src/primitives/accordion/accordion-panel.tsx
  • packages/headless/src/primitives/accordion/accordion.test.tsx
  • packages/headless/src/primitives/collapsible/collapsible-panel.tsx
  • packages/headless/src/primitives/collapsible/collapsible.test.tsx
  • packages/headless/src/primitives/tabs/tabs-context.ts
  • packages/headless/src/primitives/tabs/tabs-indicator.tsx
  • packages/headless/src/primitives/tabs/tabs-list.tsx
  • packages/headless/src/primitives/tabs/tabs-root.tsx
  • packages/headless/src/utils/index.ts
  • packages/headless/src/utils/reset-layout-styles.ts

Comment threadpackages/headless/src/utils/reset-layout-styles.ts Outdated
@alexcarpenter
alexcarpenter merged commit fc578e3 into mainJun 18, 2026
45 checks passed
@alexcarpenter
alexcarpenter deleted the carp/headless-measurement-gaps branch June 18, 2026 15:25
Sign up for freeto 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

@alexcarpenter@kylemac
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(headless): correct panel measurement and tabs indicator timing by alexcarpenter · Pull Request #8897 · clerk/javascript · GitHub
Skip to content

fix(headless): correct panel measurement and tabs indicator timing - #8897

Merged
alexcarpenter merged 4 commits into
mainfrom
carp/headless-measurement-gaps
Jun 18, 2026
Merged

fix(headless): correct panel measurement and tabs indicator timing#8897
alexcarpenter merged 4 commits into
mainfrom
carp/headless-measurement-gaps

Conversation

@alexcarpenter

@alexcarpenteralexcarpenter commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

Three correctness fixes in @clerk/headless measurement primitives, identified by comparing against base-ui's useCollapsiblePanel.

1. Reset flex/grid alignment before measuring

Accordion.Panel and Collapsible.Panel read scrollHeight/scrollWidth directly. With display: flex/grid and non-default justify-content / align-items / align-content / justify-items, alignment can make the scroll size report smaller than the true content size. A new resetLayoutStyles util forces those properties to initial before measuring and restores them on the next frame.

2. Flow at auto when idle, re-pin on close

Once the open animation settles, the panels now drop the measured pixel size (CSS vars omitted → height: auto) so content flows naturally, and re-pin a measured value when the close transition starts so exit animations still have a concrete value to animate from.

3. Tabs.Indicator measures before paint

Switched the indicator measurement from useEffect to useLayoutEffect to remove a one-frame stale indicator position on tab change. This required tracking the tab-list element as context state (not a useRef): a child's layout effect runs before its parent's ref is attached, so the prior useEffect only worked by accident (passive effects run after all refs attach). Same pattern base-ui uses.

Testing

  • @clerk/headless unit suite passes (404 tests), typecheck clean.

Summary by CodeRabbit

Summary

  • Bug Fixes

    • Improved accordion and collapsible panel open/close animations by refining how sizes are measured and restored across transitions.
    • Updated tabs indicator to reposition synchronously during tab changes to prevent brief misalignment.
  • Tests

    • Strengthened accordion and collapsible animation lifecycle tests to verify pinned dimensions during active transitions and clearing after completion.
  • Refactor

    • Refactored tabs internals to track the tabs list element via state and update indicator measurements accordingly.

- Accordion.Panel / Collapsible.Panel: reset flex/grid alignment before
reading scrollHeight/scrollWidth so non-default alignment can't truncate
the measured content size (new resetLayoutStyles util).
- Accordion.Panel / Collapsible.Panel: drop the pinned pixel size once the
open animation settles (panel flows at auto) and re-pin a measured value
when closing starts so exit transitions still animate.
- Tabs.Indicator: measure in useLayoutEffect (before paint) to remove a
one-frame stale position on tab change. Track the list element as context
state so the indicator's layout effect can read it (a child layout effect
runs before the parent ref attaches).
@changeset-bot

changeset-botBot commented Jun 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b33c1db

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

vercelBot commented Jun 17, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
clerk-js-sandboxReadyReadyPreview, CommentJun 18, 2026 1:57pm
swingsetReadyReadyPreview, CommentJun 18, 2026 1:57pm

Request Review

@coderabbitai

coderabbitaiBot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 99c52d0b-49ba-4306-a940-de48b05be979

📥 Commits

Reviewing files that changed from the base of the PR and between 17090d3 and a989491.

📒 Files selected for processing (1)
  • packages/headless/src/utils/reset-layout-styles.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/headless/src/utils/reset-layout-styles.ts

📝 Walkthrough

Walkthrough

Adds a resetLayoutStyles utility that temporarily forces flex/grid alignment properties to initial !important before scroll-dimension measurement and restores them on the next animation frame. Wires this into AccordionPanel and CollapsiblePanel to pin pixel heights during close transitions and clear them after open animations settle. Separately migrates the Tabs list element from a useRef to useState so TabsIndicator can measure synchronously with useLayoutEffect.

Changes

Panel Height Measurement & Animation Lifecycle

Layer / File(s)Summary
resetLayoutStyles utility
packages/headless/src/utils/reset-layout-styles.ts, packages/headless/src/utils/index.ts
New resetLayoutStyles(element) helper captures original inline alignment styles (justify-content, align-items, align-content, justify-items), forces them to initial !important for accurate scrollHeight/scrollWidth reads, restores originals on the next animation frame, and returns an immediate-cleanup callback. Re-exported from the utils index.
AccordionPanel measurement & transition lifecycle
packages/headless/src/primitives/accordion/accordion-panel.tsx
Integrates resetLayoutStyles into the ResizeObserver path to normalize flex/grid alignment before each height measurement. Captures transitionStatus and runOnAnimationsFinished to orchestrate clearing the pinned pixel height after open animations finish and re-pinning it when a close transition enters the 'ending' status. ResizeObserver cleanup now restores layout styles.
AccordionPanel animation lifecycle tests
packages/headless/src/primitives/accordion/accordion.test.tsx
Replaces the prior single assertion with two tests: one mocks Element.prototype.getAnimations to simulate an in-progress open animation and asserts --cl-accordion-panel-height is present; the other renders open without mocked animations and asserts the variable is absent after settling.
CollapsiblePanel measurement & transition lifecycle
packages/headless/src/primitives/collapsible/collapsible-panel.tsx
Same resetLayoutStyles + useAnimationsFinished integration as accordion, extended to both height and width dimensions. Clears pinned values after open animations settle, re-pins both axes when closing begins, and restores layout styles in ResizeObserver cleanup.
CollapsiblePanel animation lifecycle tests
packages/headless/src/primitives/collapsible/collapsible.test.tsx
Replaces prior assertions with two tests: one verifies both --collapsible-panel-height and --collapsible-panel-width are set while an open animation is mocked as pending; the other verifies both are absent from inline style once settled open.

Tabs List Ref → State Migration

Layer / File(s)Summary
TabsContextValue contract: ref → state
packages/headless/src/primitives/tabs/tabs-context.ts
TabsContextValue drops listRef: React.RefObject<HTMLElement> and adds listElement: HTMLElement | null plus setListElement: (element: HTMLElement | null) => void, with comments explaining why state (rather than a ref) is needed for measurement timing.
TabsRoot state + TabsList ref wiring
packages/headless/src/primitives/tabs/tabs-root.tsx, packages/headless/src/primitives/tabs/tabs-list.tsx
TabsRoot switches from useRef to useState for the list element, stores it in state, and provides both listElement and setListElement through the context provider with updated dependencies. TabsList passes setListElement as the Composite ref callback.
TabsIndicator synchronous measurement
packages/headless/src/primitives/tabs/tabs-indicator.tsx
Converts the indicator positioning effect from useEffect to useLayoutEffect and reads listElement directly from context instead of accessing listRef.current, eliminating the ref indirection and updating the dependency array to trigger on listElement changes.

Sequence Diagram(s)

sequenceDiagram
participant ResizeObserver
participant AccordionPanel
participant resetLayoutStyles
participant useAnimationsFinished
ResizeObserver->>AccordionPanel: observe size changes
AccordionPanel->>resetLayoutStyles: call with panel element
resetLayoutStyles->>resetLayoutStyles: capture align styles + priorities
resetLayoutStyles->>AccordionPanel: force align to initial !important
AccordionPanel->>AccordionPanel: read scrollHeight
AccordionPanel->>resetLayoutStyles: schedule restore on next frame
AccordionPanel->>AccordionPanel: set height = px value
Note over AccordionPanel,useAnimationsFinished: Open animation in progress
useAnimationsFinished->>AccordionPanel: animation finished
AccordionPanel->>AccordionPanel: clear height (undefined)
Note over AccordionPanel,useAnimationsFinished: User closes panel
AccordionPanel->>resetLayoutStyles: call with panel element (re-pin)
resetLayoutStyles->>AccordionPanel: force align to initial !important
AccordionPanel->>AccordionPanel: set height = px value
AccordionPanel->>AccordionPanel: play close animation
Loading
sequenceDiagram
participant TabsRoot
participant TabsContext
participant TabsList
participant TabsIndicator
TabsRoot->>TabsRoot: useState(listElement)
TabsRoot->>TabsContext: provide listElement + setListElement
TabsList->>TabsContext: consume setListElement
TabsList->>TabsList: pass setListElement to Composite ref
Note over TabsList,TabsIndicator: Tab list element mounted
TabsList->>TabsContext: setListElement(composite element)
TabsContext->>TabsIndicator: listElement updated
TabsIndicator->>TabsIndicator: useLayoutEffect observes listElement
TabsIndicator->>TabsIndicator: measure active tab bounding rect
TabsIndicator->>TabsIndicator: update indicator position sync
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • clerk/javascript#8475: Modifies accordion-panel.tsx and the --cl-accordion-panel-height CSS variable/testing, directly overlapping with the accordion animation lifecycle changes in this PR.
  • clerk/javascript#8474: Main PR updates to accordion-panel.tsx and collapsible-panel.tsx integrate the animation lifecycle via the useAnimationsFinished/useTransition APIs (including transitionStatus) that are foundational to this PR's implementation.

Suggested reviewers

  • wobsoriano
  • austincalvelage
  • Ephem

Poem

🐇 Hop, hop! The panel heights now know their place,
No frozen flex tricks left to leave a trace.
I measure true with resetLayoutStyles neat,
Then auto flows once open—oh so sweet!
The tabs list lives in state for sync delight,
And indicators snap in perfect sight. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 20.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the three main fixes: correcting panel measurement and improving tabs indicator timing, which are the primary changes across accordion, collapsible, and tabs components.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

@pkg-pr-new

pkg-pr-newBot commented Jun 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8897

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8897

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8897

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8897

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@8897

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8897

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8897

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8897

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8897

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8897

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8897

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8897

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8897

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8897

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8897

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8897

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8897

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8897

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8897

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8897

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8897

commit: b33c1db

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/headless/src/primitives/accordion/accordion.test.tsx (1)

194-238: ⚡ Quick win

Add a close-transition test for re-pinning height.

The updated suite validates open-path pin/clear behavior, but it still doesn’t assert the new close-path contract (re-pin measured height when closing starts). A focused test here would lock down the exact behavior this PR adds.

💡 Suggested test shape
+ it('re-pins --cl-accordion-panel-height when close transition starts', async () => {+ const user = userEvent.setup();+ renderAccordion({ defaultValue: ['item1'] });++ const original = (Element.prototype as { getAnimations?: unknown }).getAnimations;+ (Element.prototype as { getAnimations?: unknown }).getAnimations = () => [+ { finished: new Promise<void>(() => {}) },+ ];+ try {+ await user.click(screen.getByRole('button', { name: 'Section 1' }));+ const panel = document.querySelector('[data-cl-slot="accordion-panel"]') as HTMLElement;+ expect(panel.getAttribute('style') ?? '').toContain('--cl-accordion-panel-height');+ } finally {+ if (original) {+ (Element.prototype as { getAnimations?: unknown }).getAnimations = original;+ } else {+ delete (Element.prototype as { getAnimations?: unknown }).getAnimations;+ }+ }+ });

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx} should “Implement comprehensive testing including unit, integration, and E2E tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/headless/src/primitives/accordion/accordion.test.tsx` around lines
194 - 238, The animation lifecycle test suite validates the open-path behavior
for height pinning and clearing, but is missing a test case for the close-path
behavior. Add a new test within the 'animation lifecycle' describe block that
validates the height re-pinning behavior when closing starts. This test should
open an accordion panel (using defaultValue), then simulate closing it and
verify that the --cl-accordion-panel-height CSS variable is re-pinned to the
measured pixel height during the close animation phase, similar to the pattern
used in the existing 'pins --cl-accordion-panel-height while the open animation
is in progress' test case.

Source: Coding guidelines

packages/headless/src/primitives/collapsible/collapsible.test.tsx (1)

142-189: ⚡ Quick win

Add a close-transition test that asserts both dimensions are re-pinned.

This suite now covers open-path pin/clear behavior, but it doesn’t directly test the new close-path re-pin logic (--collapsible-panel-height/--collapsible-panel-width when closing starts). Adding that case will better protect this PR’s core behavior.

💡 Suggested test shape
+ it('re-pins height/width vars when close transition starts', async () => {+ const user = userEvent.setup();+ renderCollapsible({ defaultOpen: true });++ const original = (Element.prototype as { getAnimations?: unknown }).getAnimations;+ (Element.prototype as { getAnimations?: unknown }).getAnimations = () => [+ { finished: new Promise<void>(() => {}) },+ ];+ try {+ await user.click(screen.getByRole('button', { name: 'Toggle' }));+ const panel = document.querySelector('[data-cl-slot="collapsible-panel"]') as HTMLElement;+ const style = panel.getAttribute('style') ?? '';+ expect(style).toContain('--collapsible-panel-height');+ expect(style).toContain('--collapsible-panel-width');+ } finally {+ if (original) {+ (Element.prototype as { getAnimations?: unknown }).getAnimations = original;+ } else {+ delete (Element.prototype as { getAnimations?: unknown }).getAnimations;+ }+ }+ });

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx} should “Implement comprehensive testing including unit, integration, and E2E tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/headless/src/primitives/collapsible/collapsible.test.tsx` around
lines 142 - 189, Add a new test case in the 'animation lifecycle' describe block
that verifies the close-transition re-pin behavior. The test should render the
collapsible in an open state (using defaultOpen: true), then trigger the close
action, and assert that the --collapsible-panel-height and
--collapsible-panel-width CSS variables are re-pinned on the panel element (with
the data-cl-slot="collapsible-panel" selector) during the closing animation.
This mirrors the existing "pins --collapsible-panel-height/width while the open
animation is in progress" test but for the close path, ensuring that both
dimensions are preserved while the close animation is in progress before being
cleared once the animation completes.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/headless/src/utils/reset-layout-styles.ts`:
- Around line 22-40: The originalLayoutStyles object only captures the property
values but not their priority flags (the !important declaration). When restoring
in the restore() function, this causes properties that originally had !important
to be re-applied without it, changing layout behavior. Modify the code to
capture both the value and priority using element.style.getPropertyPriority(key)
for each key in originalLayoutStyles, then update the restore() function to pass
the captured priority as the third parameter to element.style.setProperty() when
re-applying the values, ensuring the !important flag is preserved if it was
originally present.
---
Nitpick comments:
In `@packages/headless/src/primitives/accordion/accordion.test.tsx`:
- Around line 194-238: The animation lifecycle test suite validates the
open-path behavior for height pinning and clearing, but is missing a test case
for the close-path behavior. Add a new test within the 'animation lifecycle'
describe block that validates the height re-pinning behavior when closing
starts. This test should open an accordion panel (using defaultValue), then
simulate closing it and verify that the --cl-accordion-panel-height CSS variable
is re-pinned to the measured pixel height during the close animation phase,
similar to the pattern used in the existing 'pins --cl-accordion-panel-height
while the open animation is in progress' test case.
In `@packages/headless/src/primitives/collapsible/collapsible.test.tsx`:
- Around line 142-189: Add a new test case in the 'animation lifecycle' describe
block that verifies the close-transition re-pin behavior. The test should render
the collapsible in an open state (using defaultOpen: true), then trigger the
close action, and assert that the --collapsible-panel-height and
--collapsible-panel-width CSS variables are re-pinned on the panel element (with
the data-cl-slot="collapsible-panel" selector) during the closing animation.
This mirrors the existing "pins --collapsible-panel-height/width while the open
animation is in progress" test but for the close path, ensuring that both
dimensions are preserved while the close animation is in progress before being
cleared once the animation completes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 3c30d6de-3161-4e3c-a8d1-3ac274415bf5

📥 Commits

Reviewing files that changed from the base of the PR and between f4ecc13 and e810695.

📒 Files selected for processing (10)
  • packages/headless/src/primitives/accordion/accordion-panel.tsx
  • packages/headless/src/primitives/accordion/accordion.test.tsx
  • packages/headless/src/primitives/collapsible/collapsible-panel.tsx
  • packages/headless/src/primitives/collapsible/collapsible.test.tsx
  • packages/headless/src/primitives/tabs/tabs-context.ts
  • packages/headless/src/primitives/tabs/tabs-indicator.tsx
  • packages/headless/src/primitives/tabs/tabs-list.tsx
  • packages/headless/src/primitives/tabs/tabs-root.tsx
  • packages/headless/src/utils/index.ts
  • packages/headless/src/utils/reset-layout-styles.ts

Comment threadpackages/headless/src/utils/reset-layout-styles.ts Outdated
@alexcarpenter
alexcarpenter merged commit fc578e3 into mainJun 18, 2026
45 checks passed
@alexcarpenter
alexcarpenter deleted the carp/headless-measurement-gaps branch June 18, 2026 15:25
Sign up for freeto 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

@alexcarpenter@kylemac