Solar installer websites have a problem: they all look the same. A wide hero photo of glistening panels on a tile roof, three "trusted by" badges, a calculator that asks for your zip code, a quote form below the fold. The category sells a product that physically moves the sun's output into a homeowner's wall outlet — and every site about it looks static.
Helia is the fifth and final template in a five-site marketplace I shipped this week. The brief: a solar installer site that reads as a living system, not a brochure.
Stack
Static HTML per page (Helia Landing.html, Helia Solar.html, Helia Storage.html...). React 18 UMD. In-browser Babel. Tailwind via CDN. The new piece for this template: Framer Motion, loaded via the window.Motion UMD bundle.
Why bring in a motion library for one site when the other four ship without it? Because Helia's hero needs a boomerang-style video loop — play forward, play reverse, repeat — and the OrbitImages component needs to animate concentric arcs of imagery. WAAPI plus CSS keyframes could do it, but Motion's animate + useReveal patterns made the orbit logic one component instead of three.
The hero
The hero is a full-bleed BoomerangVideoBg of an aerial solar farm shot at golden hour. A green-tinted gradient overlay softens it for legibility. A pill at the top reads "Powering tomorrow · 24.6 MW live" — a fake live counter, but the format is real and the same pattern works wired to a metrics endpoint.
Below: a serif headline ("Renewable power for tomorrow, infinite clean solutions"), a calm subtitle, two CTAs stacked on mobile. The primary is a soft gradient pill ("Explore options") with a play icon; the secondary is a clean white pill ("Start network") with an arrow.
OrbitImages
The "powered by" section is six concentric rings of imagery rotating around a center logo. Each ring is a Motion.div with animate={{ rotate: 360 }} and a per-ring duration. The trick: the images inside each ring are themselves rotated counter to the parent rotation, so they stay upright relative to the viewer while their ring orbits.
CSS variables (--gx, --gy) feed a radial glow that follows the cursor inside the center logo card — pure onMouseMove updating two custom properties.
Two bugs I caught in QA
1. The "infi nite" word break. The hero headline uses a StaggeredFade component that splits the string into per-character spans for the entrance animation. Those spans were display: inline-block, which meant any non-breaking space between them broke at the word boundary instead of staying glued. "infinite" wrapped as "infi nite" on the 375-wide viewport. Fix: switch the chars to display: inline and use real spaces — the inline-block was inherited from an older variant of the same component that needed independent transforms.
2. The helia/ subdir ghost references. When I flattened the source layout (originally everything lived in helia/), 16 HTML files still pointed at helia/lib.jsx, helia/primitives.jsx, and so on. The first deploy 404'd half the scripts. A sed pass over every HTML file stripped the prefix and the next deploy was clean.
Reveal animation
Helia uses a lighter reveal than the other four templates — WAAPI element.animate([{opacity: 0}, {opacity: 1}], {duration: 300, ...}). Opacity only, no transform. The reason: the hero is so dense with motion already (boomerang loop, orbit rings, gradient drift) that adding rise-up sections to every block read as twitchy. Cutting the per-section motion to a 300ms fade let the always-on background motion carry the energy.
What I would change
The OrbitImages performance dips below 60fps on the lowest-end Android I tested (a 3-year-old Moto G). Six rings × ~8 images = 48 transformed elements at 30Hz. A future pass would pre-bake a sprite atlas and animate a single canvas — fewer reflows, identical visual.
