Skip to content

Instantly share code, notes, and snippets.

@jordanmaslyn
Last active September 10, 2025 15:31
Show Gist options
  • Save jordanmaslyn/07ee67ccd0d86c31d94f03ea9e79b97b to your computer and use it in GitHub Desktop.
Save jordanmaslyn/07ee67ccd0d86c31d94f03ea9e79b97b to your computer and use it in GitHub Desktop.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
import React, { useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
export const VideoFacade = ({
videoUrl,
autoplayOnView = false,
poster,
}) => {
const [isPlaying, setIsPlaying] = useState(false);
const [hasUserInitiated, setHasUserInitiated] = useState(false);
const wrapperRef = useRef(null);
useEffect(() => {
if (!autoplayOnView || isPlaying || !wrapperRef.current) return undefined;
const node = wrapperRef.current;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsPlaying(true);
setHasUserInitiated(false);
observer.disconnect();
}
});
},
{ threshold: 0.5 }
);
observer.observe(node);
return () => observer.disconnect();
}, [autoplayOnView, isPlaying]);
const handlePlay = () => {
setIsPlaying(true);
setHasUserInitiated(true);
};
const handleKeyDown = (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handlePlay();
}
};
const containerStyle = {
position: 'relative',
width: '100%',
aspectRatio: '16 / 9',
backgroundColor: '#000',
overflow: 'hidden',
borderRadius: 8,
};
const mediaStyle = {
width: '100%',
height: '100%',
objectFit: 'cover',
display: 'block',
};
const overlayStyle = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 64,
height: 64,
borderRadius: '50%',
background: 'rgba(0,0,0,0.6)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
pointerEvents: 'none',
};
return (
<div ref={wrapperRef} style={containerStyle}>
{isPlaying ? (
<video
src={videoUrl}
style={mediaStyle}
controls
autoPlay
playsInline
muted={!hasUserInitiated}
poster={poster?.src}
/>
) : (
<div
role="button"
tabIndex={0}
aria-label="Play video"
onClick={handlePlay}
onKeyDown={handleKeyDown}
style={{ ...mediaStyle, cursor: 'pointer' }}
>
{poster?.src ? (
<img
src={poster.src}
alt={poster?.alt || 'Video poster'}
style={mediaStyle}
/>
) : (
<div style={{ ...mediaStyle, background: '#111' }} />
)}
<div style={overlayStyle}>
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 5v14l11-7-11-7z" fill="#fff" />
</svg>
</div>
</div>
)}
</div>
);
};
VideoFacade.propTypes = {
/** URL of the video source */
videoUrl: PropTypes.string.isRequired,
/** If true, video autoplays when at least 50% in viewport */
autoplayOnView: PropTypes.bool,
/** Poster image for the placeholder */
poster: PropTypes.shape({
src: PropTypes.string.isRequired,
alt: PropTypes.string,
}).isRequired,
};
export default VideoFacade;
import { declareComponent } from "@webflow/react";
import VideoFacade from "./VideoFacade";
import { props } from "@webflow/data-types";
export default declareComponent(VideoFacade, {
name: "Video Facade",
description: "Embed a video performantly!",
group: "Atoms",
props: {
videoUrl: props.Text({ name: "Video URL", defaultValue: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", }),
autoplayOnView: props.Boolean({ name: "Autoplay on View", defaultValue: false, }),
poster: props.Image({ name: "Poster", }),
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment