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:

code
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
}
}
]
}
Banner for Video Embedding

Register the Schema

Update your schema index (/schemas/index.ts) so Sanity knows about the new type:

code
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:

code
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:

code
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

code
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:

code
npm install lozad

The 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.