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 (it also exposes a slider CSS part if you'd rather style it directly):

PropertyDefaultDescription
--ef-volume-fill-colorFilled (progress) track color.
--ef-volume-heightTrack and thumb height.
--ef-volume-track-colorUnfilled 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}
/>
AttributeReact propTypeDefaultDescription
targettargetstring""ID of the target element descendant controls resolve playback through by default.
AttributeReact propTypeDefaultDescription
targettargetstring""ID of the target element this component controls or reads from.
AttributeReact propTypeDefaultDescription
targettargetstring""ID of the target element this component controls or reads from.
AttributeReact propTypeDefaultDescription
targettargetstring""ID of the target element this component controls or reads from.
AttributeReact propTypeDefaultDescription
container-widthcontainerWidthnumberExplicit container width in pixels, used in vertical orientation.
current-time-mscurrentTimeMsnumberCurrent playback time in milliseconds.
duration-msdurationMsnumberTotal duration in milliseconds.
fpsfpsnumberFrame rate used for frame-quantized calculations.
orientationorientationhorizontal | verticalLayout direction.
pixels-per-secondpixelsPerSecondnumberZoom level — pixels rendered per second of composition time.
raw-scrub-time-msrawScrubTimeMsnumber | nullUnquantized scrub position in milliseconds, updated live during a drag.
targettargetstring""ID of the target element this component controls or reads from.
zoom-scalezoomScalenumberZoom multiplier applied to vertical (timeline) scrubber sizing.
AttributeReact propTypeDefaultDescription
targettargetstring""ID of the target element this component controls or reads from.
AttributeReact propTypeDefaultDescription
targettargetstring""ID of the target element this component controls or reads from.
AttributeReact propTypeDefaultDescription
targettargetstring""ID of the target element this component controls or reads from.
AttributeReact propTypeDefaultDescription
targettargetstring""ID of the target element this component controls or reads from.
AttributeReact propTypeDefaultDescription
targettargetstring""ID of the target element this component controls or reads from.
AttributeReact propTypeDefaultDescription
targettargetstring""ID of the target element this component controls or reads from.
AttributeReact propTypeDefaultDescription
targettargetstring""ID of the target element this component controls or reads from.

CSS Parts

ef-controls renders to light DOM (it's a pure context provider around your own slotted buttons), so it has no ::part() surface of its own — style your slotted <button> elements directly. The toggle/button controls each expose a single button part wrapping their slotted content:

PartDescription
buttonClick target wrapping the slotted `play`/`pause` content.
PartDescription
buttonClick target wrapping the slotted icon/label content.
PartDescription
buttonClick target wrapping the slotted icon/label content.
PartDescription
buttonClick target — exposed directly on the `<slot>`.
PartDescription
buttonClick target wrapping the slotted `muted`/`unmuted` content.
PartDescription
buttonClick target wrapping the slotted `enter`/`exit` content.
PartDescription
buttonClick target wrapping the slotted `enter`/`exit` content.

ef-scrubber:

PartDescription
handleDraggable circular knob.
playheadVertical line at the current time position.
progressFilled portion indicating current position.
scrubberOuter track container.
ef-scrubber::part(scrubber) { background: #1a1a2e; border-radius: 4px; }
ef-scrubber::part(progress) { background: #e94560; }
ef-scrubber::part(handle)   { background: #e94560; width: 14px; height: 14px; }

ef-time-display:

PartDescription
timeSpan containing the formatted `current / duration` text.

ef-volume — an alternative to the CSS custom properties above, if you'd rather target the track directly:

PartDescription
sliderThe native `<input type="range">` volume slider.
ef-volume::part(slider) { accent-color: #e53935; }

ef-resolution:

PartDescription
height-inputCustom height numeric input (custom mode only).
selectPreset dropdown.
separatorThe "×" between width and height inputs.
width-inputCustom width numeric input (custom mode only).
ef-resolution::part(select)       { background: #1a1a2e; border-color: rgba(255,255,255,0.2); }
ef-resolution::part(width-input),
ef-resolution::part(height-input) { background: #1a1a2e; border-color: rgba(255,255,255,0.2); }
ef-resolution::part(separator)    { opacity: 0.5; }

Slots

Each toggle control renders its slotted content based on state — you supply the actual buttons/markup for each state:

ef-toggle-play:

SlotDescription
pauseShown while playing — click target to pause.
playShown while paused — click target to start playback.

ef-play / ef-pause (unnamed default slot — supply a single button):

SlotDescription
(default)Your own button/element — click target for playing.
SlotDescription
(default)Your own button/element — click target for pausing.

ef-toggle-loop (unnamed default slot — supply a single button; use [data-loop-on]-style CSS on the host to reflect state, as in the demo above):

SlotDescription
(default)Your own button/element — click target for toggling loop.

ef-mute:

SlotDescription
mutedShown while muted — click target to unmute.
unmutedShown while unmuted — click target to mute.

ef-fullscreen:

SlotDescription
enterShown when not currently fullscreen — click target to enter fullscreen.
exitShown while fullscreen — click target to exit fullscreen.

ef-pip:

SlotDescription
enterShown when not in Picture-in-Picture — click target to enter PiP.
exitShown while in Picture-in-Picture — click target to exit PiP.

Events

ef-scrubber:

EventDetailDescription
seeknumberFires when the user drags the scrubber to a new position.

ef-resolution:

EventDetailDescription
resolution-change{ width: number; height: number }Fires with `{ width, height }` when the resolution changes via preset selection or a custom width/height input.

CSS Custom Properties

ef-scrubber:

PropertyDefaultDescription
--ef-scrubber-backgroundUnfilled track color.
--ef-scrubber-handle-sizeDiameter of the draggable handle.
--ef-scrubber-heightTrack thickness.
--ef-scrubber-progress-colorFilled (progress) track color.

ef-resolution:

PropertyDefaultDescription
--ef-color-surfacergba(0, 0, 0, 0.6)Fallback background color source for `--ef-resolution-bg` (not currently part of the shared theme token set).
--ef-resolution-bgBackground color of the dropdown/inputs.
--ef-resolution-border-colorBorder color of the dropdown/inputs.
--ef-resolution-colorText color.
--ef-resolution-font-sizeFont size of the dropdown/inputs.