Build notes
How this was built
MAJUSCULE has no images, no canvas, no video and almost no JavaScript. It is five variable fonts and a set of CSS animations. That is the whole engine, and you can lift every piece of it.
The one idea: variable fonts
A variable font packs a whole family into one file, along a set of continuous axes: weight, width, optical size, slant and more. You address any point in that space with one CSS property,font-variation-settings. The trick this whole site leans on: that property is animatable.
/* one axis, animated straight in CSS, no JavaScript */
@keyframes weight-wave {
0% { font-variation-settings: "wght" 320; }
100% { font-variation-settings: "wght" 760; }
}
.hero-letter { animation: weight-wave 3.6s ease-in-out infinite alternate; }
/* stagger each letter with a negative delay to make a wave */
.hero-letter[data-i="1"] { animation-delay: -0.4s; }
.hero-letter[data-i="2"] { animation-delay: -0.8s; }Give each letter the same animation with a staggered negative delay and the weight ripples across the word like wind through grass. No library, no JavaScript, and it respects prefers-reduced-motion because the whole rule sits behind that media query.
Type that reads as you scroll
The specimens go further: instead of looping on a timer, they are tied to scroll position with a view timeline. As a specimen passes through the viewport, the browser maps that progress onto the animation, so you read the typeface's entire range in one scroll.
/* a specimen that morphs as it scrolls through the viewport */
@keyframes morph {
from { font-variation-settings: "opsz" 9, "wght" 300; }
to { font-variation-settings: "opsz" 144,"wght" 860; }
}
@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
.specimen-word {
animation: morph linear both;
animation-timeline: view(); /* tied to scroll position */
animation-range: entry 8% cover 90%;
}
}
}Scroll-driven animations are progressive enhancement: where the browser does not support them, the specimen simply sits at a well-chosen static weight. Nothing breaks.
Loading the fonts
Fonts come from Fontsource, self-hosted so they load same-origin under a strict Content Security Policy. Import the full stylesheet to get every axis, not just weight.
// load the full axis set so every axis is animatable
import '@fontsource-variable/fraunces/full.css';
// then in CSS:
// font-family: "Fraunces Variable";
// font-variation-settings: "opsz" 144, "wght" 600, "SOFT" 0, "WONK" 1;Five variable fonts is real weight, so only the flagship display face is preloaded; the rest arrive as they are needed, and text renders immediately in a fallback and swaps.
No images, on purpose
There is not one raster image here. That is the constraint that makes the site: it forces the type to carry everything, and it keeps the page tiny and instant. If you take one idea from this build, take that one.
Deploy in two commands
npx wrangler pages project create majuscule --production-branch main
npm run build
npx wrangler pages deploy dist --project-name majuscule --branch mainThis is one of three showcase sites. See the prompts, or go back to the specimens.