Skip to content

feat(tree, list, chat, expansion-panel, tabs): skip off-screen items and make collapsed content findable - #2594

Merged
rkaraivanov merged 11 commits into
masterfrom
rkaraivanov/content-visibility-find-in-page
Nov 12, 2026
Merged

rkaraivanov merged 11 commits into
masterfrom
rkaraivanov/content-visibility-find-in-page

Conversation

@rkaraivanov

Copy link
Copy Markdown
Member

Description

Two web platform features applied to the components that hold long or collapsed content:

  • content-visibility: auto on tree items, list items and chat messages. Items far from the view skip style, layout and paint until they come near it.
  • hidden="until-found" on the collapsed content of the expansion panel (also in the accordion), the tree item and the tab panel. Find-in-page (Ctrl+F), text fragments and links to an element inside now open the container, with the same events as a user toggle.

Performance (Chromium, median of 5 runs)

Case First style + layout First render
Tree, 2,000 items 113.5 → 18.3 ms −41%
Tree, 5,000 items 264.3 → 52.0 ms −39%
List, 1,000 items 46.5 → 10.3 ms −33%
Chat, 300 messages 39.6 → 9.0 ms −7% (mostly script)

A resize or a change of --ig-size costs 80–90% less. Small trees, lists and chats do not change.

Find-in-page

  • A reveal goes through the user path: igcOpening/igcOpened, igcItemExpanding/igcItemExpanded, or the igcChange of the tabs. If a handler cancels the event, the container stays closed.
  • A reveal has no animation, so the scroll of the browser lands on the match. With accordion single-expand or tree singleBranchExpand, the other panels or branches also close without animation.
  • A disabled container uses plain hidden, so the browser does not search it.
  • Browsers without until-found hide the content as before.
  • New story: Expansion panel › Help Center.

Behavior changes and known limits

  • Side effects of content-visibility:
    • In a container that takes its width from its content, a long list or tree takes the width of the items near the view.
    • innerText of a skipped item is an empty string; textContent works.
    • A list item or a chat message is now the containing block of its position: fixed children.
    • To turn the feature off, set content-visibility: visible on igc-tree-item, igc-list-item or igc-chat::part(message-item).
  • Browser support: until-found needs Chrome 102, Firefox 139 or Safari 26.2. Firefox before 148 and Safari 26.2 open the container but do not scroll to the match. Safari before 26 does not find text in skipped items.
  • WebKit: the browser does not scroll to a match in a collapsed tree item that skips its rendering.
  • Firefox: the reveal goes on after a canceled inner open, so the outer container still opens.
  • singleBranchExpand: a reveal keeps the expanded items inside the item that it expands. The browser does not tell where the match is, so a collapse could hide it.

Type of Change

  • New feature (non-breaking change that adds functionality)

Checklist

  • My code follows the project's coding standards
  • I have tested my changes locally
  • I have updated documentation if needed
  • If I changed the public API on purpose, or its JSDoc, I have run npm run public-api:update and committed public-api.json and api-reference/
  • I have added a CHANGELOG.md entry under [Unreleased]

Tree items, list items and chat messages far from the view skip style,
layout and paint (content-visibility: auto with contain-intrinsic-size).
Style and layout take 70-90% less time, and the first render of a tree
with 2,000 items takes 41% less.

- Tree: only collapsed items skip, with the row height as their size.
  The active and the focused item always render, because Firefox and
  WebKit do not scroll to an element in skipped content.
- List: an estimated size per --ig-size until the first render.
- Chat: a resize observer on the message list keeps the list at the
  bottom while messages render late. Each scroll of the chat records
  its position, so a scroll event that merges it with a scroll of the
  user still stops the follow.
- Specs: 8 new. 5 chat template specs read textContent, because
  innerText of skipped content is empty.
Groundwork for find-in-page in collapsed content. The controller binds
hidden="until-found" on the content of a collapsible host and opens the
host on beforematch, through its user path with events. The attribute
stays the browser's until the next task, because a removal in the
microtasks after the event stops the reveal of the outer ancestors in
Chromium and WebKit. A refused open sets plain hidden, and live()
restores until-found after the reveal.

