Skip to content

fix: constrain Tooltip popup width to available viewport space#529

Closed
mattrothenberg wants to merge 15 commits into
mainfrom
fix/tooltip-max-width-overflow
Closed

fix: constrain Tooltip popup width to available viewport space#529
mattrothenberg wants to merge 15 commits into
mainfrom
fix/tooltip-max-width-overflow

Conversation

@mattrothenberg
Copy link
Copy Markdown
Collaborator

@mattrothenberg mattrothenberg commented May 21, 2026

Problem

Kumo's Tooltip popup has no max-width constraint. When tooltip content is long (e.g. multi-sentence descriptions), the popup grows unbounded and overflows past the viewport edge.

The internal common/components/Kumo/Tooltip/Tooltip wrapper in Stratus hardcodes maxWidth: 300 on the popup, which masked this issue. Consumers switching to kumo's Tooltip directly hit the overflow.

Fix

Adds max-w-[var(--available-width)] to the Tooltip popup. --available-width is a CSS variable that Base UI's Positioner already sets on the positioner element — it represents the available space between the trigger and the viewport edge. Since CSS custom properties inherit, the Popup (a child of the Positioner) can use it directly.

This means:

  • Tooltips near the center of the page remain unconstrained (available width is large)
  • Tooltips near viewport edges automatically wrap to fit
  • No hardcoded magic number — the constraint is dynamic and responsive

Applied to both the Tooltip component and the tooltipVariants() utility function.

Demo

Added a "Long Content / Overflow" example to the Astro docs (/components/tooltip) with three triggers spread across the full width, each with the same long lorem ipsum content. Hovering the edge triggers shows the tooltip constraining correctly.

- Replace border-kumo-hairline with border-kumo-line throughout
- Replace bg-kumo-hairline with bg-kumo-line on separator
- Add --sidebar-animation-duration, --sidebar-easing, --sidebar-bg CSS custom properties
- Replace hardcoded duration-250/ease-cubic-bezier with CSS custom property references
- Use bg-(--sidebar-bg) on sidebar root containers for theme overridability
- Update focus styles: ring-2/ring-kumo-brand → outline-none/text-kumo-strong/bg-kumo-tint
- Add opacity-50 to MenuButton icons, remove [&>svg]:text-kumo-subtle override
- Update spacing: header h-[58px], content px-3.5, footer h-12, menu gap-y-px, button min-h-8.5
- Replace MenuSub border-l with absolute w-px bg-kumo-line indicator
- Change Separator from hr to div with nested border-b
- Update width constants: 16rem→16.25rem, 3rem→57px
- Add isolate to sidebar root, reduce z-index from z-20 to z-1/z-2
- Slim resize handle from w-1 to w-0.5
Adds data-state, data-side, data-variant, data-collapsible and group/sidebar
class to the mobile dialog inner div so collapsible groups and other
state-dependent styles work correctly on mobile.

Cherry-picked from PR #450.
- Widen hit area to 16px with thin visual line via ::after pseudo-element
- Add role=separator, aria-orientation, aria-valuenow/min/max for a11y
- Add tabIndex=0 for keyboard focusability
- Support Arrow keys (resize by 10px steps), Home (collapse), End (expand to max)
- Add focus-visible styling on the visual line
…ontent

BREAKING CHANGE: Remove three unused subcomponents to simplify the API surface.

- Remove SidebarInput (search trigger button) — consumers should build
  their own search trigger inline or use CommandPalette
- Remove SidebarMenuAction (right-aligned action overlay) — unused in practice
- Remove SidebarGroupContent and group-level collapsible props (collapsible,
  defaultOpen, open, onOpenChange) from SidebarGroup — all collapse behavior
  is now exclusively at the item level via Sidebar.Collapsible
- Remove SidebarGroupCollapsibleContext
- Simplify SidebarGroup to a plain flex container
- Rework SidebarGroupLabel: collapsed state now shows a thin divider between
  icon groups (hidden for first group); uses grid-rows animation
- Update all JSDoc to remove MenuAction references
- Clean up index.ts and src/index.ts exports
…ementation

- Remove @base-ui/react/collapsible dependency from sidebar
- New SidebarCollapseContext provides isOpen, toggle, contentId to children
- SidebarCollapsible: custom forwardRef component with controlled/uncontrolled
  state, keyboard auto-expand on focus-visible, auto-collapse on blur
