Scripting
Framework recipes
Fideo is framework-agnostic: it takes an element and gives back an instance. The only rule that matters is
mount on attach, destroy() on detach.
React
import { useEffect, useRef } from 'react'; import { mountFideo } from 'fideo-js'; export function Video({ src, poster, ...options }) { const ref = useRef(null); useEffect(() => { const player = mountFideo(ref.current, options); return () => player.destroy(); // Options are read at mount, so remount when they change. }, [JSON.stringify(options)]); return <video ref={ref} src={src} poster={poster} playsInline />; }
mountFideo is idempotent besides. Do not skip the cleanup to work around
it.
Driving it from the parent
export const Video = forwardRef(({ src, ...options }, ref) => { const elRef = useRef(null); const playerRef = useRef(null); useEffect(() => { playerRef.current = mountFideo(elRef.current, options); return () => playerRef.current?.destroy(); }, []); useImperativeHandle(ref, () => ({ play: () => playerRef.current?.play(), pause: () => playerRef.current?.pause(), seek: (t) => playerRef.current?.seek(t), })); return <video ref={elRef} src={src} playsInline />; });
Next.js and other SSR frameworks
Fideo touches the DOM at import time only behind a typeof window guard, so importing it on the
server is safe. Mounting is not — keep it inside an effect, in a client component.
'use client'; import { useEffect, useRef } from 'react'; import { mountFideo } from 'fideo-js'; // No CSS import needed — mounting injects the wrapper styles, // and nothing the stylesheet targets exists before that.
Vue 3
<script setup> import { onMounted, onBeforeUnmount, ref } from 'vue'; import { mountFideo } from 'fideo-js'; const el = ref(null); let player = null; onMounted(() => { player = mountFideo(el.value, { muted: true, loop: true }); }); onBeforeUnmount(() => player?.destroy()); </script> <template> <video ref="el" src="/media/clip.mp4" playsinline></video> </template>
Svelte
<script> import { mountFideo } from 'fideo-js'; function fideo(node, options = {}) { const player = mountFideo(node, options); return { destroy: () => player.destroy() }; } </script> <video use:fideo={{ muted: true }} src="/media/clip.mp4" playsinline></video>
Single-page apps and injected content
initFideo() is safe to call repeatedly — mounted elements are tracked in a
WeakMap, so a second pass skips them. Re-run it after new markup lands.
let current = initFideo(); router.afterEach(() => { current.destroy(); // tear down the previous view's players current = initFideo(); });
const observer = new MutationObserver(() => initFideo()); observer.observe(document.body, { childList: true, subtree: true });
MutationObserver —
re-running initFideo() is the intended pattern, and it is cheap because already-mounted elements
are skipped.
CMS templates
Where the page author is not writing JavaScript, expose fields that map to data attributes and let auto-init do the rest. This is the case data attributes exist for.
<video data-fideo data-fideo-src="{{ video_url }}" data-fideo-poster="{{ poster_url }}" data-fideo-autoplay="{{ autoplay ? 'true' : 'false' }}" data-fideo-accent="{{ brand_colour }}" playsinline ></video>
An empty or malformed value falls back to the default instead of throwing, so a half-filled field cannot break the page.
Your own player UI
Turn the control bar off and drive the instance yourself. Everything below is live.
const player = mountFideo(el, { controls: false, muted: true }); toggle.addEventListener('click', () => { const { paused } = player.getState(); paused ? player.play() : player.pause(); }); player.element.addEventListener('fideo:timeupdate', (event) => { readout.textContent = format(event.detail.state.currentTime); });
A playlist with one player
setSource() swaps media in place, so a playlist does not need a player per item.
async function select(item) { await player.setSource(item.src); await player.play(); }
setSource() with responsive sources. A resize past a
breakpoint reapplies the configured source and undoes your swap. Pick one approach per player.
Analytics
Because the events bubble, one delegated listener covers every player on the page.
const seen = new WeakMap(); document.addEventListener('fideo:timeupdate', (event) => { const { state } = event.detail; if (!state.duration) return; const quartile = Math.floor((state.currentTime / state.duration) * 4); if (seen.get(event.target) === quartile) return; seen.set(event.target, quartile); track('video_progress', { id: event.target.id, quartile }); });
Integration checklist
- Always
destroy()on unmount. It is the one thing a component must not skip. - Options are read at mount. Changing them means destroying and mounting again.
- Never render the element inside a
.fideowrapper of your own — Fideo adopts it and removes it on teardown. - Catch
play(). Autoplay policy rejects it far more often than you would expect. - Guard mounting against SSR by keeping it in an effect or lifecycle hook.