Skip to content

docs(design): menus#1078

Open
sampotts wants to merge 1 commit intomainfrom
feat/menu-ui-component
Open

docs(design): menus#1078
sampotts wants to merge 1 commit intomainfrom
feat/menu-ui-component

Conversation

@sampotts
Copy link
Copy Markdown
Collaborator

Refs #1066

Copilot AI review requested due to automatic review settings March 23, 2026 05:07
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 23, 2026

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

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment Mar 23, 2026 5:07am

Request Review

@netlify
Copy link
Copy Markdown

netlify bot commented Mar 23, 2026

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit f2943dc
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/69c0ca90706c5b00088e5711
😎 Deploy Preview https://deploy-preview-1078--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions
Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

🎨 @videojs/html — no changes
Presets (7)
Entry Size
/video (default) 23.37 kB
/video (default + hls) 153.84 kB
/video (minimal) 23.21 kB
/video (minimal + hls) 153.65 kB
/audio (default) 21.51 kB
/audio (minimal) 21.52 kB
/background 6.51 kB
Media (5)
Entry Size
/media/background-video 1.03 kB
/media/container 1.59 kB
/media/dash-video 236.04 kB
/media/hls-video 131.57 kB
/media/simple-hls-video 12.33 kB
Players (3)
Entry Size
/video/player 6.24 kB
/audio/player 6.23 kB
/background/player 6.22 kB
Skins (16)
Entry Type Size
/video/minimal-skin.css css 3.25 kB
/video/skin.css css 3.31 kB
/video/minimal-skin js 22.40 kB
/video/minimal-skin.tailwind js 22.82 kB
/video/skin js 22.57 kB
/video/skin.tailwind js 22.81 kB
/audio/minimal-skin.css css 2.37 kB
/audio/skin.css css 2.36 kB
/audio/minimal-skin js 20.77 kB
/audio/minimal-skin.tailwind js 21.10 kB
/audio/skin js 20.73 kB
/audio/skin.tailwind js 21.02 kB
/background/skin.css css 117 B
/background/skin js 1.13 kB
/base.css css 157 B
/shared.css css 86 B
UI Components (21)
Entry Size
/ui/alert-dialog 2.16 kB
/ui/alert-dialog-close 1.60 kB
/ui/alert-dialog-description 1.52 kB
/ui/alert-dialog-title 1.50 kB
/ui/buffering-indicator 1.79 kB
/ui/captions-button 1.83 kB
/ui/controls 1.60 kB
/ui/fullscreen-button 1.82 kB
/ui/mute-button 1.81 kB
/ui/pip-button 1.81 kB
/ui/play-button 1.81 kB
/ui/playback-rate-button 1.83 kB
/ui/popover 3.29 kB
/ui/poster 1.70 kB
/ui/seek-button 1.80 kB
/ui/slider 2.00 kB
/ui/thumbnail 2.14 kB
/ui/time 1.62 kB
/ui/time-slider 2.12 kB
/ui/tooltip 2.42 kB
/ui/volume-slider 2.20 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react — no changes
Presets (7)
Entry Size
/video (default) 18.95 kB
/video (default + hls) 149.81 kB
/video (minimal) 18.98 kB
/video (minimal + hls) 149.77 kB
/audio (default) 15.80 kB
/audio (minimal) 15.85 kB
/background 3.13 kB
Media (4)
Entry Size
/media/background-video 476 B
/media/dash-video 236.19 kB
/media/hls-video 131.59 kB
/media/simple-hls-video 12.34 kB
Skins (14)
Entry Type Size
/video/minimal-skin.css css 3.25 kB
/video/skin.css css 3.31 kB
/video/minimal-skin js 18.91 kB
/video/minimal-skin.tailwind js 22.16 kB
/video/skin js 18.89 kB
/video/skin.tailwind js 22.11 kB
/audio/minimal-skin.css css 2.37 kB
/audio/skin.css css 2.36 kB
/audio/minimal-skin js 15.78 kB
/audio/minimal-skin.tailwind js 18.21 kB
/audio/skin js 15.75 kB
/audio/skin.tailwind js 18.16 kB
/background/skin.css css 90 B
/background/skin js 272 B
UI Components (18)
Entry Size
/ui/alert-dialog 2.21 kB
/ui/buffering-indicator 2.20 kB
/ui/captions-button 2.25 kB
/ui/controls 2.23 kB
/ui/fullscreen-button 2.25 kB
/ui/mute-button 2.26 kB
/ui/pip-button 2.22 kB
/ui/play-button 2.25 kB
/ui/playback-rate-button 2.25 kB
/ui/popover 2.85 kB
/ui/poster 2.05 kB
/ui/seek-button 2.30 kB
/ui/slider 3.16 kB
/ui/thumbnail 2.01 kB
/ui/time 1.92 kB
/ui/time-slider 2.66 kB
/ui/tooltip 2.69 kB
/ui/volume-slider 2.68 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core — no changes
Entries (6)
Entry Size
. 4.81 kB
/dom 8.41 kB
/dom/media/custom-media-element 1.81 kB
/dom/media/dash 235.62 kB
/dom/media/hls 131.27 kB
/dom/media/simple-hls 11.85 kB
🏷️ @videojs/element — no changes
Entries (2)
Entry Size
. 999 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Size
. 1.32 kB
/html 700 B
/react 360 B
🔧 @videojs/utils — no changes
Entries (10)
Entry Size
/array 104 B
/dom 1.25 kB
/events 227 B
/function 261 B
/object 119 B
/predicate 265 B
/string 148 B
/style 190 B
/time 478 B
/number 158 B
📦 @videojs/spf — no changes
Entries (3)
Entry Size
. 40 B
/dom 10.04 kB
/playback-engine 9.94 kB