- SidebarCollapsibleTrigger: uses React.cloneElement to merge aria-expanded,
  aria-controls, data-open, and onClick onto the render element
- SidebarCollapsibleContent: CSS grid-rows-[0fr]/[1fr] animation (no DOM
  height measurement needed), always mounted, proper aria-hidden + inert
- SidebarMenuChevron: reads isOpen from context instead of relying on
  Base UI data-panel-open attribute; uses size=12 weight=bold for consistency
…Trigger

New features:
- contained prop: uses absolute (not fixed) positioning for collapsed
  sidebar, scoped inside bounded parent for demos/embedded sidebars
- peekable prop: hovering/focusing collapsed sidebar temporarily expands
  it; state becomes 'peeking'; moving away collapses back
- SidebarState type: 'expanded' | 'collapsed' | 'peeking'
- animationDuration prop: configurable animation duration (default 250ms)

Architecture:
- Two-layer rail/content design: <aside> (rail) width drives layout,
  inner content-container can overlay when peeking
- Footer rendered outside peek zone so hover doesn't trigger peek
- Footer now sticky bottom with width tracking via CSS custom properties

UI:
- SidebarPanelIcon: animated custom SVG replacing Phosphor SidebarSimpleIcon;
  vertical divider line slides based on open/closed state
- SidebarTrigger: now aria-expanded, dynamic aria-label, size-8.5 grid layout
- Remove @phosphor-icons/react SidebarSimpleIcon dependency
New subcomponents:
- SidebarSlidingViews: container managing activeKey-based horizontal
  slide transitions between navigation surfaces (e.g., account ↔ zone)
- SidebarSlidingView: individual panel; inactive views get aria-hidden,
  inert, invisible, and pointer-events-none

Uses CSS transforms (no motion/react dependency needed) with
prefers-reduced-motion support. Animation uses the same --sidebar-easing
and --sidebar-animation-duration custom properties as the rest of the sidebar.
- Remove CollapsibleGroupDemo (group-level collapsible removed)
- Remove references to SidebarInput, MenuAction, GroupContent
- Add PeekingDemo, SlidingViewsDemo showcasing new features
- Rewrite FullDemo as kitchen sink with sliding views, nested collapsibles,
  custom search button, and footer trigger
- All demos now use contained prop for bounded demo containers
- Update tokens: hairline→line, duration-250→CSS custom properties
- Update icon imports: CubeIcon, StackIcon etc. replacing Cloud/Rocket/Flask
- Update docs page: remove deprecated sections, add peeking + sliding views
- Inset focus rings (ring-2 ring-inset ring-kumo-brand) on all interactive items
- Remove background-color transitions to prevent flash on theme switch
- Footer inside content container with mt-auto (fixes collapse float)
- MenuSubButton restored to original sizing, flex content span for chevron
- No collapsed padding hacks — overflow-hidden clips naturally
- Vertical scroll-fade CSS (data-overflowing-y) in kumo-binding.css
- SidebarContent overflow detection via ResizeObserver + MutationObserver
- Demo: search uses MenuButton with ring class, inside SlidingView
Covers:
- Export structure (compound component, removed components, variants, displayNames)
- Toggle: defaultOpen, trigger click, aria-expanded, controlled onOpenChange
- Collapsible: default closed, defaultOpen, toggle click, aria-controls
  linking, role=region, inert on closed content
- Peeking: peekable=false does nothing, mouseEnter triggers peek,
  mouseLeave stops peek, no peek when expanded
- SlidingViews: active view visible, inactive aria-hidden + inert,
  switching activeKey
- ResizeHandle: ARIA separator role, orientation, label, valuemin/max, tabindex
- MenuButton: auto-wrap in li, data-active, link rendering
- Contained mode: min-h-svh presence/absence
- Remove MutationObserver from SidebarContent — ResizeObserver already
  fires when content rect changes (collapsible open/close), the
  MutationObserver on childList+subtree was a performance hazard
- Extract inline onBlur handler in SidebarRoot to useCallback
- Remove will-change-[width] from rail and content container — animating
  width triggers layout every frame regardless, will-change just wastes
  GPU memory by pre-promoting to a compositing layer
- Simplify useMemo dep array in Provider — only reactive state values
  (state, open, openMobile, isMobile, width, isResizing, isPeeking)
  need to be deps; props and useCallback refs are stable
