Controls

Playback control elements. Compose them freely against any timegroup.

Controls are unstyled building blocks — each one wires a single playback concern to a target timegroup. Use ef-controls as a context provider so child controls share a target without individual target attributes, or wire each element directly with target.

<ef-timegroup id="demo" mode="fixed" duration="5s" loop class="w-[1920px] h-[1080px] bg-slate-900 flex items-center justify-center">
  <style>
    @keyframes pulse {
      0%, 100% { transform: scale(1); }
      50% { transform: scale(1.08); }
    }
    .demo-text {
      animation: pulse 2.4s ease-in-out infinite;
      display: inline-block;
    }
  </style>
  <ef-text class="demo-text text-white text-7xl font-bold">Hello</ef-text>
</ef-timegroup>

<ef-controls target="demo">
  <div class="ctrl-grid">
    <span class="ctrl-label">ef-toggle-play</span>
    <div class="ctrl-cell">
      <ef-toggle-play>
        <button slot="play" class="ctrl-btn">▶ Play</button>
        <button slot="pause" class="ctrl-btn">⏸ Pause</button>
      </ef-toggle-play>
    </div>

    <span class="ctrl-label">ef-play</span>
    <div class="ctrl-cell ctrl-pair">
      <ef-play><button class="ctrl-btn">▶ Play</button></ef-play>
    </div>

    <span class="ctrl-label">ef-pause</span>
    <div class="ctrl-cell ctrl-pair">
      <ef-pause><button class="ctrl-btn">⏸ Pause</button></ef-pause>
    </div>

    <span class="ctrl-label">ef-scrubber</span>
    <div class="ctrl-cell">
      <ef-scrubber class="ctrl-scrubber"></ef-scrubber>
    </div>

    <span class="ctrl-label">ef-time-display</span>
    <div class="ctrl-cell">
      <ef-time-display class="ctrl-time"></ef-time-display>
    </div>

    <span class="ctrl-label">ef-toggle-loop</span>
    <div class="ctrl-cell">
      <ef-toggle-loop data-loop-on onclick="this.toggleAttribute('data-loop-on')">
        <button class="ctrl-btn ctrl-loop-btn" title="Toggle loop">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
            <polyline points="17 1 21 5 17 9"></polyline>
            <path d="M3 11V9a4 4 0 0 1 4-4h14"></path>
            <polyline points="7 23 3 19 7 15"></polyline>
            <path d="M21 13v2a4 4 0 0 1-4 4H3"></path>
          </svg>
          Loop
        </button>
      </ef-toggle-loop>
    </div>

    <span class="ctrl-label">ef-volume</span>
    <div class="ctrl-cell">
      <ef-volume class="ctrl-volume"></ef-volume>
    </div>

    <span class="ctrl-label">ef-mute</span>
    <div class="ctrl-cell">
      <ef-mute>
        <button slot="unmuted" class="ctrl-btn">🔊 Mute</button>
        <button slot="muted" class="ctrl-btn">🔇 Unmute</button>
      </ef-mute>
    </div>

    <span class="ctrl-label">ef-fullscreen</span>
    <div class="ctrl-cell">
      <ef-fullscreen target="demo">
        <button slot="enter" class="ctrl-btn">⛶ Fullscreen</button>
        <button slot="exit" class="ctrl-btn">✕ Exit</button>
      </ef-fullscreen>
    </div>

    <span class="ctrl-label">ef-pip</span>
    <div class="ctrl-cell">
      <ef-pip target="demo">
        <button slot="enter" class="ctrl-btn">⧉ Picture in Picture</button>
        <button slot="exit" class="ctrl-btn">✕ Exit PiP</button>
      </ef-pip>
    </div>

    <span class="ctrl-label">ef-resolution</span>
    <div class="ctrl-cell ctrl-resolution">
      <ef-resolution target="demo"></ef-resolution>
    </div>
  </div>
</ef-controls>

