Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/layer-dialog-native-scrollbar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/kumo": patch
---

Hide the native scrollbars of the LayerDialog body and Sidebar content with Kumo's own CSS. Base UI only hides them with a style element it injects into the document head, so client routers that swap the head (such as Astro's ClientRouter) or a Content Security Policy that blocks inline styles could leave empty scrollbar tracks along the dialog body.
14 changes: 14 additions & 0 deletions packages/kumo/src/components/layer-dialog/layer-dialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,20 @@ describe("LayerDialog.Body edge clipping", () => {

cleanup();
});

it("hides native scrollbars without Base UI's injected style", () => {
// Base UI forces `overflow: scroll` on the viewport and hides the native
// scrollbars only through a <style> it adds to <head> once per document.
// Head-swapping client routers (Astro's ClientRouter) remove that style,
// which shows empty scrollbar tracks wherever scrollbars are always
// visible, so the viewport must carry Kumo's own rule.
const { viewport, cleanup } = renderBodyWithLeadingInput(true);

expect(viewport.style.overflow).toBe("scroll");
expect(viewport.className).toMatch(/(^|\s)no-scrollbar(\s|$)/);

cleanup();
});
});

describe("LayerDialog dismissal", () => {
Expand Down
9 changes: 7 additions & 2 deletions packages/kumo/src/components/layer-dialog/layer-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -455,9 +455,14 @@ function LayerDialogBody({ children }: LayerDialogBodyProps) {
<ScrollAreaBase.Root className="relative flex min-h-0 flex-1 flex-col">
{/* The top fade falls back to 0px: the body always opens scrolled to
the top, and Safari skips Base UI's registered 0px initial value,
so a non-zero fallback fades out the first row until measured. */}
so a non-zero fallback fades out the first row until measured.

no-scrollbar keeps the native scrollbars of Base UI's
`overflow: scroll` viewport hidden. Base UI hides them only with a
<style> it adds to <head> once per document, which head-swapping
client routers and strict CSPs can drop. */}
<ScrollAreaBase.Viewport
className="min-h-0 flex-1 overscroll-none [mask-image:linear-gradient(to_bottom,transparent_0,black_min(24px,var(--scroll-area-overflow-y-start,0px)),black_calc(100%-min(24px,var(--scroll-area-overflow-y-end,24px))),transparent_100%)]"
className="no-scrollbar min-h-0 flex-1 overscroll-none [mask-image:linear-gradient(to_bottom,transparent_0,black_min(24px,var(--scroll-area-overflow-y-start,0px)),black_calc(100%-min(24px,var(--scroll-area-overflow-y-end,24px))),transparent_100%)]"
onScroll={handleScroll}
>
{/* pt-0.5 keeps a first-child control's ring-drawn border and
Expand Down
13 changes: 13 additions & 0 deletions packages/kumo/src/components/sidebar/sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,19 @@ describe("Sidebar contained mode", () => {
});
});

describe("Sidebar.Content scrollbar", () => {
it("should hide the native scrollbar without Base UI's injected style", () => {
render(
<TestSidebar defaultOpen>
<SidebarContent>Content</SidebarContent>
</TestSidebar>,
);
// Base UI's own rule lives in a <style> that head-swapping client
// routers can remove, so the viewport must carry Kumo's.
expect(getViewport().className).toMatch(/(^|\s)no-scrollbar(\s|$)/);
});
});

// ============================================================================
// Mobile behavior
// ============================================================================
Expand Down
4 changes: 4 additions & 0 deletions packages/kumo/src/components/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,10 @@ const SidebarContent = forwardRef<
"h-full px-[11px] py-3 group-not-data-[state=collapsed]/sidebar:px-3.5",
"transition-[padding] duration-(--sidebar-animation-duration)",
"overflow-x-hidden!",
// Hide the native scrollbar with Kumo's CSS rather than relying on
// the <style> Base UI adds to <head>, which head-swapping client
// routers and strict CSPs can drop.
"no-scrollbar",
// Scroll fade via CSS mask driven by Base UI overflow CSS variables
"[mask-image:linear-gradient(to_bottom,transparent_0,black_min(24px,var(--scroll-area-overflow-y-start,24px)),black_calc(100%-min(24px,var(--scroll-area-overflow-y-end,24px))),transparent_100%)]",
)}
Expand Down
Loading