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.

the hero waveCSS
/* 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.

scroll-driven specimenCSS
/* 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.

a variable fontJS + CSS
// 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

terminalbash
npx wrangler pages project create majuscule --production-branch main
npm run build
npx wrangler pages deploy dist --project-name majuscule --branch main

This is one of three showcase sites. See the prompts, or go back to the specimens.