ℹ️ How to interpret

All sizes are standalone totals (minified + brotli).

Icon Meaning
No change
🔺 Increased ≤ 10%
🔴 Increased > 10%
🔽 Decreased
🆕 New (no baseline)

Run pnpm size locally to check current sizes.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

@videojs videojs deleted a comment from Copilot AI Mar 23, 2026
@videojs videojs deleted a comment from Copilot AI Mar 23, 2026
@videojs videojs deleted a comment from Copilot AI Mar 23, 2026
@videojs videojs deleted a comment from Copilot AI Mar 23, 2026
@videojs videojs deleted a comment from Copilot AI Mar 23, 2026
@videojs videojs deleted a comment from Copilot AI Mar 23, 2026
@videojs videojs deleted a comment from Copilot AI Mar 23, 2026
Copy link
Copy Markdown
Member

@mihar-22 mihar-22 left a comment

Choose a reason for hiding this comment

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

Blockers

  1. "Flyout" and "panel" aren't established terminology. The component library ecosystem (Base UI, Radix) uses "submenu" - the distinction is how submenus render (cascading vs non-cascading), not a different component category. Introducing unfamiliar vocabulary adds friction for anyone coming from or working simultaneously with these libraries.
  2. mode prop creates a polymorphic composition tree. The valid set of children depends on a runtime prop. Panel parts are no-ops in flyout mode, SubMenu parts are no-ops in panel mode. TypeScript can't catch this, and there's no runtime error either. It'd be cleaner if the composition itself determined the behavior rather than a prop changing which children are valid.
  3. Cascading (flyout) submenus may not be the right starting point for the player. The player operates in a constrained popover - cascading submenus that open as separate positioned popups don't fit well here. In-place transitions within a single container are what YouTube, Plyr, and most video player settings UIs use. It might make more sense to ground the design in non-cascading submenus rather than starting cascading first with a mode escape hatch for panels.
  4. Panel navigation model feels premature. PanelNavigationState, createCarouselTransition(), panel stack, rapid-navigation cancellation, 4 panel-specific parts - there's a lot of machinery here
  5. Panel parts overlap with SubMenu semantics. Panel is effectively a SubMenuContent rendered in-place, PanelTrigger is a SubMenuTrigger, PanelBack is a SubMenuTrigger in its open state. The hierarchy is the same - the difference is rendering strategy (in-place vs cascading). Base UI's NavigationMenu handles this distinction compositionally (Portal presence vs inline Viewport) rather than with parallel parts. Worth considering the same approach here.

Suggested direction

Non-cascading submenus as the default, using the existing SubMenu parts. Content acts as the viewport/transition container. SubMenuTrigger handles both forward and back navigation via visual state (data-open). Cascading (flyout) support can be additive later via Portal composition.

Transitions

Transitions and sizing are CSS-driven via custom properties and data attributes on Content:

  • --menu-width / --menu-height - measured dimensions of the active submenu, enabling smooth container resize transitions
  • --available-height - from popover positioning, for scroll containment on long lists
  • data-open - transition direction for slide animations

React

