fideo-js docs

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

A reusable componentjsx
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 />;
}
Strict Mode mounts effects twice in development. That is fine here — the cleanup destroys the first instance, and mountFideo is idempotent besides. Do not skip the cleanup to work around it.

Driving it from the parent

Expose the instance with a refjsx
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.

app/components/Video.jsxjsx
'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

Composition APIvue
<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

An action is the natural fitsvelte
<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.

After a route change or an AJAX insertjs
let current = initFideo();

router.afterEach(() => {
  current.destroy();   // tear down the previous view's players
  current = initFideo();
});
Or mount as content appearsjs
const observer = new MutationObserver(() => initFideo());
observer.observe(document.body, { childList: true, subtree: true });
Fideo does not watch the DOM for you. There is no built-in 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.

A template partialhtml
<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.

controls: false0:00
The wiringjs
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.

Current posterclip one
Swapping in placejs
async function select(item) {
  await player.setSource(item.src);
  await player.play();
}
Do not combine 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.

Quartile trackingjs
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