~12 kB · zero dependencies · v0.6.0

One video API for every provider.

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.

Tiny + fast

~12 kB gzipped. No framework runtime, no virtual DOM.

Multi-provider

HTML5, YouTube, Vimeo, Wistia. One identical API.

Responsive

Per-breakpoint sources and posters. No jank on resize.

Drop-in

Auto-mount via data-fideo, or drive from JS.

Quick start

From plain markup to polished player.

Drop in the CDN bundle, mark up a <video> or <iframe> with data-fideo, and the library auto-discovers it on load.

Installhtml
<!-- 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>
Mark up a playerhtml
<video
  data-fideo
  data-fideo-src="/clip.mp4"
  data-fideo-poster="/poster.jpg"
  data-fideo-muted
></video>
Programmatic initts
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 / Vimeo / Wistiahtml
<!-- 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>
Provider gallery

Same markup. Four different providers.

Each card below uses identical data attributes — only the source URL changes. Provider SDKs are lazy-loaded on demand.

HTML5 video

Native <video> with custom shadow-DOM controls.

native
<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,
});

YouTube

Normalized to youtube-nocookie.com.

youtube
<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,
});

Vimeo

Public, private, and unlisted URLs supported.

vimeo
<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,
});

Wistia

Swaps the iframe for <wistia-player>.

wistia
<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,
});
Background mode

Hero videos that just behave.

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.

Any aspect ratio

Use data-fideo-background-aspect-ratio to describe the source video's shape — Fideo handles the cover math.

background-aspect-ratio
16:9
9:16 · YT Short
1:1
4:3
Responsive sources

Different videos. Different posters. Same player.

Specify desktop / tablet / mobile variants and Fideo picks the right asset per breakpoint. Resize your browser to watch the source swap.

Breakpoint-aware sources & posters

Defaults: mobile ≤ 767px, tablet ≤ 1024px. Override with data-fideo-breakpoint-*.

desktop
<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,
});
Viewport playback

Plays when seen. Pauses when not.

Tie playback to an IntersectionObserver with a single attribute. Useful for autoplaying card grids without burning CPU off-screen.

Play-pause on scroll

Threshold 0.5 — half the player must be visible.

"play-pause"
<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,
});

Pause-only on exit

User starts it, viewport pauses it when scrolled away.

"pause"
<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,
});
JavaScript API

Drive the player from anywhere.

Construct a Fideo instance and call play(), pause(), seek(), setVolume(), setMuted(), setPlaybackRate(), setSource(), and getState(). State is also dispatched as normalized fideo:* DOM events.

Live player

Mounted from JS without data-fideo.

Playback
Playback rate
Swap source (player.setSource)

Live state

player.getState()

paused
currentTime
duration
volume
muted
playbackRate

Event stream

Normalized fideo:* DOM events

Control configurations

Show only what you need.

Toggle individual controls — play, timeline, volume, settings, fullscreen — or hide everything for an ambient look.

Minimal

play + fullscreen
<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>

Ambient

no timeline
<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>

No controls

data-fideo-controls="false"
<video
  data-fideo
  data-fideo-src="/loop.mp4"
  data-fideo-controls="false"
  data-fideo-muted
  data-fideo-loop
  data-fideo-autoplay
></video>

Full + rates

custom speeds
<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>
Theming

Theme every pixel.

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.

Frosted glass

backdrop-filter
/* 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>

CTA · filled play

::part(play-button)
/* 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>

Brutalist · mono

sharp + uppercase
/* 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>

Neon

heavy ::part()
/* 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>
Custom icons

Bring your own SVGs.

Pass inline SVG strings for any control icon. Supported keys: play, pause, volume, volumeLow, muted, settings, fullscreen, fullscreenExit.

Lucide-style icons

new Fideo({ icons })
How it's wired upts
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>',
  },
});