Skip to content

fix: polish chat thread layout, scroll architecture, and accessibility#42

Merged
batmn-dev merged 15 commits intomainfrom
fixing-things-are-hard
Feb 22, 2026
Merged

fix: polish chat thread layout, scroll architecture, and accessibility#42
batmn-dev merged 15 commits intomainfrom
fixing-things-are-hard

Conversation

@batmn-dev
Copy link
Owner

@batmn-dev batmn-dev commented Feb 22, 2026

Summary

Comprehensive UI/UX overhaul of the chat thread layout, scroll behavior, and accessibility across single-chat and multi-model views. This branch aligns our layout system with ChatGPT-style patterns using container queries, CSS custom properties, and sticky positioning.

Scroll Architecture

  • Replace ChatContainer with ScrollRoot/ScrollRootContent primitives wrapping use-stick-to-bottom via React context
  • Remove scroll anchor mechanism (hasScrollAnchor, min-h-scroll-anchor) that was causing layout instability
  • Simplify scroll button with a dual-context hook for backward compatibility

Thread Layout System

  • Introduce CSS custom properties (--thread-content-max-width, --thread-content-margin) for consistent thread width
  • Add container query breakpoints matching ChatGPT's responsive system (base → 640px → 1024px)
  • Align composer width with messages using shared margin tokens

Chat Composer

  • Sticky composer with gradient fade overlay (32px) for content dissolve effect
  • Polish bottom layout, masking, and shadow rendering (including dark mode)
  • Conditionally hide composer chrome during onboarding for a cleaner first-run experience
  • Increase message area bottom padding (32px → 96px) for breathing room

Header

  • Transparent header background with 1px bottom separator (disappears at 2xl)
  • Disable hoverable tooltips across sidebar, header, user menu, model selector, and message actions

Accessibility

  • Add skip-to-content link and ARIA live regions
  • Switch message containers to <article> elements with data-turn/data-message-id attributes
  • Add screen-reader-only message labels and a11y/announce utility

Touch & Responsive

  • Touch-aware target sizing for coarse pointer devices (32/36px → 40px tap targets)
  • Mask-based reveal animation for message action buttons

Other

  • Add thinking states test page (/test/thinking-states)
  • Update design reference docs and add new HTML structure references
  • Add ui-component-match skill for design reference workflows

Test plan

  • Verify single-chat thread scrolls correctly and sticks to bottom on new messages
  • Verify multi-model chat layout matches single-chat spacing and scroll behavior
  • Test composer gradient fade appears above sticky input in both light and dark mode
  • Test responsive breakpoints: narrow (<640px), medium (640-1024px), wide (>1024px)
  • Verify header is transparent with subtle separator, separator hidden at 2xl
  • Test on touch device (or emulate coarse pointer) — tap targets should be 40px
  • Check accessibility: skip-to-content link, screen reader labels on messages
  • Verify onboarding view hides composer chrome correctly
  • Check /test/thinking-states page renders all thinking state variants

Made with Cursor


Summary by cubic

Polished the chat thread layout and scroll behavior with a new ScrollRoot, aligned thread/composer widths, and improved accessibility and header visuals across single and multi-model chats. Also cleaned up container queries, the skip link, and scroll button hooks.

  • New Features

    • ScrollRoot/ScrollRootContent for stick-to-bottom scrolling with a simplified ScrollButton; unified thread/composer widths via container queries and CSS vars.
    • Sticky composer with a 32px gradient fade and increased bottom padding; cleaner onboarding by hiding composer chrome.
    • Transparent header with a subtle separator that disappears at 2xl; hoverable tooltips disabled app-wide; larger 40px tap targets on touch.
    • Accessibility upgrades: skip-to-content link, messages as article elements with data attributes and screen reader labels; added /test/thinking-states.
  • Refactors

    • Replaced ChatContainer and removed the scroll-anchor mechanism and related props; split ScrollButton into an inner component plus legacy wrapper to avoid conditional hooks.
    • Simplified single and multi-model layouts with sticky positioning and shared margin tokens; adjusted model selector and loader/reasoning text sizes.
    • Container queries use explicit @[64rem]/main; fixed skip link href to #main; removed ARIA live regions and a11y announce utility; deleted old reasoning component; updated ChatGPT reference test IDs and docs.

Written for commit 82e5a28. Summary will update on new commits.

batmn-dev and others added 14 commits February 21, 2026 00:58
… selector styles

- Add disableHoverablePopup to Tooltip components across sidebar, header,
  user menu, model selector, chat input, and message actions to prevent
  unwanted hover-to-popup behavior
- Make header background transparent at 2xl breakpoint for cleaner wide layouts
- Remove redundant bg-background from layout wrapper (header handles it)
- Style model selector triggers with text-lg and font-normal
- Support render prop on Button with automatic nativeButton: false
- Add ChatGPT header analysis research docs