<Menu.Root>
  <Menu.Trigger>Settings</Menu.Trigger>
  <Menu.Content>
    <Menu.SubMenu>
      <Menu.SubMenuTrigger>Quality</Menu.SubMenuTrigger>
      <Menu.SubMenuContent>
        <Menu.RadioGroup value={quality} onValueChange={setQuality} label="Quality">
          <Menu.RadioItem value="auto">Auto</Menu.RadioItem>
          <Menu.RadioItem value="1080p">1080p</Menu.RadioItem>
          <Menu.RadioItem value="720p">720p</Menu.RadioItem>
        </Menu.RadioGroup>
      </Menu.SubMenuContent>
    </Menu.SubMenu>
    <Menu.SubMenu>
      <Menu.SubMenuTrigger>Speed</Menu.SubMenuTrigger>
      <Menu.SubMenuContent>
        <Menu.RadioGroup value={speed} onValueChange={setSpeed} label="Speed">
          <Menu.RadioItem value="0.5">0.5x</Menu.RadioItem>
          <Menu.RadioItem value="1">Normal</Menu.RadioItem>
          <Menu.RadioItem value="2">2x</Menu.RadioItem>
        </Menu.RadioGroup>
      </Menu.SubMenuContent>
    </Menu.SubMenu>
  </Menu.Content>
</Menu.Root>

HTML

<button commandfor="settings-menu">Settings</button>
<media-menu id="settings-menu">
  <media-menu-item commandfor="quality-menu">Quality</media-menu-item>
  <media-menu id="quality-menu">
    <media-menu-radio-group label="Quality">
      <media-menu-radio-item value="auto">Auto</media-menu-radio-item>
      <media-menu-radio-item value="1080p">1080p</media-menu-radio-item>
      <media-menu-radio-item value="720p">720p</media-menu-radio-item>
    </media-menu-radio-group>
  </media-menu>
  <media-menu-item commandfor="speed-menu">Speed</media-menu-item>
  <media-menu id="speed-menu">
    <media-menu-radio-group label="Speed">
      <media-menu-radio-item value="0.5">0.5x</media-menu-radio-item>
      <media-menu-radio-item value="1">Normal</media-menu-radio-item>
      <media-menu-radio-item value="2">2x</media-menu-radio-item>
    </media-menu-radio-group>
  </media-menu>
</media-menu>

Doc structure

We simplified design doc conventions in #1173. Recommendations:

  • Rename index.md to menus.md as the single design doc - anatomy, API surface, keyboard, accessibility
  • Decisions around cascading vs non-cascading handling can live in menus.md directly
  • Drop decisions.md - add decisions as they're debated during review and implementation
  • Drop architecture.md - this is an implementation plan, save for that phase
  • Drop parts.md - redundant with the API surface already in menus.md, add per-part detail inline if needed

Out of scope

  • Cascading (flyout) submenus - the player doesn't need them right now. Can be added later via Portal composition if a use case appears.
  • Context menus - needs its own design, starting with what we'd actually use them for in the player.

@sampotts
Copy link
Copy Markdown
Collaborator Author

sampotts commented Apr 1, 2026

Blockers

  1. "Flyout" and "panel" aren't established terminology. The component library ecosystem (Base UI, Radix) uses "submenu" - the distinction is how submenus render (cascading vs non-cascading), not a different component category. Introducing unfamiliar vocabulary adds friction for anyone coming from or working simultaneously with these libraries.

Happy to change naming but flyout menu is a common design term outside of Base UI circles, such as the W3C - their example refers to navigation but the principle is the same. Maybe "panel" (or pane) isn't so obvious but to me sub-menu is too ambiguous as that could apply to either design pattern, hence needing some form of differentiation - a problem that Base UI doesn't have.

  1. mode prop creates a polymorphic composition tree. The valid set of children depends on a runtime prop. Panel parts are no-ops in flyout mode, SubMenu parts are no-ops in panel mode. TypeScript can't catch this, and there's no runtime error either. It'd be cleaner if the composition itself determined the behavior rather than a prop changing which children are valid.

Sure, we could have completely different components paths for each type.

  1. Cascading (flyout) submenus may not be the right starting point for the player. The player operates in a constrained popover - cascading submenus that open as separate positioned popups don't fit well here. In-place transitions within a single container are what YouTube, Plyr, and most video player settings UIs use. It might make more sense to ground the design in non-cascading submenus rather than starting cascading first with a mode escape hatch for panels.