<style>
  .ctrl-grid {
    display: grid;
    grid-template-columns: max-content 1fr;
    grid-auto-rows: 44px;
    align-items: center;
    border-top: 1px solid var(--border-subtle);
  }
  .ctrl-label {
    font-size: 11px;
    font-family: monospace;
    color: var(--chrome-fg-muted);
    white-space: nowrap;
    padding: 0 12px 0 16px;
    height: 100%;
    display: flex;
    align-items: center;
    border-bottom: 1px solid var(--border-subtle);
  }
  .ctrl-cell {
    display: flex;
    align-items: center;
    padding: 0 16px 0 0;
    height: 100%;
    border-bottom: 1px solid var(--border-subtle);
  }
  .ctrl-pair {
    gap: 8px;
  }
  .ctrl-btn {
    cursor: pointer;
    padding: 4px 12px;
    font-size: 13px;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--chrome-fg);
    background: transparent;
    border: 1px solid var(--border-subtle);
    border-radius: 4px;
  }
  .ctrl-btn:hover {
    border-color: var(--chrome-fg-muted);
  }
  .ctrl-loop-btn {
    color: var(--chrome-fg-muted);
    border-style: dashed;
    transition: background 0.15s, color 0.15s, border-color 0.15s, border-style 0.15s;
  }
  ef-toggle-loop[data-loop-on] .ctrl-loop-btn {
    background: var(--chrome-fg);
    color: var(--chrome-bg);
    border-color: var(--chrome-fg);
    border-style: solid;
  }
  .ctrl-scrubber {
    flex: 1;
    --ef-scrubber-background: var(--chrome-fg-muted);
    --ef-scrubber-progress-color: var(--chrome-fg);
  }
  .ctrl-time {
    font-variant-numeric: tabular-nums;
    color: var(--chrome-fg);
  }
  .ctrl-volume {
    flex: 1;
    max-width: 180px;
    accent-color: var(--chrome-fg);
  }
  .ctrl-resolution {
    height: auto;
    min-height: 44px;
    padding-top: 6px;
    padding-bottom: 6px;
  }
  ef-resolution select,
  ef-resolution input {
    background: transparent;
    color: var(--chrome-fg);
    border: 1px solid var(--border-subtle);
    border-radius: 4px;
    padding: 3px 8px;
    font-size: 13px;
  }
</style>

Elements

ElementDescription
ef-controlsContext provider — wraps controls and connects them to a target timegroup
ef-toggle-playSingle button that swaps between play and pause slots
ef-playPlay-only button — hides itself when already playing
ef-pausePause-only button — hides itself when already paused
ef-scrubberSeek slider — drag to scrub through the composition
ef-time-displayLive currentTime / duration readout
ef-toggle-loopFlips the loop property on click — bring your own UI
ef-volumeRange slider (0–1) controlling playback volume; auto-unmutes when raised from zero. Customisable via CSS custom properties.
ef-muteToggle button for muting/unmuting — unmuted and muted slots
ef-fullscreenEnters/exits browser fullscreen on a target element — enter and exit slots
ef-pipFloats a video or canvas in the browser's native Picture-in-Picture window — enter and exit slots
ef-resolutionPreset dropdown and custom inputs for setting a timegroup's output dimensions

Customising ef-volume

ef-volume exposes CSS custom properties for styling the track without touching shadow DOM:

PropertyDefaultDescription
--ef-volume-height4pxTrack and thumb height
--ef-volume-track-colorrgba(255,255,255,0.2)Unfilled track color
--ef-volume-fill-colorwhiteFilled (progress) track color
<ef-volume style="
  --ef-volume-height: 3px;
  --ef-volume-track-color: rgba(255,255,255,0.15);
  --ef-volume-fill-color: #e53935;
"></ef-volume>

In React:

<Volume
  className="w-16"
  style={{
    "--ef-volume-height": "3px",
    "--ef-volume-track-color": "rgba(255,255,255,0.15)",
    "--ef-volume-fill-color": "var(--brand-red)",
  } as React.CSSProperties}
/>