Files
v10/.agents/skills/write-api-reference/references/demo-patterns.md
T

7.4 KiB

Demo Patterns

File structure and conventions for interactive component demos.

Directory Structure

site/src/components/docs/demos/{component}/
├── html/css/
│   ├── BasicUsage.astro     # Astro wrapper (renders HTML, imports CSS, bundles script)
│   ├── BasicUsage.html      # Markup only (no <style> or <script>)
│   ├── BasicUsage.css       # Styles
│   └── BasicUsage.ts        # Side-effect imports for custom element registration
└── react/css/
    ├── BasicUsage.tsx        # React component
    └── BasicUsage.css        # Styles

BEM Naming

Block = {framework}-{component}-{variant}, element = __{part}:

html-play-button-basic              /* HTML framework, block */
html-play-button-basic__button      /* HTML framework, element */
react-play-button-basic             /* React framework, block */
react-play-button-basic__button     /* React framework, element */

The framework prefix (html- / react-) prevents CSS leaking between HTML and React demos on the same page (both render but one is hidden).

HTML Demo Files

.astro wrapper

---
import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro';
import html from './BasicUsage.html?raw';
import './BasicUsage.css';
---
<HtmlDemo html={html} />
<script>
  import './BasicUsage.ts';
</script>

The .astro wrapper is required because only Astro <script> tags go through Vite's bundling pipeline.

.html (markup only)

<video-player class="html-mute-button-basic">
    <video
        src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4"
        autoplay
        muted
        playsinline
        loop
    ></video>
    <media-mute-button class="html-mute-button-basic__button">
        <span class="show-when-muted">Unmute</span>
        <span class="show-when-unmuted">Mute</span>
    </media-mute-button>
</video-player>
  • No <style> or <script> tags
  • Video attributes: autoplay muted playsinline loop
  • State labels use CSS class names toggled by data attributes

.css (styles)

.html-mute-button-basic {
  position: relative;
}

.html-mute-button-basic video {
  width: 100%;
}

.html-mute-button-basic__button {
  padding-block: 8px;
  position: absolute;
  bottom: 10px;
  left: 10px;
  background: rgba(255, 255, 255, 0.7);
  backdrop-filter: blur(10px);
  color: black;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 9999px;
  padding-inline: 20px;
  cursor: pointer;
}

/* State-based visibility via data attributes */
.html-mute-button-basic__button .show-when-muted { display: none; }
.html-mute-button-basic__button .show-when-unmuted { display: none; }
.html-mute-button-basic__button[data-muted] .show-when-muted { display: inline; }
.html-mute-button-basic__button:not([data-muted]) .show-when-unmuted { display: inline; }

.ts (registration imports)

import '@videojs/html/video/player';
import '@videojs/html/ui/mute-button';

Import registration for:

  • @videojs/html/video/player — always needed (registers <video-player>)
  • @videojs/html/ui/{component} — registers the component's custom element

React Demo Files

.tsx (component)

import { createPlayer, MuteButton } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';

import './BasicUsage.css';

const Player = createPlayer({ features: videoFeatures });

export default function BasicUsage() {
  return (
    <Player.Provider>
      <Player.Container className="react-mute-button-basic">
        <Video
          src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4"
          autoPlay
          muted
          playsInline
          loop
        />
        <MuteButton
          className="react-mute-button-basic__button"
          render={(props, state) => (
            <button {...props}>{state.muted ? 'Unmute' : 'Mute'}</button>
          )}
        />
      </Player.Container>
    </Player.Provider>
  );
}

Key patterns:

  • createPlayer({ features: videoFeatures }) creates the player
  • Video attributes: autoPlay muted playsInline loop (React camelCase)
  • render prop for state-based rendering: render={(props, state) => ...}
  • Spread {...props} on the rendered element for accessibility attributes

.css (styles)

Same base styling as HTML but with react- BEM prefix:

.react-mute-button-basic {
  position: relative;
}

.react-mute-button-basic video {
  width: 100%;
}

.react-mute-button-basic__button {
  padding-block: 8px;
  position: absolute;
  bottom: 10px;
  left: 10px;
  background: rgba(255, 255, 255, 0.7);
  backdrop-filter: blur(10px);
  color: black;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 9999px;
  padding-inline: 20px;
  cursor: pointer;
}

React CSS files typically don't need data-attribute selectors since the render prop handles state-based rendering. Include them only when CSS state reflection is used.

State Reflection Patterns

HTML: Data attribute selectors

Components expose state via data-* attributes. CSS toggles visibility:

/* Hide all by default */
.html-play-button-basic__button .show-when-paused { display: none; }
.html-play-button-basic__button .show-when-playing { display: none; }

/* Show based on state */
.html-play-button-basic__button[data-paused] .show-when-paused { display: inline; }
.html-play-button-basic__button:not([data-paused]) .show-when-playing { display: inline; }

For multi-value attributes (e.g., data-volume-level):

.html-mute-button-volume-levels__button .level-off,
.html-mute-button-volume-levels__button .level-low,
.html-mute-button-volume-levels__button .level-medium,
.html-mute-button-volume-levels__button .level-high {
  display: none;
}

.html-mute-button-volume-levels__button[data-volume-level="off"] .level-off { display: inline; }
.html-mute-button-volume-levels__button[data-volume-level="low"] .level-low { display: inline; }

React: Render prop

<MuteButton
  render={(props, state) => (
    <button {...props}>
      {state.volumeLevel === 'off'
        ? 'Off'
        : state.volumeLevel === 'low'
          ? 'Low'
          : state.volumeLevel === 'medium'
            ? 'Medium'
            : 'High'}
    </button>
  )}
/>

Three-state pattern (Play/Pause/Replay)

HTML uses :not() combinators to handle mutually exclusive states:

.html-play-button-basic__button[data-paused]:not([data-ended]) .show-when-paused { display: inline; }
.html-play-button-basic__button:not([data-paused]) .show-when-playing { display: inline; }
.html-play-button-basic__button[data-ended] .show-when-ended { display: inline; }

React uses nested ternary in the render prop:

render={(props, state) => (
  <button {...props}>{state.ended ? 'Replay' : state.paused ? 'Play' : 'Pause'}</button>
)}

Video Sources

  • Video: https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4
  • Poster: https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/thumbnail.jpg

Base Button Styles

All button demos share this base overlay style:

.__button {
  padding-block: 8px;
  position: absolute;
  bottom: 10px;
  left: 10px;
  background: rgba(255, 255, 255, 0.7);
  backdrop-filter: blur(10px);
  color: black;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 9999px;
  padding-inline: 20px;
  cursor: pointer;
}