Claude-skill-registry-data macos-scrollbar
Custom themed scrollbars for macOS WKWebView apps. Use when styling scrollbars in the native macOS app, fixing scrollbar theming issues, implementing custom scroll containers that work in WKWebView, or debugging scroll position persistence issues with tabs.
git clone https://github.com/majiayu000/claude-skill-registry-data
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry-data "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/macos-scrollbar" ~/.claude/skills/majiayu000-claude-skill-registry-data-macos-scrollbar && rm -rf "$T"
data/macos-scrollbar/SKILL.mdMacOS WKWebView Custom Scrollbars
The Problem
WKWebView on macOS does not support standard CSS scrollbar styling:
pseudo-elements are ignored::-webkit-scrollbar
andscrollbar-color
CSS properties don't work reliablyscrollbar-width- Native scrollbars always render with system appearance
This means CSS-based scrollbar theming that works in browsers will NOT work in the native macOS app.
The Solution: Negative Margin Technique
Hide the native scrollbar using pure CSS layout (not pseudo-elements):
- Outer wrapper:
clips the native scrollbaroverflow: hidden - Inner scrollable div:
+overflow-y: scroll
pushes scrollbar outsidemarginRight: -20px - Padding compensation:
ensures content isn't cut offpaddingRight: 20px - Custom overlay: Render a themed scrollbar as a positioned DOM element
Usage
Use the
OverlayScrollbar component from @/components/OverlayScrollbar:
import { OverlayScrollbar } from "@/components/OverlayScrollbar"; // Basic usage <OverlayScrollbar className="h-full"> <div>Your scrollable content here</div> </OverlayScrollbar> // With scroll position persistence const scrollRef = useTabScrollPersistence(tabId); <OverlayScrollbar scrollRef={scrollRef} className="flex-1 h-full" style={{ backgroundColor: currentTheme.styles.surfacePrimary }} > <div>Content with scroll position saved</div> </OverlayScrollbar>
Component Props
| Prop | Type | Description |
|---|---|---|
| | Scrollable content |
| | CSS classes for outer wrapper |
| | Inline styles for outer wrapper |
| | Optional ref for scroll position access |
Features
- Theme-aware: Uses
for scrollbar colorcurrentTheme.styles.borderDefault - Auto-hide: Scrollbar fades out after 1 second of inactivity
- Hover to show: Scrollbar appears when hovering the container
- Drag support: Click and drag the thumb to scroll
- Track click: Click the track to jump to position
- Resize-aware: Updates when content or container size changes
When to Use
Use
OverlayScrollbar instead of native overflow-y-auto when:
- The scroll container needs themed scrollbars
- The component renders in the macOS WKWebView app
- You want consistent scrollbar appearance across web and native
When NOT to Use
- Very small scroll areas (the overlay adds complexity)
- Performance-critical lists with thousands of items (consider virtualization)
- Areas where native scrollbar behavior is preferred
Implementation Details
See the full component at:
src/components/OverlayScrollbar.tsx
Key constants:
- Margin to hide native scrollbar (macOS scrollbar is ~15-17px)SCROLLBAR_WIDTH = 20- Thumb minimum height: 30px
- Hide delay: 1000ms after scroll stops
- Fade transition: 150ms
Scroll Position Persistence for Tabs
When implementing scroll persistence for workspace tabs, use
useTabScrollPersistence with OverlayScrollbar.
How It Works
-
returns a ref and:useTabScrollPersistence(tabId)- Saves scroll position to a module-level Map on every scroll event
- Restores position when the component mounts (using ResizeObserver/MutationObserver for async content)
-
Pass the ref to
:OverlayScrollbarconst scrollRef = useTabScrollPersistence(tabId); <OverlayScrollbar scrollRef={scrollRef} className="flex-1"> {/* content */} </OverlayScrollbar>
Critical Rule: Keep OverlayScrollbar Mounted
The ref must be attached to a mounted element when
's effect runs.useTabScrollPersistence
If you conditionally render a different tree during loading, the ref won't be set and restoration will fail:
// BAD - OverlayScrollbar unmounts during loading, ref is null when effect runs if (isLoading) { return <Loader />; // Different tree, no OverlayScrollbar! } return ( <OverlayScrollbar scrollRef={scrollRef}> {/* content */} </OverlayScrollbar> );
// GOOD - OverlayScrollbar stays mounted, ref is always set return ( <OverlayScrollbar scrollRef={scrollRef} className="flex-1"> {isLoading ? ( <div className="flex h-full items-center justify-center"> <Loader /> </div> ) : ( {/* actual content */} )} </OverlayScrollbar> );
Why This Matters
The
useTabScrollPersistence hook runs its effect on mount with [tabId] dependency:
useEffect(() => { const element = scrollRef.current; if (!element) return; // Early return if ref not set! // Set up observers and attempt restoration... }, [tabId]);
If the element isn't mounted when the effect runs:
isscrollRef.currentnull- Effect returns early without setting up observers
- When content loads and OverlayScrollbar mounts, the effect doesn't re-run
- No scroll restoration happens
Checklist for Scroll Persistence
- Use
(not nativeOverlayScrollbar
) for the scroll containeroverflow-y-auto - Pass
fromscrollRef
touseTabScrollPersistenceOverlayScrollbar - Keep
in the component tree during ALL render states (loading, error, etc.)OverlayScrollbar - Render loading/error states as CHILDREN of
, not as alternative returnsOverlayScrollbar
Key Files
| File | Purpose |
|---|---|
| Hook that saves/restores scroll position per tab |
| Custom scrollbar with prop |
| Reference implementation (lines 1370-1457) |
| Chat implementation with loading state handling |