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>
Element Description 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
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):
Property Default Description --ef-volume-fill-color— Filled (progress) track color. --ef-volume-height— Track and thumb height. --ef-volume-track-color— Unfilled 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}
/>
Attribute React prop Type Default Description targettargetstring""ID of the target element descendant controls resolve playback through by default.
Attribute React prop Type Default Description targettargetstring""ID of the target element this component controls or reads from.
Attribute React prop Type Default Description targettargetstring""ID of the target element this component controls or reads from.
Attribute React prop Type Default Description targettargetstring""ID of the target element this component controls or reads from.
Attribute React prop Type Default Description container-widthcontainerWidthnumber— Explicit container width in pixels, used in vertical orientation. current-time-mscurrentTimeMsnumber— Current playback time in milliseconds. duration-msdurationMsnumber— Total duration in milliseconds. fpsfpsnumber— Frame rate used for frame-quantized calculations. orientationorientationhorizontal | vertical— Layout direction. pixels-per-secondpixelsPerSecondnumber— Zoom level — pixels rendered per second of composition time. raw-scrub-time-msrawScrubTimeMsnumber | null— Unquantized scrub position in milliseconds, updated live during a drag. targettargetstring""ID of the target element this component controls or reads from. zoom-scalezoomScalenumber— Zoom multiplier applied to vertical (timeline) scrubber sizing.
Attribute React prop Type Default Description targettargetstring""ID of the target element this component controls or reads from.
Attribute React prop Type Default Description targettargetstring""ID of the target element this component controls or reads from.
Attribute React prop Type Default Description targettargetstring""ID of the target element this component controls or reads from.
Attribute React prop Type Default Description targettargetstring""ID of the target element this component controls or reads from.
Attribute React prop Type Default Description targettargetstring""ID of the target element this component controls or reads from.
Attribute React prop Type Default Description targettargetstring""ID of the target element this component controls or reads from.
Attribute React prop Type Default Description targettargetstring""ID of the target element this component controls or reads from.
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:
Part Description buttonClick target wrapping the slotted `play`/`pause` content.
Part Description buttonClick target wrapping the slotted icon/label content.
Part Description buttonClick target wrapping the slotted icon/label content.
Part Description buttonClick target — exposed directly on the `<slot>`.
Part Description buttonClick target wrapping the slotted `muted`/`unmuted` content.
Part Description buttonClick target wrapping the slotted `enter`/`exit` content.
Part Description buttonClick target wrapping the slotted `enter`/`exit` content.
ef-scrubber:
Part Description 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:
Part Description 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:
Part Description sliderThe native `<input type="range">` volume slider.
ef-volume::part(slider) { accent-color: #e53935; }
ef-resolution:
Part Description 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; }
Each toggle control renders its slotted content based on state — you supply the actual buttons/markup for each state:
ef-toggle-play:
Slot Description 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):
Slot Description (default) Your own button/element — click target for playing.
Slot Description (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):
Slot Description (default) Your own button/element — click target for toggling loop.
ef-mute:
Slot Description mutedShown while muted — click target to unmute. unmutedShown while unmuted — click target to mute.
ef-fullscreen:
Slot Description enterShown when not currently fullscreen — click target to enter fullscreen. exitShown while fullscreen — click target to exit fullscreen.
ef-pip:
Slot Description enterShown when not in Picture-in-Picture — click target to enter PiP. exitShown while in Picture-in-Picture — click target to exit PiP.
ef-scrubber:
Event Detail Description seeknumberFires when the user drags the scrubber to a new position.
ef-resolution:
Event Detail Description resolution-change{ width: number; height: number }Fires with `{ width, height }` when the resolution changes via preset selection or a custom width/height input.
ef-scrubber:
Property Default Description --ef-scrubber-background— Unfilled track color. --ef-scrubber-handle-size— Diameter of the draggable handle. --ef-scrubber-height— Track thickness. --ef-scrubber-progress-color— Filled (progress) track color.
ef-resolution:
Property Default Description --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-bg— Background color of the dropdown/inputs. --ef-resolution-border-color— Border color of the dropdown/inputs. --ef-resolution-color— Text color. --ef-resolution-font-size— Font size of the dropdown/inputs.