fix(code-block): use absolute positioning for action buttons to fix clicks in nested scroll containers - #498
fix(code-block): use absolute positioning for action buttons to fix clicks in nested scroll containers#498sleitor wants to merge 2 commits into
Conversation
…lick events in nested scroll containers Switches the code block action button wrapper from position:sticky to position:absolute (with position:relative on the container) so that hit-testing works correctly in layouts with multiple nested overflow:auto scroll containers. Previously, the sticky + pointer-events-none/auto pattern caused browsers to mis-route click events to the code block wrapper rather than the copy/ download buttons when embedded in 2+ nested scroll containers. Closes vercel#494
|
@sleitor is attempting to deploy a commit to the Vercel Team on Vercel. A member of the Team first needs to authorize it. |
|
@sleitor Can you update the vercel (bot) suggested changes, as 'mermaid' also has similar issue |
…ks in nested scroll containers Same fix as code-block (ab0a5ae): switches the mermaid block action button wrapper from position:sticky to position:absolute so that hit-testing works correctly in layouts with multiple nested overflow:auto scroll containers. The mermaid block container already has position:relative, so only the inner toolbar wrapper needed updating. Fixes vercel#494
|
@manjujidagi Good catch! The Mermaid block action buttons (download/copy/fullscreen toolbar) had the same |
|
@sleitor When do you think this will be merged ? I am waiting for this update |
|
@manjujidagi The fix is ready — the remaining blocker is Vercel team authorization for the deployment preview (the standard bot checkpoint for external contributors). Merge timeline is up to the Vercel/streamdown maintainers, but I'll continue to follow up with them. Thanks for your patience! |
|
Hey @sleitor, Thanks for the PR |
|
Hey @farnabaz, thanks for taking a look! Here's a minimal standalone HTML reproduction (no build step needed — just save and open in a browser, or paste into a local static server): <!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Streamdown nested-scroll button repro</title>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
html, body { margin: 0; height: 100%; font-family: sans-serif; }
.sidebar-layout { display: flex; height: 100vh; }
.sidebar { width: 220px; overflow: auto; background: #f3f3f3; border-right: 1px solid #ddd; }
.sidebar-item { padding: 12px; border-bottom: 1px solid #ddd; }
.chat-scroll { flex: 1; overflow: auto; padding: 16px; }
/* Reproduces streamdown's pre-fix sticky+pointer-events pattern */
.code-block { position: static; margin: 16px 0; border: 1px solid #ccc; border-radius: 6px; }
.code-actions-wrapper {
pointer-events: none;
position: sticky;
top: 8px;
z-index: 10;
margin-top: -40px;
height: 32px;
display: flex;
align-items: center;
justify-content: flex-end;
}
.code-actions { pointer-events: auto; }
.code-actions button { padding: 6px 10px; cursor: pointer; }
.code-body { overflow-x: auto; padding: 12px; background: #1e1e1e; color: #eee; }
pre { margin: 0; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState } = React;
function CodeBlock() {
const [clicks, setClicks] = useState(0);
return (
<div className="code-block" data-streamdown="code-block">
<div className="code-actions-wrapper">
<div className="code-actions" data-streamdown="code-block-actions">
<button onClick={() => setClicks((c) => c + 1)}>
Copy (clicked {clicks}x)
</button>
</div>
</div>
<div className="code-body">
<pre>{`function example() {\n return "hello world";\n}`}</pre>
</div>
</div>
);
}
function App() {
const rows = Array.from({ length: 60 }, (_, i) => i);
return (
<div className="sidebar-layout">
<div className="sidebar">
{rows.map((i) => (
<div className="sidebar-item" key={i}>Sidebar item {i}</div>
))}
</div>
<div className="chat-scroll">
<p>Click "Copy" below — with two nested overflow:auto ancestors (sidebar + this chat-scroll area), the click doesn't reach the button; DevTools "select element" over it selects the code-block wrapper div instead.</p>
<CodeBlock />
{rows.map((i) => (
<p key={i}>Filler paragraph {i}.</p>
))}
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
</script>
</body>
</html>Steps:
This mirrors exactly the CSS pattern from #494 ( |
|
Thanks for reproduction @sleitor, but I still didn't get it. Checkout quick video below. What did I miss? (I did test with chrome and firefox too, but they all were same) Screen.Recording.2026-08-31.at.18.38.35.mov |
|
Hey @farnabaz — thanks for testing it out! Sorry the standalone HTML repro didn't click for you. A couple of things that could explain why it works fine in your test:
Rather than iterate on more guesses, let me build an actual
That'll help me nail an exact match. Appreciate your patience on this one! |
Problem
When streamdown is embedded in a layout with two or more nested scroll containers (e.g., an outer
overflow-autosidebar + an inneroverflow-autochat scroll area), the code block copy/download buttons are visible but unclickable.The root cause: the
sticky+pointer-events-none/autopattern for the action button wrapper causes browser hit-testing to resolve clicks to the code block containerdivinstead of reaching the buttons when there are multipleoverflow: autoancestors.Fixes #494
Solution
position: stickytoposition: absolutewithtop-2 right-2position: relativeto theCodeBlockContainerso absolute positioning is scoped to the code blockThis makes buttons reliably clickable in all scroll-container configurations.
Trade-off: Buttons no longer scroll with long code blocks (they stay fixed at the top-right of the code block). This is the same behavior as the workaround documented in the issue, and arguably cleaner UX — the buttons are always visible and always reachable.
Changes
packages/streamdown/lib/code-block/container.tsx: addrelativeclasspackages/streamdown/lib/code-block/index.tsx: change outer wrapper frompointer-events-none sticky top-2 z-10 -mt-10 flex h-8 items-center justify-endtopointer-events-none absolute top-2 right-2 z-10 flex items-center.changeset/fix-code-block-buttons-absolute.md: patch changesetTesting
All 982 existing tests pass (
pnpm --filter streamdown test). Biome check clean.