Skip to content

Show the heading chat button when the pointer is over the section #372

Description

@HMarzban

Related

#367 and #369 change how tall a heading is drawn. A shorter heading has a shorter hover band. The two issues meet this one on h3 to h6.

A search of issues and discussions found no open request for this hover.

What happened

This work is planned for the next production release of the webapp.

Edward Saperia wrote on 2026-09-26: "It should show whenever I'm over the section, not just the heading".

He also wrote: "for small headings (h3 etc) the vertical height where it shows is really small".

The button is a blue speech-bubble icon in a white circle. It sits on the border between the white document sheet and the grey-blue gutter.

The screenshot is not attached. The maintainer will drag it into this issue.

What we expect

On a desktop, the button shows when the pointer is anywhere over that heading's section.

The section is the heading plus the top-level blocks that follow it, up to the next heading of the same or higher level.

The button stays on the sheet border. The pointer can move from the words to the button, and the button stays shown.

For an h3, h4, h5, or h6, the band that reveals the button is at least 24 CSS pixels tall.

Steps to reproduce

  1. Open an editable pad on a desktop.
  2. Add an h3, then two paragraphs under it.
  3. Rest the pointer on the heading line. The chat button shows.
  4. Move the pointer onto a paragraph under that heading. The button hides.
  5. Repeat with an h6. The heading line is about 18px tall, so the band is easy to miss.

Findings

Measured in a browser on 2026-09-29. The theme was light (docsplus) only. Dark theme was not measured.

The route was /editor. That shell carries history_editor, which hides .ha-wrap. The class was removed in the browser tab only, so the hover rules could run. No file in the repo was changed.

The viewport was 882px wide. The root font size was 16px. The sheet end pad was 2rem. (hover: hover) and (pointer: fine) matched.

The reveal rule lives in apps/webapp/src/styles/components/_heading-actions.scss.

On a fine pointer, the button shows in three cases. The heading matches :hover. .ha-wrap matches :hover. The heading matches :focus-within.

The selector is:

:is(h1, h2, h3, h4, h5, h6)[data-toc-id]:hover .ha-wrap:not(.has-selection) .ha-single

The same block also has .ha-wrap:not(.has-selection):hover .ha-single.

Until one of those matches, .ha-single has opacity 0, visibility hidden, and pointer-events none.

The hover box over the words is the heading border box. Vertical padding was 0. The margin above the heading is outside that box.

A forced :hover on an h6 set the button to opacity 1 and visibility visible.

A forced :hover on the paragraph under an h3 left that button at opacity 0 and visibility hidden.

Eight pixels above the h6 words, the hit target was the editor.

In the gutter, the same offset hit .ha-wrap. That hit is the ::before bridge on .ha-wrap. The bridge was 44px tall and 110px wide.

The bridge covers the path from the heading to the button. It does not cover the blocks under the heading.

The button was 44 by 44 CSS pixels. That is size-11 and $ha-hit-size (2.75rem).

.ha-wrap comes from createHoverChatPlugin in hoverChatPlugin.ts. TipTap.tsx sets hoverChat from editable.

The table below is one section that held h1 through h6, so HeadingScale set the sizes. Line height is 1.15. Hover height is offsetHeight.

Heading Size Font size Line box Hover height Margin above
Title 28pt painted 37.33px 42.93px 43px 49.65px
h1 20pt 26.67px 30.66px 31px 35.47px
h2 18.4pt 24.53px 28.21px 28px 28.21px
h3 16.8pt 22.4px 25.76px 26px 22.4px
h4 15.2pt 20.27px 23.3px 23px 20.27px
h5 13.6pt 18.13px 20.85px 21px 18.13px
h6 12pt 16px 18.4px 18px 16px

Title is the first h1. CSS paints it at 28pt. The plugin still writes --hd-size: 20pt on it.

The h1 row is the first heading of a section that also holds h2 through h6. Its rank size is 20pt.

h4, h5, and h6 hover boxes are under 24 CSS pixels. The h3 box is 26px. The button is 44px, and it stays hidden until the pointer hits the line or the gutter bridge.

computeSection in compute-section.ts returns the heading and the top-level siblings that follow it. It stops at the next heading of the same or higher level, or at the end of the document.

Those blocks are siblings. A :hover rule on the heading does not include them.

Read from code, not measured. A folded block has the class heading-fold-hidden and display: none. A hidden block is not a hover target.

Unread was measured. A button with data-unread-count set to 3 reached opacity 1 and visibility visible after the 0.15s fade.

Chat open is read from code, not measured. No rule ties chatRoom.headingId to .ha-wrap. The editor button does not stay shown because chat is open. The TOC row uses menu-active for that state. That row is a different control.

Focus was measured. The caret sat in an h6, and .ProseMirror had focus. The heading did not match :focus-within. The button stayed hidden. A focus() call on the hidden button did not move focus.