- Fix TS error in CollapsibleTrigger onClick prop typing
- Trigger uses same px-3 padding as MenuButton for icon alignment
- Trigger is natural width (not w-full) so it doesn't stretch when expanded
- Footer px-2 matches content area padding
- Drop MutationObserver (ResizeObserver sufficient for overflow detection)
- Extract inline onBlur to useCallback
- Remove will-change-[width] (pure GPU memory overhead)
- Simplify useMemo dep array to only reactive state values
- Fix TS error in CollapsibleTrigger onClick typing
- Replace manual ResizeObserver overflow detection with Base UI ScrollArea
- Slim custom scrollbar (w-1.5, rounded thumb, bg-kumo-line) that fades
  in on scroll/hover
- Scroll fade via CSS mask-image driven by Base UI's --scroll-area-overflow-y
  CSS variables — no JS needed, works with SSR
- Remove custom scroll-fade-y keyframes from kumo-binding.css
- Stub getAnimations in test for happy-dom compatibility
Tooltip popups with long content could overflow past the viewport edge.
This adds max-w-[var(--available-width)] to the popup, using the CSS
variable Base UI's Positioner already provides. The constraint applies
to both the Tooltip component and the tooltipVariants() utility.

Also adds an overflow demo to the Astro docs showing the behavior with
triggers at different viewport positions.
@mattrothenberg mattrothenberg deleted the fix/tooltip-max-width-overflow branch May 21, 2026 15:19
@github-actions
Copy link
Copy Markdown
Contributor

Docs Preview

View docs preview

Commit: 9689c13

@github-actions
Copy link
Copy Markdown
Contributor

Visual Regression Report — 23 changed, 15 unchanged

23 screenshot(s) with visual changes:

Button / Variant: Secondary

161 px (0.16%) changed

Before After Diff
Before After Diff

Button / Variant: Ghost

175 px (0.17%) changed

Before After Diff
Before After Diff

Button / Sizes

676 px (0.67%) changed

Before After Diff
Before After Diff

Button / With Icon

422 px (0.42%) changed

Before After Diff
Before After Diff

Button / Loading State

7 px (0.01%) changed

Before After Diff
Before After Diff

Dialog / Dialog Alert

1,182 px (1.16%) changed

Before After Diff
Before After Diff

Dialog / Dialog Confirmation

821 px (0.81%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Select

130 px (0.13%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Dropdown

244 px (0.24%) changed

Before After Diff
Before After Diff

Dialog (Open)

0 px (0%) changed

Before After Diff
Before After Diff

Select / Select Basic

485 px (0.48%) changed

Before After Diff
Before After Diff

Select / Select Sizes

869 px (0.47%) changed

Before After Diff
Before After Diff

Select / Select Without Label

161 px (0.16%) changed

Before After Diff
Before After Diff

Select / Select With Field

852 px (0.72%) changed

Before After Diff
Before After Diff

Select / Select Placeholder

418 px (0.41%) changed

Before After Diff
Before After Diff

Select / Select With Tooltip

294 px (0.29%) changed

Before After Diff
Before After Diff

Select / Select Custom Rendering

413 px (0.41%) changed

Before After Diff
Before After Diff

Select / Select Loading

1,429 px (0.71%) changed

Before After Diff
Before After Diff

Select / Select Multiple

851 px (0.84%) changed

Before After Diff
Before After Diff

Select / Select Disabled Items

176 px (0.17%) changed

Before After Diff
Before After Diff

Select / Select Grouped

194 px (0.19%) changed

Before After Diff
Before After Diff

Select / Select Long List

1,428 px (1.21%) changed

Before After Diff
Before After Diff

Select (Open)

2,657 px (0.01%) changed

Before After Diff
Before After Diff
15 screenshot(s) unchanged
  • Button / Basic
  • Button / Variant: Primary
  • Button / Variant: Destructive
  • Button / Variant: Outline
  • Button / Variant: Secondary Destructive
  • Button / Icon Only
  • Button / Disabled State
  • Button / Title
  • Button / Link as Button
  • Dialog / Dialog With Actions
  • Dialog / Dialog Basic
  • Dialog / Dialog With Combobox
  • Select / Select Complex
  • Select / Select Disabled Options
  • Select / Select Grouped With Disabled

Generated by Kumo Visual Regression

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