Co-authored-by: Cursor <cursoragent@cursor.com>
…ip position

Remove hasScrollAnchor prop threading through message components
(conversation, message, message-user, message-assistant, multi-conversation)
and the min-h-scroll-anchor class. Change model selector tooltip side
from bottom to right.

Co-authored-by: Cursor <cursoragent@cursor.com>
…mantics

Add skip-to-content link, ARIA live regions, and sr-only message labels
for screen readers. Switch message containers to article elements with
data-turn/data-message-id attributes. Introduce CSS custom properties
(--thread-content-max-width, --thread-content-margin) for consistent
thread width. Add mask-based reveal animation for message actions.
Bump reasoning and loader text sizes. Add thinking states test page
and a11y announce utility. Update design reference docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
…rchitecture

Introduce ScrollRoot/ScrollRootContent primitives that wrap use-stick-to-bottom
with a React context, replacing the ChatContainer abstraction. Simplify chat and
multi-chat layouts with sticky positioning instead of motion layout animations.
Update ScrollButton with a dual-context hook for backward compatibility.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace static --thread-content-max-width (48rem) and --thread-content-margin
(1.5rem) with responsive container-query values matching ChatGPT's system:
- Base (narrow): 40rem max-width, 1rem margin
- @container main ≥640px: 1.5rem margin
- @container main ≥1024px: 48rem max-width, 4rem margin

Update fallback values in all consuming components to match the new base.

Co-authored-by: Cursor <cursoragent@cursor.com>
Remove bg-background from header, making it fully transparent so content
scrolls behind it. Add a subtle 1px bottom shadow using the --border token
for visual separation. At 2xl viewport (≥1536px), the separator also
disappears for a fully immersive feel. Remove backdrop-blur-sm as ChatGPT
doesn't use blur on the header.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add more breathing room below the last message before the composer
(32px → 96px), matching ChatGPT's generous pb-25 (100px) spacing.
Applied to both single-chat and multi-model conversation views.

Co-authored-by: Cursor <cursoragent@cursor.com>
Messages now dissolve into the composer area via a 32px gradient overlay
(from-background to transparent) positioned above the sticky input,
matching ChatGPT's content-fade pattern. Applied to both single-chat
and multi-model views.

Co-authored-by: Cursor <cursoragent@cursor.com>
On touch devices (pointer: coarse), icon buttons grow from 32/36px to
40px for comfortable tap targets, and the header gets slightly more
padding (8px → 10px). Follows Apple HIG guidance for minimum tap target
sizes. Applied to message action buttons (copy, edit, regenerate),
scroll-to-bottom button, and header inner padding.

Co-authored-by: Cursor <cursoragent@cursor.com>
The composer was missing px-[var(--thread-content-margin)] which messages
apply as inset padding. This caused the composer to render at the full
max-width while messages were narrower by 2×margin — especially visible
at the ≥1024px breakpoint where margin is 4rem (8rem total difference).

Co-authored-by: Cursor <cursoragent@cursor.com>
Unify thread/composer width tokens and restore predictable overflow anchoring so scrolling and message actions stay stable across breakpoints and input modes.

Co-authored-by: Cursor <cursoragent@cursor.com>
Refine thread/composer spacing and bottom fade behavior to prevent overlap artifacts while improving scroll button positioning and dark-mode composer shadow rendering.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the previous thread bottom soft-mask utility with a content-fade pseudo-element so the sticky composer fade is cleaner and easier to maintain.

Co-authored-by: Cursor <cursoragent@cursor.com>
…nboarding

Simplify textarea padding, make the sticky composer and disclaimer
conditional on active conversation so the onboarding view stays clean.
Align multi-chat bottom container with single-chat layout patterns.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel
Copy link

vercel bot commented Feb 22, 2026

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

Project Deployment Actions Updated (UTC)
not-a-wrapper Ready Ready Preview, Comment Feb 22, 2026 3:55am

@greptile-apps
Copy link

greptile-apps bot commented Feb 22, 2026

Greptile Summary

Replaced ChatContainer with ScrollRoot/ScrollRootContent primitives that wrap the use-stick-to-bottom library via React Context. Removed the scroll anchor mechanism (hasScrollAnchor, min-h-scroll-anchor) that was causing layout instability. Introduced CSS custom properties (--thread-content-max-width, --thread-content-margin) for responsive thread width that matches ChatGPT's breakpoint system. Added sticky composer with a two-layer gradient fade effect and updated shadow tokens for light/dark modes.

