GIFs have long been used to add short, looping visuals to websites, but they come with real drawbacks: they"re heavy on bandwidth, low in quality, and lack playback flexibility. Videos, on the other hand, offer higher quality, smaller file sizes, and more control. By replacing GIFs with videos in a Sanity + Gatsby workflow, you can significantly improve both performance and user experience.
Prerequisites
Before replacing GIFs with videos, you need:
- A Sanity Studio project set up (sanity init or an existing schema folder).
- A Gatsby project already connected to Sanity via GraphQL or GROQ.
- Installed packages: @sanity/asset-utils, @sanity/ui, react-icons, and lozad (for lazy loading).
- Access to Sanity Studio and your Gatsby source code.
- Basic familiarity with React, GraphQL, and Sanity schemas.
Create a Video Schema in Sanity
Inside your schema folder (/schemas/videoAnimation.ts), define a custom object schema for video assets:
import { MdVideoLibrary } from "react-icons/md";
export default {
name: 'videoAnimation',
title: 'Video animation',
type: 'object',
icon: MdVideoLibrary,
fields: [
{
name: 'webm',
title: 'WebM format',
type: 'file',
options: {
accept: 'video/webm'
}
},
{
name: 'fallback',
title: 'Fallback format',
type: 'file',
options: {
accept: 'video/mp4'
}
},
{
name: 'aspectratio',
title: 'Aspect Ratio',
type: 'number',
},
{
name: 'caption',
title: 'Caption',
type: 'string',
options: {
isHighlighted: true
}
},
{
name: 'alt',
title: 'Alternative text (for screen readers)',
type: 'string',
options: {
isHighlighted: true
}
}
]
}
Register the Schema
Update your schema index (/schemas/index.ts) so Sanity knows about the new type:
import videoAnimation from './videoAnimation';
export const schemaTypes = [
blockContent,
post,
author,
videoAnimation, // ??? register here
];
Enable Video in Block Content
Modify your blockContent.ts to allow videoAnimation blocks inside rich text:
import {defineType, defineArrayMember} from 'sanity'
export default defineType({
title: 'Block Content',
name: 'blockContent',
type: 'array',
of: [
defineArrayMember({
title: 'Block',
type: 'block',
styles: [
{title: 'Normal', value: 'normal'},
{title: 'H1', value: 'h1'},
{title: 'H2', value: 'h2'},
{title: 'H3', value: 'h3'},
{title: 'H4', value: 'h4'},
{title: 'Quote', value: 'blockquote'},
],
lists: [{title: 'Bullet', value: 'bullet'}],
marks: {
decorators: [
{title: 'Strong', value: 'strong'},
{title: 'Emphasis', value: 'em'},
],
annotations: [
{
title: 'URL',
name: 'link',
type: 'object',
fields: [
{title: 'URL', name: 'href', type: 'url'},
],
},
],
},
}),
defineArrayMember({
type: 'image',
options: {hotspot: true},
}),
defineArrayMember({
type: 'videoAnimation', // ??? enable video
}),
],
})
Create a Video Component in Gatsby
Inside your Gatsby project, create components/VideoAnimation.js:
import React, { useEffect } from 'react'
import { getFileAsset } from '@sanity/asset-utils'
import sanityConfig from '../../studio/sanity.json'
function videoAssetFor(source) {
return getFileAsset(source, sanityConfig.api)
}
const getPaddingFromAspectRatio = (ratio) => {
if (!ratio) return "0";
return `${(1 / ratio) * 100}%`
}
const VideoAnimation = ({ webm, fallback, aspectratio, alt, caption }) => {
if (!webm || !fallback) return null
const webmAsset = videoAssetFor(webm)
const fallbackAsset = videoAssetFor(fallback)
useEffect(() => {
const lozad = require('lozad')
const observer = lozad()
observer.observe()
}, [])
return (
<figure style={{ paddingTop: getPaddingFromAspectRatio(aspectratio) }}>
<video
title={alt}
className="lozad"
loop
muted
autoPlay
playsInline
>
<source data-src={webmAsset.url} type={`video/${webmAsset.extension}`} />
<source data-src={fallbackAsset.url} type={`video/${fallbackAsset.extension}`} />
</video>
{caption && <figcaption>{caption}</figcaption>}
</figure>
)
}
export default VideoAnimation
Update Block Content Serializers
In your Gatsby project"s block content renderer (BlockContent.js):
Example: Gatsby GraphQL Query
import BaseBlockContent from '@sanity/block-content-to-react'
import React from 'react'
import VideoAnimation from './VideoAnimation'
const serializers = {
types: {
videoAnimation: (props) => <VideoAnimation {...props.node} />
}
}
const BlockContent = ({ blocks }) => (
<BaseBlockContent blocks={blocks} serializers={serializers} />
)
export default BlockContent
Lazy Loading with Lozad
Make sure lozad is installed:
npm install lozadThe useEffect hook in VideoAnimation.js initializes Lozad, and the <video> sources use data-src attributes for deferred loading. This ensures videos only load when in the viewport.