- The component base sheet no longer forces display: none on
  hidden="until-found".
- lit-analyzer treats hidden as a global attribute, because its data
  still calls hidden boolean.
…page

Find-in-page, a text fragment or a link to an element in the content now
opens a collapsed expansion panel. The content uses hidden="until-found"
through the hidden-until-found controller, and the panel opens through
the user path: igcOpening (cancelable) and igcOpened. A disabled panel
stays closed, with plain hidden.

- The content shows, and is inert, only during the close animation. The
  open animation removes hidden first, so it measures the content.
- While a reveal runs in any host (isRevealing()), panels open and close
  without their animation. Before, a single-expand accordion that
  animated a panel closed above the match left the match out of view.
- Specs reveal with a real click on a link (clickWithMouse). The 6 lines
  that asserted inert now assert hidden.
Find-in-page, a text fragment or a link to an element in the children of
a collapsed tree item now expands the collapsed items above it. The
children container uses hidden="until-found" through the
hidden-until-found controller, and each item expands through the user
path, from the inside out: igcItemExpanding (cancelable) and
igcItemExpanded. A disabled item stays collapsed, with plain hidden.

- The children show, and are inert, only during the collapse animation.
  The expand animation removes hidden first, so it measures them.
- Items expand and collapse without their animation while a reveal runs.
- singleBranchExpand keeps the items below the revealed item expanded
  during a reveal. Before, the outer item of the path collapsed the
  inner one that the browser had just revealed.
- WebKit expands the path but does not scroll to a match in a collapsed
  leaf item that skips its rendering. Documented in the spec.
- Specs reveal with a real click on a link. The assertion of inert
  children now asserts hidden.
Find-in-page, a text fragment or a link to an element in a panel that
is not selected now selects its tab. The panels use hidden="until-found"
through the hidden-until-found controller, and the tab is selected as a
click does, with igcChange, but without moving the focus or following
the link of a link tab. The panel of a disabled tab has plain hidden.

- A hidden panel takes no height and does not scroll, so it does not
  cover the selected panel in their shared area.
- A hidden tab panel and the children container of a collapsed tree
  item get aria-hidden. The browser keeps their box for find-in-page,
  and Chromium exposed them as empty containers.
- Specs reveal with a real click on a link. The checks for inert and
  for display: none on unselected panels now check hidden.
In Safari before 26, which includes iOS 18, find-in-page does not find
the text of an item that skips its rendering (content-visibility: auto).
The spec files of the tree, the list and the chat, and the CHANGELOG
entry of the change, now say so.
The HelpCenter story of the expansion panel keeps the answers of a help
page in closed panels. Find-in-page, the links under "Popular answers"
and a text fragment open the panel with the match, and a status line
shows the igcOpened event. The AdvancedSettings description now says
that the content of a closed panel is hidden, not inert.
…oller

The hidden-until-found controller now plays the open and close animation
of its host. It skips the animation during a reveal, shows the content
before an open measures it, and keeps the content visible and inert
while a close runs. The expansion panel and the tree item drop their own
copies of these steps. The controller uses createTimer and a set of the
revealing controllers, the spec helpers reuse getCenterPoint, and long
comments are shorter.
A tab panel that is not selected keeps its box for find-in-page. Its
zero height, padding and border are now important, so styles of the
tab-body part cannot give it a box that covers the selected panel and
takes its clicks.

With singleBranchExpand, a reveal keeps the expanded items in the item
that it expands: the browser does not tell where the match is, and a
collapse could hide it. The tree spec documents this and a test keeps
it. A tree spec also checks the animations in the item shadow roots.
@rkaraivanov
rkaraivanov merged commit 4d72780 into master Nov 12, 2026
10 checks passed
@rkaraivanov
rkaraivanov deleted the rkaraivanov/content-visibility-find-in-page branch November 12, 2026 14:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant