[collapsible][accordion] Find-in-page match is not highlighted on first reveal when a hiddenUntilFound panel uses CSS transitions
- Dominant language
- TypeScript
- Stars
- 10.9k
- Forks
- 543
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 101
Description
# Bug report
## Current behavior
When `hiddenUntilFound` is combined with the transition-based panel CSS the docs recommend
(`height: var(--accordion-panel-height)` + `[data-starting-style] { height: 0 }`), find-in-page
reveals the panel but the matched text is **not highlighted**. The panel itself opens correctly —
only the highlight is lost. Repeating the search highlights it.
Searching for text that exists solely inside a closed `hiddenUntilFound` panel on the two experiment
pages, by pasting the term into the find bar:
| Panel CSS | Chrome 151 | Safari 26.5.2 | Firefox 153 |
| -------------------------------------------------------------------- | ------------------- | ------------------- | ----------- |
| [transitions](https://base-ui.com/experiments/accordion/transitions) | **not highlighted** | **not highlighted** | highlighted |
| [keyframes](https://base-ui.com/experiments/accordion/animations) | highlighted | highlighted | highlighted |
Firefox highlights the match correctly in both cases, so the collapsed panel is recoverable — that
engine evidently recomputes the highlight after the panel expands.
**Paste the term — do not type it.** Typing runs a fresh find on every keystroke, so an earlier
prefix reveals the panel and the final keystroke re-highlights it on an already-open panel. Pasting
is a single find operation, so nothing recomputes the highlight afterwards. Reaching the match by
pressing Enter to cycle behaves like pasting.
## Expected behavior
The matched text should be highlighted the first time find-in-page reveals a `hiddenUntilFound`
panel, regardless of whether the panel animates with CSS transitions or CSS keyframes — as it
already does with keyframes everywhere, and with both variants in Firefox.
## Reproducible example
In Chrome or Safari:
1. Open and scroll to the `hiddenUntilFound`
section.
2. Open find-in-page (Cmd/Ctrl+F) and **paste** `amber`. That text
exists only inside the closed panels, so one find operation both reveals a panel and computes the
highlight.
3. The panel opens, but the revealed `amber` is not highlighted. Pressing Enter or
re-running the search highlights it.
As a control, the same steps on the keyframe-based page — pasting `silver` — highlight correctly in
every browser tested:
In **Chrome only**, a scroll-to-text fragment reproduces it without any manual input:
(compare
, which highlights). Use the
find bar in other browsers: Safari highlights scroll-to-text fragments correctly even on the
transitions page, and Firefox does not reveal `hidden="until-found"` content from a text fragment at
all (its fragments work, but only against already-visible text).
## Base UI version
v1.7.0 (also reproduces on current `master` — nothing in `packages/react/src/collapsible` or
`packages/react/src/accordion` has changed since the v1.7.0 tag).
## Which browser are you using?
Chrome 151.0.7922.138 and Safari 26.5.2 — both affected. Firefox 153.0.4 is not affected.
## Which OS are you using?
macOS
## Additional context
### What differs between the two experiments
The panel's animation type decides whether `data-starting-style` is persisted while the panel is
closed. In `useCollapsiblePanel`:
```ts
const shouldPersistHiddenTransitionStyles =
hiddenUntilFound && hidden && animationTypeRef.current !== 'css-animation';
```
…which is then spread into the panel props as `[CollapsiblePanelDataAttributes.startingStyle]: ''`.
So:
- **CSS transitions** — a closed `hiddenUntilFound` panel keeps `data-starting-style`, and therefore
author CSS keeps it at `height: 0`. Measured on the closed panel:
`height: 0px`, `data-starting-style` present.
- **CSS keyframes** — the attribute is not persisted, so the panel's computed height is its full
size as soon as `hidden` is removed. Measured: `height: 420px` immediately.
That persistence is deliberate (it lets a later trigger-driven open transition from `0`), but it
means that at the moment the browser removes `hidden="until-found"` the transition-based panel is
still fully collapsed.
### Timing
`beforematch` only schedules React state (`setOpen(true)`), so the expansion lands in a later task.
Measured with `MessageChannel` turns after dispatching `beforematch` + removing `hidden`, the panel
goes `0px → full height` on turn 2 — identical in a dev build and on the deployed site, so this is
not a dev-mode/StrictMode artifact.
Chrome and Safari compute the highlight for the revealing find operation while the panel is still
collapsed, and the highlight is lost; only a later find operation recomputes it against the expanded
panel. Keyframe panels are already at full height when `hidden` is removed, so their very first
operation highlights correctly in every browser. Firefox highlights both variants correctly, so
recomputing after the expansion is clearly possible — Chrome and Safari just don't.
### Why this matters beyond the experiments page
The transition pattern is the one the docs teach (accordion and collapsible hero demos, and every
other accordion demo), so any consumer combining the documented CSS with `hiddenUntilFound` inherits
this behavior.
The repro above uses Accordion, but `Accordion.Panel` and `Collapsible.Panel` both run through
`useCollapsiblePanel`, so `Collapsible.Panel hiddenUntilFound` behaves the same way.
### Suggested fix
`beforematch` fires *before* the browser removes the `hidden` attribute, so its handler is the one
synchronous hook point available. Dropping the persisted `data-starting-style` there — instead of
waiting for the layout effect that runs at `transitionStatus === 'starting'` — makes the panel's
geometry correct before the browser measures the match.
I tried this locally and it fixes the repro: a one-line addition to the `beforematch` handler in
`useCollapsiblePanel`, with the open/close lifecycle otherwise untouched. The `Collapsible` and
`Accordion` suites still pass in both jsdom and chromium. Happy to open a PR, with a regression test
that observes the panel from a `beforematch` listener registered after the component's own (so it
sees what the browser sees) — it fails without the change and passes with it.
Contributor guide
Assessment
This issue has not been assessed yet.