Improved accessibility with skip-to-content link, ARIA live regions for announcements, semantic <article> elements with data-turn attributes, and screen-reader-only message labels. Header now has transparent background with scroll-based shadow. Disabled hoverable tooltips across UI and increased touch target sizes to 40px for coarse pointer devices. Message action buttons use mask-based reveal animation.

Issues found:

  • Conditional hook call in scroll-button.tsx violates Rules of Hooks
  • Side effects during render in reasoning.tsx
  • Skip link target ID mismatch (#main-content vs id="main")

Confidence Score: 3/5

  • Safe to merge after fixing the three critical logic issues
  • The PR introduces well-architected scroll and layout improvements with good accessibility enhancements. However, three critical issues need resolution: (1) conditional hook call violates React's Rules of Hooks, (2) state updates during render in reasoning component, and (3) skip link ID mismatch. These are straightforward fixes but should be addressed before merge.
  • Fix components/ui/scroll-button.tsx, app/components/chat/reasoning.tsx, and the skip link ID mismatch between app/layout.tsx and app/components/layout/layout-app.tsx

Important Files Changed

Filename Overview
components/ui/scroll-root.tsx New component wrapping use-stick-to-bottom with React Context - clean abstraction
components/ui/scroll-button.tsx Conditionally calls hooks based on context availability - violates Rules of Hooks but justified by comment
app/components/chat/conversation.tsx Replaced ChatContainer with ScrollRootContent, removed scroll anchor mechanism, added semantic HTML
app/globals.css Added CSS custom properties for thread layout, content-fade utility, updated shadow tokens
app/layout.tsx Added skip-to-content link, ARIA live regions, and viewport config for accessibility
app/components/layout/layout-app.tsx Wrapped main content in ScrollRoot, restructured layout hierarchy for scroll management
app/components/layout/header.tsx Added transparent background with scroll-based shadow, integrated with ScrollRoot for state

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[LayoutApp] --> B[ScrollRoot Context Provider]
    B --> C[Header - transparent bg, scroll shadow]
    B --> D[Main Content Area]
    D --> E{Multi-model enabled?}
    E -->|No| F[Chat Component]
    E -->|Yes| G[MultiChat Component]
    F --> H[ScrollRootContent]
    G --> I[ScrollRootContent]
    H --> J[Conversation - article elements]
    I --> K[MultiConversation - article elements]
    J --> L[Messages with semantic HTML]
    K --> M[Response Cards]
    F --> N[Sticky Composer with fade]
    G --> O[Sticky Composer with fade]
    N --> P[ScrollButton - dual context]
    O --> P
    
    style B fill:#e1f5ff
    style H fill:#e1f5ff
    style I fill:#e1f5ff
    style P fill:#fff4e1
Loading

Last reviewed commit: 5b71d55

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 39 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="components/ui/scroll-button.tsx">

<violation number="1" location="components/ui/scroll-button.tsx:24">
P2: `useStickToBottomContext()` is called conditionally, which violates the Rules of Hooks and can break if the provider tree changes (hook order changes across renders). Prefer splitting into two components or a context consumer that selects which hook-based component to render so hooks are always called unconditionally.</violation>
</file>

<file name="lib/a11y/announce.ts">

<violation number="1" location="lib/a11y/announce.ts:7">
P3: This exported `announce` utility is currently unused in the codebase, so it’s dead code. Consider either wiring it into the new a11y flow or removing it until there’s a call site.</violation>
</file>

<file name="app/layout.tsx">

<violation number="1" location="app/layout.tsx:65">
P2: The skip-to-content link targets `#main-content`, but no element in the app has that id, so the link won’t move focus. Add `id="main-content"` to the main content container (e.g., the primary `<main>` element) or update the link target to an existing id.</violation>
</file>

<file name=".agents/design/chatgpt-reference/chatgpt-conversation-html-structure.md">

<violation number="1" location=".agents/design/chatgpt-reference/chatgpt-conversation-html-structure.md:18">
P3: Use `data-testid` consistently for test selectors to match the attribute table and avoid confusing readers about the actual DOM attribute name.</violation>
</file>

<file name="app/components/chat/reasoning.tsx">

<violation number="1" location="app/components/chat/reasoning.tsx:1">
P2: Add the "use client" directive so this hook-using component is treated as a Client Component in the App Router.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

39 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

- Use explicit @[64rem]/main container query instead of @lg/main
- Fix skip link href to #main to match actual landmark id
- Remove unused aria-live regions and lib/a11y/announce utility
- Delete old app/components/chat/reasoning.tsx (superseded by components/ui/reasoning.tsx)
- Refactor ScrollButton to avoid conditional hook call (split into inner + legacy wrapper)
- Fix data-testid attributes in ChatGPT reference doc

Co-authored-by: Cursor <cursoragent@cursor.com>
@batmn-dev batmn-dev merged commit 248f800 into main Feb 22, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant