A tiny, framework-free video UI that wraps HTML5, YouTube, Vimeo, and Wistia behind a single set of data attributes. Responsive sources, background loops, viewport triggers, themable controls — without the heavyweight player bundle.
~12 kB gzipped. No framework runtime, no virtual DOM.
HTML5, YouTube, Vimeo, Wistia. One identical API.
Per-breakpoint sources and posters. No jank on resize.
Auto-mount via data-fideo, or drive from JS.
Drop in the CDN bundle, mark up a <video> or
<iframe> with data-fideo, and the library
auto-discovers it on load.
<!-- via CDN — styles are injected, no stylesheet needed --> <script src="https://cdn.jsdelivr.net/gh/devjtv/fideo-js@v0.7.0/dist/fideo.global.js"></script>
<video data-fideo data-fideo-src="/clip.mp4" data-fideo-poster="/poster.jpg" data-fideo-muted ></video>
import { Fideo, initFideo } from 'fideo-js'; // auto-mount every [data-fideo] on the page initFideo(); // or construct a single player const player = new Fideo('#hero', { muted: true, loop: true, sources: { desktop: '/hero.mp4', mobile: '/hero-m.mp4' }, }); await player.play();
<!-- YouTube --> <iframe data-fideo src="https://youtu.be/M7lc1UVf-VE"></iframe> <!-- Vimeo (with hash for unlisted videos) --> <iframe data-fideo src="https://vimeo.com/76979871"></iframe> <!-- Wistia --> <iframe data-fideo src="https://fast.wistia.com/embed/medias/358edhd4og"></iframe>
Each card below uses identical data attributes — only the source URL changes. Provider SDKs are lazy-loaded on demand.
Native <video> with custom shadow-DOM controls.
<video data-fideo data-fideo-src="/sea_turtle.mp4" data-fideo-poster="/poster.jpg" data-fideo-muted data-fideo-loop data-fideo-autoplay ></video>
import { Fideo } from 'fideo-js'; // Target must be a <video> element. // If it already has src="..." you can omit `sources`. new Fideo('#player', { sources: '/sea_turtle.mp4', // string shorthand for { desktop: '...' } posters: '/poster.jpg', muted: true, loop: true, autoplay: true, });
Normalized to youtube-nocookie.com.
<iframe data-fideo src="https://youtu.be/pb-j3svRQLI" data-fideo-poster="https://img.youtube.com/vi/pb-j3svRQLI/maxresdefault.jpg" data-fideo-muted data-fideo-loop data-fideo-autoplay ></iframe>
import { Fideo } from 'fideo-js'; // Target must be an <iframe>. Provider is inferred from the URL, // so you don't need `provider: 'youtube'`. If the iframe already has // src="..." you can omit `sources` entirely. new Fideo('#yt', { posters: 'https://img.youtube.com/vi/pb-j3svRQLI/maxresdefault.jpg', muted: true, loop: true, autoplay: true, });
Public, private, and unlisted URLs supported.
<iframe data-fideo src="https://vimeo.com/347119375" data-fideo-poster="/poster.jpg" data-fideo-muted data-fideo-loop data-fideo-autoplay ></iframe>
import { Fideo } from 'fideo-js'; // Target must be an <iframe>. Provider is auto-detected from the URL. // If the iframe already has src="..." you can omit `sources`. new Fideo('#vimeo', { posters: '/poster.jpg', muted: true, loop: true, autoplay: true, });
Swaps the iframe for <wistia-player>.
<iframe data-fideo src="https://fast.wistia.com/embed/medias/358edhd4og" data-fideo-muted data-fideo-loop data-fideo-autoplay ></iframe>
import { Fideo } from 'fideo-js'; // Target must be an <iframe>. Provider is auto-detected from the URL. // If the iframe already has src="..." you can omit `sources`. new Fideo('#wistia', { muted: true, loop: true, autoplay: true, });
One attribute turns any provider into a silent, looping, container-filling background —
even when the source is an iframe. Fideo computes cover-fill math for embeds that
can't use object-fit.
Use data-fideo-background-aspect-ratio to describe the source
video's shape — Fideo handles the cover math.
Specify desktop / tablet / mobile variants and Fideo picks the right asset per breakpoint. Resize your browser to watch the source swap.
Defaults: mobile ≤ 767px, tablet ≤ 1024px. Override with
data-fideo-breakpoint-*.
<video data-fideo data-fideo-src="/desktop.mp4" data-fideo-src-tablet="/tablet.mp4" data-fideo-src-mobile="/mobile.mp4" data-fideo-poster="/poster-desktop.jpg" data-fideo-poster-tablet="/poster-tablet.jpg" data-fideo-poster-mobile="/poster-mobile.jpg" data-fideo-muted data-fideo-loop ></video>
import { Fideo } from 'fideo-js'; // Target a <video> (or <iframe> for embed providers). // Responsive swap requires the `sources` object — a native // src="..." only acts as the single fallback. new Fideo('#player', { sources: { desktop: '/desktop.mp4', tablet: '/tablet.mp4', mobile: '/mobile.mp4', }, posters: { desktop: '/poster-desktop.jpg', tablet: '/poster-tablet.jpg', mobile: '/poster-mobile.jpg', }, // breakpoints: { mobile: 767, tablet: 1024 } // override defaults if needed muted: true, loop: true, });
Tie playback to an IntersectionObserver with a single attribute. Useful
for autoplaying card grids without burning CPU off-screen.
Threshold 0.5 — half the player must be visible.
<video data-fideo data-fideo-src="/clip.mp4" data-fideo-poster="/poster.jpg" data-fideo-viewport="play-pause" data-fideo-viewport-threshold="0.5" data-fideo-muted data-fideo-loop ></video>
import { Fideo } from 'fideo-js'; // Target must be a <video> (or <iframe>). // If it already has src="..." you can omit `sources`. new Fideo('#player', { sources: '/clip.mp4', posters: '/poster.jpg', viewport: 'play-pause', viewportThreshold: 0.5, muted: true, loop: true, });
User starts it, viewport pauses it when scrolled away.
<video data-fideo data-fideo-src="/clip.mp4" data-fideo-poster="/poster.jpg" data-fideo-viewport="pause" data-fideo-muted ></video>
import { Fideo } from 'fideo-js'; // Target must be a <video> (or <iframe>). // If it already has src="..." you can omit `sources`. new Fideo('#player', { sources: '/clip.mp4', posters: '/poster.jpg', viewport: 'pause', muted: true, });
Construct a Fideo instance and call play(),
pause(), seek(), setVolume(),
setMuted(), setPlaybackRate(),
setSource(), and getState(). State is also dispatched as
normalized fideo:* DOM events.
Mounted from JS without data-fideo.
player.setSource)player.getState()
Normalized fideo:* DOM events
Toggle individual controls — play, timeline, volume, settings, fullscreen — or hide everything for an ambient look.
<video data-fideo data-fideo-src="/sea_turtle.mp4" data-fideo-poster="/poster.jpg" data-fideo-show-timeline="false" data-fideo-show-volume="false" data-fideo-show-settings="false" data-fideo-show-time="false" data-fideo-muted ></video>
<video data-fideo data-fideo-src="/clip.mp4" data-fideo-poster="/poster.jpg" data-fideo-show-timeline="false" data-fideo-show-settings="false" data-fideo-muted ></video>
<video data-fideo data-fideo-src="/loop.mp4" data-fideo-controls="false" data-fideo-muted data-fideo-loop data-fideo-autoplay ></video>
<video data-fideo data-fideo-src="/elephants.mp4" data-fideo-poster="/poster.jpg" data-fideo-playback-rates="0.5, 1, 1.5, 2, 3" data-fideo-muted ></video>
Override --fideo-* variables globally, per element via
data-fideo-accent, or in JS with cssVars. Deep customisation
uses Shadow DOM ::part() selectors. v0.4.0 adds
--fideo-button-radius, --fideo-icon-size,
--fideo-thumb-size, and a settings-menu part.
/* CSS */ .fideo-frosted { --fideo-accent: #6cf2c0; --fideo-track-fill: #ffffff; --fideo-track: rgba(255,255,255,.22); --fideo-track-size: 3px; } .fideo-frosted .fideo__controls { background: rgba(15,18,28,.55); backdrop-filter: blur(22px) saturate(160%); border: 1px solid rgba(255,255,255,.10); border-radius: 14px; padding: 6px 14px 8px; bottom: 14px; box-shadow: 0 6px 24px rgba(0,0,0,.35); } .fideo-frosted .fideo__controls::part(play-button):hover { color: #6cf2c0; } <!-- HTML --> <video data-fideo data-fideo-class="fideo-frosted" data-fideo-src="/clip.mp4" data-fideo-muted></video>
/* CSS — uses new v0.4.0 vars */ .fideo-cta { --fideo-accent: #ff5e5e; --fideo-track-fill: #ff5e5e; --fideo-track-size: 4px; --fideo-button-size: 32px; --fideo-button-radius: 8px; --fideo-icon-size: 16px; --fideo-gap: 14px; } .fideo-cta .fideo__controls { background: linear-gradient(to top, rgba(0,0,0,.85), transparent); padding: 40px 16px 12px; inset: auto 0 0 0; } .fideo-cta .fideo__controls::part(play-button) { background: #ff5e5e; color: #fff; } .fideo-cta .fideo__controls::part(settings-menu) { background: #ff5e5e; border: 0; border-radius: 8px; } <!-- HTML --> <video data-fideo data-fideo-class="fideo-cta" data-fideo-src="/clip.mp4" data-fideo-muted></video>
/* CSS */ .fideo-brutalist { --fideo-accent: #c98aff; --fideo-track-fill: #c98aff; --fideo-track-size: 2px; --fideo-radius: 0; } .fideo-brutalist .fideo__controls { background: #08060d; border-top: 2px solid #c98aff; padding: 8px 14px 10px; inset: auto 0 0 0; border-radius: 0; } .fideo-brutalist .fideo__controls::part(play-button), .fideo-brutalist .fideo__controls::part(mute-button), .fideo-brutalist .fideo__controls::part(settings-button), .fideo-brutalist .fideo__controls::part(fullscreen-button) { border-radius: 0; } .fideo-brutalist .fideo__controls::part(current-time), .fideo-brutalist .fideo__controls::part(duration) { font-family: ui-monospace, monospace; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; } <!-- HTML --> <video data-fideo data-fideo-class="fideo-brutalist" data-fideo-src="/clip.mp4" data-fideo-muted></video>
/* CSS */ .fideo-neon { --fideo-accent: #00d4ff; --fideo-track-fill: #00d4ff; --fideo-track: rgba(0,212,255,.18); --fideo-track-size: 6px; } .fideo-neon .fideo__controls { background: rgba(5,10,22,.65); backdrop-filter: blur(10px); border: 1px solid rgba(0,212,255,.35); border-radius: 10px; box-shadow: 0 0 24px rgba(0,212,255,.15); padding: 6px 14px 8px; bottom: 14px; } .fideo-neon .fideo__controls::part(timeline) { filter: drop-shadow(0 0 6px rgba(0,212,255,.6)); } .fideo-neon .fideo__controls::part(play-button):hover { color: #00d4ff; filter: drop-shadow(0 0 6px rgba(0,212,255,.7)); } .fideo-neon .fideo__controls::part(current-time), .fideo-neon .fideo__controls::part(duration) { color: #00d4ff; font-variant-numeric: tabular-nums; } <!-- HTML --> <video data-fideo data-fideo-class="fideo-neon" data-fideo-src="/clip.mp4" data-fideo-muted></video>
Pass inline SVG strings for any control icon. Supported keys:
play, pause, volume, volumeLow,
muted, settings, fullscreen,
fullscreenExit.
import { Fideo } from 'fideo-js'; new Fideo('#p-icons', { sources: { desktop: '/clip.mp4' }, icons: { play: '<svg ...><polygon points="6 4 20 12 6 20"/></svg>', pause: '<svg ...><rect x="6" y="4" .../><rect x="14" .../></svg>', volume: '<svg ...>...</svg>', muted: '<svg ...>...</svg>', settings:'<svg ...><circle cx="12" cy="12" r="3"/>...</svg>', }, });