Yep, fully aware of the constraints of course and that's why I built Plyr the way I did but we may also want to consider offering some variance in design, otherwise skins all start looking very similar. Happy to go down that path though and add some more options later.

  1. Panel navigation model feels premature. PanelNavigationState, createCarouselTransition(), panel stack, rapid-navigation cancellation, 4 panel-specific parts - there's a lot of machinery here

Yep, I think some of it will come out in the wash as I build it. Not aiming to get a 100% waterfall implementation documented here. Just the general direction.

  1. Panel parts overlap with SubMenu semantics. Panel is effectively a SubMenuContent rendered in-place, PanelTrigger is a SubMenuTrigger, PanelBack is a SubMenuTrigger in its open state. The hierarchy is the same - the difference is rendering strategy (in-place vs cascading). Base UI's NavigationMenu handles this distinction compositionally (Portal presence vs inline Viewport) rather than with parallel parts. Worth considering the same approach here.

Will take a look.

Suggested direction

Non-cascading submenus as the default, using the existing SubMenu parts. Content acts as the viewport/transition container. SubMenuTrigger handles both forward and back navigation via visual state (data-open). Cascading (flyout) support can be additive later via Portal composition.

Transitions

Transitions and sizing are CSS-driven via custom properties and data attributes on Content:

  • --menu-width / --menu-height - measured dimensions of the active submenu, enabling smooth container resize transitions
  • --available-height - from popover positioning, for scroll containment on long lists
  • data-open - transition direction for slide animations

React

<Menu.Root>
  <Menu.Trigger>Settings</Menu.Trigger>
  <Menu.Content>
    <Menu.SubMenu>
      <Menu.SubMenuTrigger>Quality</Menu.SubMenuTrigger>
      <Menu.SubMenuContent>
        <Menu.RadioGroup value={quality} onValueChange={setQuality} label="Quality">
          <Menu.RadioItem value="auto">Auto</Menu.RadioItem>
          <Menu.RadioItem value="1080p">1080p</Menu.RadioItem>
          <Menu.RadioItem value="720p">720p</Menu.RadioItem>
        </Menu.RadioGroup>
      </Menu.SubMenuContent>
    </Menu.SubMenu>
    <Menu.SubMenu>
      <Menu.SubMenuTrigger>Speed</Menu.SubMenuTrigger>
      <Menu.SubMenuContent>
        <Menu.RadioGroup value={speed} onValueChange={setSpeed} label="Speed">
          <Menu.RadioItem value="0.5">0.5x</Menu.RadioItem>
          <Menu.RadioItem value="1">Normal</Menu.RadioItem>
          <Menu.RadioItem value="2">2x</Menu.RadioItem>
        </Menu.RadioGroup>
      </Menu.SubMenuContent>
    </Menu.SubMenu>
  </Menu.Content>
</Menu.Root>

HTML

<button commandfor="settings-menu">Settings</button>
<media-menu id="settings-menu">
  <media-menu-item commandfor="quality-menu">Quality</media-menu-item>
  <media-menu id="quality-menu">
    <media-menu-radio-group label="Quality">
      <media-menu-radio-item value="auto">Auto</media-menu-radio-item>
      <media-menu-radio-item value="1080p">1080p</media-menu-radio-item>
      <media-menu-radio-item value="720p">720p</media-menu-radio-item>
    </media-menu-radio-group>
  </media-menu>
  <media-menu-item commandfor="speed-menu">Speed</media-menu-item>
  <media-menu id="speed-menu">
    <media-menu-radio-group label="Speed">
      <media-menu-radio-item value="0.5">0.5x</media-menu-radio-item>
      <media-menu-radio-item value="1">Normal</media-menu-radio-item>
      <media-menu-radio-item value="2">2x</media-menu-radio-item>
    </media-menu-radio-group>
  </media-menu>
</media-menu>

Doc structure

We simplified design doc conventions in #1173. Recommendations:

  • Rename index.md to menus.md as the single design doc - anatomy, API surface, keyboard, accessibility
  • Decisions around cascading vs non-cascading handling can live in menus.md directly
  • Drop decisions.md - add decisions as they're debated during review and implementation
  • Drop architecture.md - this is an implementation plan, save for that phase
  • Drop parts.md - redundant with the API surface already in menus.md, add per-part detail inline if needed

Sure, this all changed after your recent PR so I'll update. Its current state was based on the previous standards we had in place.

Out of scope

  • Cascading (flyout) submenus - the player doesn't need them right now. Can be added later via Portal composition if a use case appears.
  • Context menus - needs its own design, starting with what we'd actually use them for in the player.

Context menus (as in right click) aren't covered in this design doc.

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.

3 participants