The button is a <button type="button"> with tab index 0. While visibility is hidden, the browser does not focus it.

Coarse pointers are read from code, not measured. Inside @media (hover: none), (pointer: coarse), .ha-single stays visible.

Mobile is read from code, not measured. _mobile.scss paints a 32 by 32 tab and forces it visible. selectionChat is off when isMobile is true.

History is read from code, not measured on a history route. hoverChat is false when the editor is not editable. .history_editor also sets .ha-wrap and .ha-selection-comment-dock to display: none.

The selection comment chip is selectionChatPlugin. It shows for a non-heading text selection on a desktop. Its class is ha-selection-comment-dock.

The chip center sits on the sheet border. The shift is translateX(calc(var(--tiptap-inline-pad-end) + 0.5px + 50%)). The docked chat panel and the TOC rail paint above the sheet. That overlap was read from code, not measured.

Reference points

WCAG 2.2 Success Criterion 2.5.8 Target Size (Minimum) asks for a pointer target of at least 24 by 24 CSS pixels. The understanding page is Target Size (Minimum).

The chat button is 44px, so the control meets that size. The hover band over an h6 is 18px. That band is the heading line. The control is the button.

WCAG 2.2 Success Criterion 1.4.13 Content on Hover or Focus says hover content must stay shown while the pointer moves onto it. The understanding page is Content on Hover or Focus.

Notion shows a ⋮⋮ handle in the left margin when the pointer is over a block. That handle belongs to one block. The page does not describe a handle for a heading plus the blocks under it.

Google Docs help says to highlight the text, then use Add comment in the toolbar. A second page says a button appears in the right margin after you select text.

GitHub shows a comment icon when the pointer rests on one line of a pull request diff.

Confluence Cloud says an inline comment can start from highlighted text or from a hover over a section. The steps on that page cover highlighted text, and a hover on an image or a video.

Acceptance criteria

  • On a desktop, the button shows when the pointer is anywhere over a heading's section.
  • For an h3, h4, h5, or h6, the reveal band is at least 24 CSS pixels tall. The 24px floor comes from WCAG 2.2 Success Criterion 2.5.8. When blocks follow the heading, the band is the whole section.
  • The button stays shown while the pointer moves from the heading text to the button.
  • The button center stays on the sheet border.
  • Folded sections, the unread state, an open chat, history read-only mode, and the selection comment chip keep their current behavior.
  • A pointer move does not re-render React, write the store, or rebuild decorations.
  • Any new listener is removed when the editor is destroyed.
  • A Cypress spec in apps/webapp/cypress/e2e/ fails before the fix. The spec hovers a paragraph under an h3 and expects the button to show.
  • The check covers the light theme and the dark theme.

Blocked by

None.

Agent brief

Type: HITL for the method. AFK after the maintainer picks one.

Category: enhancement

Current behavior: The button shows on heading hover, on the gutter bridge, on unread, and on a coarse pointer. It stays hidden when the pointer is on a block under the heading.

Desired behavior: On a desktop, the pointer anywhere over the section shows the button. The button stays on the sheet border.

Where to start: apps/webapp/src/styles/components/_heading-actions.scss (the :hover block, $ha-hit-size, $ha-sheet-border-straddle-x). apps/webapp/src/components/TipTap/extensions/HeadingActions/plugins/hoverChatPlugin.ts (createHoverChatPlugin). apps/webapp/src/components/TipTap/extensions/shared/compute-section.ts (computeSection). Search by symbol. File names are hints as of 2026-09-29.

Rules that apply: Do not rebuild decorations on each pointer move. Do not write the store on each move. Remove any listener when the editor is destroyed. Keep the sheet-border straddle. Keep the chip under the docked chat panel and the TOC rail. Do not change settled TOC behavior. History read-only keeps the widgets hidden. Folded blocks stay hidden. See apps/webapp/CLAUDE.md §TOC And Heading Actions and §Pad Workspace Surfaces. See apps/webapp/src/components/TipTap/CLAUDE.md §Editor Performance and §Heading Schema.

Open choice: The maintainer has not picked a method.

  1. One pointer listener on the editor maps the pointer to a section with computeSection, and sets one class on that heading's .ha-wrap. This path does not rebuild decorations. posAtCoords on every move can force layout. The button stays on the heading, so a long section can show it off screen.
  2. Taller heading padding, or a pseudo-element, is CSS only. It cannot cover following siblings. A tall box over the text steals clicks.
  3. A decoration on every block in the section matches the section model. CSS cannot select the previous heading from a hovered sibling. A wrapper or a listener is still required. A rebuild on each move breaks the performance rule.

Verify: On a desktop, hover an h3 line, then a paragraph under it, then the gutter path to the button. Check an h6. Check unread, an open chat, a folded section, history read-only, and a body text selection. Check the light theme and the dark theme. Run the new Cypress spec. Watch it fail before the fix and pass after it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions