
Security News
AI Agent Lands PRs in Major OSS Projects, Targets Maintainers via Cold Outreach
An AI agent is merging PRs into major OSS projects and cold-emailing maintainers to drum up more work.
react-lite-youtube-embed
Advanced tools
A private by default, faster and cleaner YouTube embed component for React applications
Private, performant YouTube embeds for React. Under 5KB gzipped.
Interactive demo with all features and code examples ⢠Updated with each release
YouTube's standard iframe embed can add over 500KB to your page and make dozens of network requests before the user even clicks play. This component fixes that:
The result? Faster page loads, better privacy, and a superior user experience.
npm install react-lite-youtube-embed
import LiteYouTubeEmbed from 'react-lite-youtube-embed';
import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css';
export default function App() {
return (
<LiteYouTubeEmbed
id="dQw4w9WgXcQ"
title="Rick Astley - Never Gonna Give You Up"
/>
);
}
That's it. You now have a performant, private YouTube embed.
Privacy-Enhanced Mode is the default. Videos load from youtube-nocookie.com, blocking YouTube cookies and tracking until the user explicitly clicks play.
// Default: Privacy-Enhanced Mode (youtube-nocookie.com)
<LiteYouTubeEmbed id="VIDEO_ID" title="Video Title" />
// Opt into standard YouTube (with cookies)
<LiteYouTubeEmbed id="VIDEO_ID" title="Video Title" cookie={true} />
Enable lazy loading for images to defer offscreen thumbnails and boost Lighthouse scores:
<LiteYouTubeEmbed
id="VIDEO_ID"
title="Video Title"
lazyLoad={true}
/>
Impact: Defers loading offscreen images, reduces bandwidth, improves mobile performance.
Help search engines discover your videos with structured data:
<LiteYouTubeEmbed
id="VIDEO_ID"
title="Video Title"
seo={{
name: "Full Video Title",
description: "Video description for search engines",
uploadDate: "2024-01-15T08:00:00Z",
duration: "PT3M33S"
}}
/>
Includes:
Fetch metadata automatically:
./scripts/fetch-youtube-metadata.sh VIDEO_ID --format react
React to player state changes, playback controls, quality, and errors. All core events are fully tested and verified working!
import LiteYouTubeEmbed, { PlayerState, PlayerError } from 'react-lite-youtube-embed';
<LiteYouTubeEmbed
id="VIDEO_ID"
title="Video Title"
enableJsApi
// Simple handlers (ā
All verified)
onPlay={() => console.log('Started')}
onPause={() => console.log('Paused')}
onEnd={() => console.log('Finished')}
// Advanced handlers (ā
All verified)
onStateChange={(e) => console.log('State:', e.state)}
onPlaybackRateChange={(rate) => console.log('Speed:', rate)}
onPlaybackQualityChange={(quality) => console.log('Quality:', quality)}
onError={(code) => console.error('Error:', code)}
/>
Control the player via YouTube's iframe API using refs:
function VideoPlayer() {
const playerRef = useRef(null);
const [isReady, setIsReady] = useState(false);
const handlePause = () => {
playerRef.current?.contentWindow?.postMessage(
'{"event":"command","func":"pauseVideo"}',
'*'
);
};
return (
<>
<LiteYouTubeEmbed
id="VIDEO_ID"
title="Video Title"
ref={playerRef}
enableJsApi
onIframeAdded={() => setIsReady(true)}
/>
{isReady && <button onClick={handlePause}>Pause</button>}
</>
);
}
ā Full Control Documentation
npm install react-lite-youtube-embed
yarn add react-lite-youtube-embed
npm install @ibrahimcesar/react-lite-youtube-embed
See GITHUB_PACKAGES.md for authentication details.
| Prop | Type | Description |
|---|---|---|
| id | string | YouTube video or playlist ID |
| title | string | Video title for iframe (accessibility requirement) |
| Prop | Type | Default | Description |
|---|---|---|---|
| cookie | boolean | false | Use standard YouTube (true) or Privacy-Enhanced Mode (false) |
| lazyLoad | boolean | false | Enable native lazy loading for thumbnails |
| poster | string | "hqdefault" | Thumbnail quality: "default", "mqdefault", "hqdefault", "sddefault", "maxresdefault" |
| params | string | "" | Additional URL parameters (e.g., "start=90&end=120") |
| enableJsApi | boolean | false | Enable iframe API for programmatic control |
| playlist | boolean | false | Set to true if ID is a playlist |
enableJsApi={true})| Prop | Type | Description |
|---|---|---|
| onReady | (event) => void | Player is ready to receive commands |
| onPlay | () => void | Video started playing |
| onPause | () => void | Video was paused |
| onEnd | () => void | Video finished playing |
| onBuffering | () => void | Video is buffering |
| onStateChange | (event) => void | Player state changed |
| onError | (code) => void | Player encountered an error |
| onPlaybackRateChange | (rate) => void | Playback speed changed |
| onPlaybackQualityChange | (quality) => void | Video quality changed |
| Prop | Type | Default | Description |
|---|---|---|---|
| adNetwork | boolean | false | Preconnect to Google's ad network |
| alwaysLoadIframe | boolean | false | Load iframe immediately (not recommended) |
| announce | string | "Watch" | Screen reader announcement text |
| aspectHeight | number | 9 | Custom aspect ratio height |
| aspectWidth | number | 16 | Custom aspect ratio width |
| autoplay | boolean | false | Autoplay video (requires muted={true}) |
| focusOnLoad | boolean | false | Focus iframe when loaded |
| muted | boolean | false | Mute video audio |
| noscriptFallback | boolean | true | Include noscript tag with YouTube link |
| onIframeAdded | () => void | - | Callback when iframe loads (use for ref availability) |
| playlistCoverId | string | - | Video ID for playlist cover image |
| referrerPolicy | string | "strict-origin-when-cross-origin" | Iframe referrer policy |
| seo | VideoSEO | - | SEO metadata object |
| stopOnEnd | boolean | false | Stop video when it ends to prevent related videos |
| style | object | {} | Custom container styles |
| thumbnail | string | - | Custom thumbnail image URL |
| webp | boolean | false | Use WebP format for thumbnails |
| Prop | Type | Default | Description |
|---|---|---|---|
| wrapperClass | string | "yt-lite" | Main wrapper class |
| playerClass | string | "lty-playbtn" | Play button class |
| iframeClass | string | "" | Iframe element class |
| activeClass | string | "lyt-activated" | Class when activated |
| containerElement | string | "article" | HTML element for container |
| Prop | Replacement | Note |
|---|---|---|
| noCookie | Use cookie prop | Inverted logic for clarity |
| rel | Use resourceHint | Conflicted with YouTube's rel parameter |
ā See all props with examples in the demo
import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css';
For Next.js, Remix, or other frameworks, copy the CSS to your global stylesheet. See CSS source
Use CSS-in-JS or pass custom class names:
<LiteYouTubeEmbed
id="VIDEO_ID"
title="Video Title"
wrapperClass="my-custom-wrapper"
playerClass="my-custom-button"
activeClass="video-playing"
/>
Automatically return to thumbnail when the video ends:
<LiteYouTubeEmbed
id="VIDEO_ID"
title="Video Title"
enableJsApi
stopOnEnd={true}
params="rel=0"
/>
function VideoGallery() {
return videos.map(video => (
<LiteYouTubeEmbed
key={video.id}
id={video.id}
title={video.title}
lazyLoad
onPlay={() => analytics.track('video_play', { id: video.id })}
onEnd={() => analytics.track('video_complete', { id: video.id })}
/>
));
}
function Playlist() {
const videos = ['video1', 'video2', 'video3'];
const [currentIndex, setCurrentIndex] = useState(0);
return (
<LiteYouTubeEmbed
id={videos[currentIndex]}
title={`Video ${currentIndex + 1}`}
enableJsApi
onEnd={() => {
if (currentIndex < videos.length - 1) {
setCurrentIndex(currentIndex + 1);
}
}}
/>
);
}
function CustomPlayer() {
const playerRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
const togglePlayPause = () => {
const command = isPlaying ? 'pauseVideo' : 'playVideo';
playerRef.current?.contentWindow?.postMessage(
`{"event":"command","func":"${command}"}`,
'*'
);
};
return (
<>
<LiteYouTubeEmbed
id="VIDEO_ID"
title="Video Title"
ref={playerRef}
enableJsApi
alwaysLoadIframe
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
/>
<button onClick={togglePlayPause}>
{isPlaying ? 'Pause' : 'Play'}
</button>
</>
);
}
Using Next.js 13+ App Router or any server-side rendering framework? See the SSR Guide for:
Full TypeScript support is included. Import types as needed:
import LiteYouTubeEmbed, {
PlayerState,
PlayerError,
VideoSEO,
PlayerReadyEvent,
PlayerStateChangeEvent
} from 'react-lite-youtube-embed';
Improve your video discoverability in search engines with structured data and fallback links.
By default, search engine crawlers cannot discover videos embedded with lite embeds because:
This component now supports JSON-LD structured data and noscript fallbacks to solve these issues.
<LiteYouTubeEmbed
id="L2vS_050c-M"
title="What's new in Material Design"
seo={{
name: "What's new in Material Design for the web",
description: "Learn about the latest Material Design updates presented at Chrome Dev Summit 2019",
uploadDate: "2019-11-11T08:00:00Z",
duration: "PT15M33S"
}}
/>
This generates:
There are several ways to get complete video metadata for SEO:
The fastest way is to visit the YouTube video page and get the info directly:
https://www.youtube.com/watch?v=VIDEO_IDPT4M23S (4 minutes 23 seconds)PT1H30M45S (1 hour 30 minutes 45 seconds)PT15M (15 minutes)YYYY-MM-DDTHH:MM:SSZ (the Z indicates UTC timezone)2018-12-05T08:00:00Z2025-06-05T08:00:00Z00:00:00Z or any time if you don't know the exact upload time.Use the included script to fetch title and thumbnail:
# Make the script executable (first time only)
chmod +x scripts/fetch-youtube-metadata.sh
# Fetch metadata in JSON format
./scripts/fetch-youtube-metadata.sh dQw4w9WgXcQ
# Get ready-to-use React component code
./scripts/fetch-youtube-metadata.sh dQw4w9WgXcQ --format react
Note: This script uses YouTube's oEmbed API which only provides basic info (title, author, thumbnail). You'll need to add uploadDate and duration manually.
Requirements: curl and jq must be installed.
For complete automation, use YouTube's official API:
curl "https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=VIDEO_ID&key=YOUR_API_KEY"
snippet.publishedAt ā uploadDatecontentDetails.duration ā duration (already in ISO 8601 format!)snippet.description ā descriptionAPI Limits: Free tier provides 10,000 quota units/day (sufficient for most use cases)
interface VideoSEO {
name?: string; // Video title (falls back to title prop)
description?: string; // Video description (50-160 chars recommended)
uploadDate?: string; // ISO 8601 date (e.g., "2024-01-15T08:00:00Z")
duration?: string; // ISO 8601 duration (e.g., "PT3M33S")
thumbnailUrl?: string; // Custom thumbnail (auto-generated if omitted)
contentUrl?: string; // YouTube watch URL (auto-generated)
embedUrl?: string; // Embed URL (auto-generated)
}
ISO 8601 duration format: PT#H#M#S
"PT3M33S" - 3 minutes 33 seconds"PT15M" - 15 minutes"PT1H30M" - 1 hour 30 minutes"PT2H15M30S" - 2 hours 15 minutes 30 secondsTest your structured data:
Get real-time notifications when the YouTube player changes state, encounters errors, or when users interact with playback controls.
CRITICAL: Events require both of these:
enableJsApi={true} - Enables YouTube's JavaScript APIref={yourRef} - A React ref MUST be passed to the component (used to communicate with YouTube's iframe)Without a ref, events will NOT work!
This event system relies on YouTube's internal postMessage API, which is not officially documented by Google and may change at any time without prior notice. While we strive to keep the implementation up-to-date, YouTube could modify their iframe communication protocol in future updates, potentially breaking event functionality.
Recommendations:
| Event | Status | Notes |
|---|---|---|
onIframeAdded | ā Verified Working | Fires when iframe is added to DOM |
onReady | ā Verified Working | Fires when YouTube player initializes |
onStateChange | ā Verified Working | Fires on all state changes |
onPlay | ā Verified Working | Convenience wrapper for PLAYING state |
onPause | ā Verified Working | Convenience wrapper for PAUSED state |
onEnd | ā Verified Working | Convenience wrapper for ENDED state |
onBuffering | ā Verified Working | Convenience wrapper for BUFFERING state |
onPlaybackRateChange | ā Verified Working | Fires when speed changes (use āļø settings) |
onPlaybackQualityChange | ā Verified Working | Fires when quality changes (use āļø settings) |
onError | ā ļø Untested | Should work but not confirmed with invalid video |
Technical Note: YouTube sends state changes, playback rate, and quality changes via infoDelivery postMessage events. This library handles this automatically.
import { useRef } from 'react';
import LiteYouTubeEmbed, { PlayerState, PlayerError } from 'react-lite-youtube-embed';
function App() {
const ytRef = useRef(null); // ā ļø REQUIRED for events to work!
return (
<LiteYouTubeEmbed
id="dQw4w9WgXcQ"
title="Rick Astley - Never Gonna Give You Up"
ref={ytRef} // ā ļø CRITICAL: Must pass ref
enableJsApi // ā ļø REQUIRED for events
// Simple convenience handlers
onPlay={() => console.log('Video started playing')}
onPause={() => console.log('Video paused')}
onEnd={() => console.log('Video ended')}
// Advanced state change handler
onStateChange={(event) => {
console.log('State:', event.state);
console.log('Current time:', event.currentTime);
}}
// Advanced playback handlers
onPlaybackRateChange={(rate) => console.log('Speed:', rate)}
onPlaybackQualityChange={(quality) => console.log('Quality:', quality)}
// Error handling
onError={(errorCode) => {
if (errorCode === PlayerError.VIDEO_NOT_FOUND) {
alert('Video not available');
}
}}
/>
);
}
onReady(event: PlayerReadyEvent)
Fires when the player is loaded and ready to receive commands.
onReady={(event) => {
console.log(`Player ready for: ${event.videoId}`);
}}
onStateChange(event: PlayerStateChangeEvent)
Fires whenever the player's state changes.
onStateChange={(event) => {
switch (event.state) {
case PlayerState.PLAYING:
console.log('Playing at', event.currentTime, 'seconds');
break;
case PlayerState.PAUSED:
console.log('Paused');
break;
case PlayerState.ENDED:
console.log('Video finished');
break;
}
}}
PlayerState values:
PlayerState.UNSTARTED (-1)PlayerState.ENDED (0)PlayerState.PLAYING (1)PlayerState.PAUSED (2)PlayerState.BUFFERING (3)PlayerState.CUED (5)onError(errorCode: PlayerError)
Fires when the player encounters an error.
onError={(code) => {
switch (code) {
case PlayerError.INVALID_PARAM:
console.error('Invalid video parameter');
break;
case PlayerError.VIDEO_NOT_FOUND:
console.error('Video not found or removed');
break;
case PlayerError.NOT_EMBEDDABLE:
console.error('Video cannot be embedded');
break;
}
}}
PlayerError codes:
PlayerError.INVALID_PARAM (2)PlayerError.HTML5_ERROR (5)PlayerError.VIDEO_NOT_FOUND (100)PlayerError.NOT_EMBEDDABLE (101)PlayerError.NOT_EMBEDDABLE_DISGUISED (150)Simple wrappers for common use cases:
<LiteYouTubeEmbed
id="VIDEO_ID"
title="Video Title"
enableJsApi
onPlay={() => analytics.track('video_play')}
onPause={() => analytics.track('video_pause')}
onEnd={() => loadNextVideo()}
onBuffering={() => showLoadingSpinner()}
/>
onPlaybackRateChange(playbackRate: number)
Fires when playback speed changes. To test this event, click the āļø settings button in the YouTube player and change the playback speed.
Common values: 0.25, 0.5, 1, 1.5, 2.
onPlaybackRateChange={(rate) => {
console.log(`Playback speed: ${rate}x`);
// Example: Save user's preferred playback speed
localStorage.setItem('preferredSpeed', rate.toString());
}}
onPlaybackQualityChange(quality: string)
Fires when video quality changes (either automatically or manually). To test this event manually, click the āļø settings button in the YouTube player and change the video quality.
Common values: "small" (240p), "medium" (360p), "large" (480p), "hd720", "hd1080", "hd1440", "hd2160" (4K).
onPlaybackQualityChange={(quality) => {
console.log(`Quality changed to: ${quality}`);
// Example: Track quality changes for analytics
analytics.track('video_quality_change', {
quality,
timestamp: Date.now()
});
}}
The live demo includes an Event Status Tracker that visually shows which events have fired during your interaction with the video. Each event displays:
This interactive tracker helps you:
Try it yourself:
function VideoWithAnalytics() {
const [playStartTime, setPlayStartTime] = useState(null);
return (
<LiteYouTubeEmbed
id="dQw4w9WgXcQ"
title="My Video"
enableJsApi
onReady={() => analytics.track('video_ready')}
onPlay={() => {
setPlayStartTime(Date.now());
analytics.track('video_play');
}}
onEnd={() => {
const watchTime = Date.now() - playStartTime;
analytics.track('video_complete', { watchTime });
}}
onError={(code) => analytics.track('video_error', { errorCode: code })}
/>
);
}
function VideoPlaylist() {
const videos = ['dQw4w9WgXcQ', 'abc123def', 'xyz789uvw'];
const [currentIndex, setCurrentIndex] = useState(0);
return (
<LiteYouTubeEmbed
id={videos[currentIndex]}
title={`Video ${currentIndex + 1}`}
enableJsApi
onEnd={() => {
if (currentIndex < videos.length - 1) {
setCurrentIndex(currentIndex + 1);
}
}}
onError={() => {
// Skip to next video on error
if (currentIndex < videos.length - 1) {
setCurrentIndex(currentIndex + 1);
}
}}
/>
);
}
ā ļø Events require enableJsApi={true}
ā ļø Lazy Loading Limitation - By default, the iframe only loads after the user clicks. Events won't fire until after user interaction. Use onIframeAdded callback to know when ready, or use alwaysLoadIframe={true} (not recommended for privacy/performance).
ā ļø Origin Validation - The component automatically validates events from YouTube domains for security.
ā ļø Cleanup - Event listeners are automatically cleaned up on unmount.
You can programmatically control the YouTube player via YouTube's IFrame Player API using refs and postMessage.
ā ļø Important: This requires
enableJsApi={true}. The ref is only available after the user clicks the poster (useonIframeAddedcallback to know when ready).
function VideoPlayer() {
const ytRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
return (
<div>
<button
onClick={() => {
setIsPlaying((oldState) => !oldState);
ytRef.current?.contentWindow?.postMessage(
`{"event": "command", "func": "${isPlaying ? "pauseVideo" : "playVideo"}"}`,
"*",
);
}}
>
{isPlaying ? 'Pause' : 'Play'}
</button>
<LiteYouTubeEmbed
title="My Video"
id="L2vS_050c-M"
ref={ytRef}
enableJsApi
alwaysLoadIframe
/>
</div>
);
}
Important: The ref only becomes available after the user clicks the poster.
onIframeAdded Callbackconst videoRef = useRef(null);
const handleIframeAdded = () => {
console.log("Iframe loaded and ready!");
if (videoRef.current) {
videoRef.current.contentWindow?.postMessage(
'{"event":"command","func":"playVideo"}',
'*'
);
}
};
return (
<LiteYouTubeEmbed
id="VIDEO_ID"
title="My Video"
ref={videoRef}
onIframeAdded={handleIframeAdded}
enableJsApi
/>
);
// This won't work - iframe doesn't exist yet!
useEffect(() => {
if (videoRef.current) {
console.log("This never runs");
}
}, []); // Empty deps - runs before iframe exists
Short answer: No, this is a YouTube platform limitation.
What changed: In September 2018, YouTube changed the rel=0 parameter to only limit related videos to the same channel, not hide them completely.
Best solution: Use the built-in stopOnEnd prop:
<LiteYouTubeEmbed
id="VIDEO_ID"
title="Video Title"
enableJsApi
stopOnEnd={true}
params="rel=0"
/>
This automatically stops the video when it ends and returns to the thumbnail view, preventing related videos from showing.
ā See more solutions in the docs
See the SSR Guide for detailed Next.js setup instructions and troubleshooting.
Yes! Set playlist={true} and optionally provide a playlistCoverId:
<LiteYouTubeEmbed
id="PLAYLIST_ID"
title="My Playlist"
playlist={true}
playlistCoverId="VIDEO_ID"
/>
Yes! Use the thumbnail prop to provide a custom image URL:
<LiteYouTubeEmbed
id="VIDEO_ID"
title="Video Title"
thumbnail="https://example.com/custom-thumbnail.jpg"
/>
Or choose a different YouTube thumbnail quality with poster:
<LiteYouTubeEmbed
id="VIDEO_ID"
title="Video Title"
poster="maxresdefault"
/>
We welcome contributions! See CONTRIBUTING.md for guidelines.
# Install dependencies
npm install
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Build
npm run build
# Lint
npm run lint
# Format
npm run format
This package includes:
Verify package authenticity:
npm audit signatures
See .github/SLSA.md for more details.
MIT Ā© Ibrahim Cesar
See LICENSE for full details.
Made with š§© in Brazil š§š·
FAQs
A private by default, faster and cleaner YouTube embed component for React applications
The npm package react-lite-youtube-embed receives a total of 97,220 weekly downloads. As such, react-lite-youtube-embed popularity was classified as popular.
We found that react-lite-youtube-embed demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.Ā It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
An AI agent is merging PRs into major OSS projects and cold-emailing maintainers to drum up more work.

Research
/Security News
Chrome extension CL Suite by @CLMasters neutralizes 2FA for Facebook and Meta Business accounts while exfiltrating Business Manager contact and analytics data.

Security News
After Matplotlib rejected an AI-written PR, the agent fired back with a blog post, igniting debate over AI contributions and maintainer burden.