A video hero that does not wreck your text or your data bundle
The Camelphat hero paints a still first, loads video only if you can afford it, and gets its contrast measured against the worst frame in the clip.
- performance
- accessibility
- video
The lead paragraph in the Camelphat video hero measured 1.07:1 against the footage behind it. That is not “low contrast”. That is white text on white.
It did not last the whole clip. The clip is Pexels stock footage, and part-way through a run of bright aluminium rails sweeps across the frame — directly under the copy. Nothing about a still frame or an average would have caught it. The moment the rails arrived, the paragraph disappeared.
That is the whole difference between a still and a video, and it is the part people skip. With a still you audit one frame. If it fails you pick a darker photo, or you darken the one you have, and you are done. With a video you are shipping thirty frames a second and the type has to survive all of them. The frame you looked at is not the frame your visitor is looking at.
Measure the worst frame, not a nice one
The method is boring and it works: pull frames out of the clip, sample the real rendered pixels behind the text box in each one, and take the worst result as the number. Not the poster frame — the poster is the frame you chose, so of course it passes. Not an eyeballed midpoint. The worst.
Once you have the worst frame, you can fix it deliberately instead of guessing. Two changes did it here:
A brightness floor on the footage. It holds the clip under a known ceiling, so no frame is allowed past it. That gives the type a guaranteed darkest baseline instead of one that swings with the content.
A scrim that changes axis at 1024px. A gradient over the video, under the text. The axis changes at 1024px, so the scrim follows where the copy actually sits at each width. A scrim that ignores where the text actually is will always leave a corner unguarded.
Final measured numbers on the worst frame: 11.2:1 for the headline, 6.8:1 for the lead paragraph on mobile. From 1.07:1.
The poster paints, the video maybe arrives
The hero is a poster image with a video attached, not a video with a fallback. preload="none", poster set, so the first paint is a still and nothing has been downloaded off the network for the motion yet.
The video file is only requested once the checks below pass. When they fail, no .mp4 is ever fetched and the poster is simply the hero. It looks intentional because it is — the design does not depend on the motion.
Two files, one breakpoint
The clip ships at two resolutions: 1280x720 above 768px, 640x360 below. A phone does not need a 720p file to fill a 390px-wide box, and it is the phone that is paying for it.
That is one breakpoint, not five. More resolutions means more encodes, more cache misses, more things to get wrong at deploy. One split at the point where the layout itself changes covers most of the benefit.
Two cases where it never loads at all
prefers-reduced-motion: reduce — someone has told their operating system that moving imagery makes them ill or distracted. Autoplaying a hero at them anyway is not a design decision, it is ignoring a request.
Save-Data: on — someone has told their browser they are paying for every megabyte. In South Africa that is not an edge case. People buy data in bundles and watch the bundle.
In both cases: no request is made, the poster stands. Verified in headless Chrome with the request log filtered — zero .mp4 requests. Not “should not fetch”. Zero.
Verify the built output, not the source
Everything above is a claim until a browser confirms it. I drive headless Chrome with Puppeteer against the built site and check three things:
- The network log, filtered to
.mp4, under emulated reduced-motion and with Save-Data set. - The contrast, by pixel-sampling the actual rendered background behind the text, frame by frame through the clip — not by reading hex codes out of a stylesheet, because the stylesheet does not know what the video is doing.
- Horizontal overflow at 320, 390, 768 and 1440, because a full-bleed video element is an excellent way to add a few pixels of sideways scroll to a page.
Source code tells you what you intended. The built output tells you what you shipped.
The rule, generalised
If the background moves, the contrast number is the worst frame’s number. Everything else is an average, and averages do not read paragraphs.
If the motion is decoration, it must be the last thing loaded and the first thing dropped — behind a poster, behind preload="none", behind an explicit opt-out for reduced motion and metered data.
And the design has to still work with the video switched off. If the still version looks broken, the video was carrying the layout, and you have built a page that only works for people on fast connections who do not mind things moving.
The site is live: camelphat-solar-503426587396.europe-west1.run.app. Astro 5, Tailwind 4, vanilla JavaScript, no UI framework, served by nginx from a container on Cloud Run. 12 pages. Load it on a phone with data saver on and watch the network log stay free of .mp4 requests.