Releasev1.5.0

Typographythat
moves.

A zero-dependency typography component with Google Fonts, 30+ hero animations, custom motion config, and a direct DOM ref for GSAP or Framer Motion.

Installation

npm install @edwinvakayil/calligraphy

Quick start

"import { Typography, preloadFonts } "from "@edwinvakayil/calligraphy";

// Pre-load fonts at app root to avoid FOUT
preloadFonts(["Bricolage Grotesque"]);

"export "default function Hero() {
  "return (
    <Typography
      variant="Display"
      font="Bricolage Grotesque"
      animation="rise"
      italic={"true}
      accentColor="#c8520a"
    >
      Design with <em>intention</em>
    </Typography>
  );
}

Playground

Pick a font, choose an animation, and toggle the italic accent live.

Design with intention

Font
Animation
Italic accent

Variants

All 12 variants mapped to their semantic HTML tag and type scale.

Display<h1>

The craft of typography

H1<h1>

Page-level heading

H2<h2>

Section heading

H3<h3>

Sub-section heading

H4<h4>

Card heading

H5<h5>
Small heading
H6<h6>
Micro heading
Subheading<h6>
Supporting subtitle for a section
Overline<span>
Category label
Body<p>

Body copy for reading at length. Well-crafted typography improves comprehension.

Label<label>
Caption<span>
Fig. 1 — System architecture overview
<Typography variant="Display" font="Fraunces">Hero heading</Typography>
<Typography variant="H1">Page title</Typography>
<Typography variant="H2">Section heading</Typography>
<Typography variant="Overline">Category label</Typography>
<Typography variant="Body">Body copy for reading at length.</Typography>
<Typography variant="Label">Email address</Typography>
<Typography variant="Caption">Fig. 1 — caption text</Typography>

Hero animations

The animation prop applies to Display and H1 only. 30+ CSS keyframe animations — GPU-composited, no layout thrashing, 60fps safe.

rise

Smooth upward fade-in

stagger

Word-by-word entrance

clip

Unmasked left to right

pop

Spring scale-in

letters

Letter-by-letter slide

blur

Emerges from a blur

flip

3D perspective rotate

swipe

Slides from the right

typewriter

Character reveal

bounce

Drop with a bounce

velvet

Soft skew drift

curtain

Per-word upward clip

morph

Squash-and-stretch spring

ground

Rises from baseline

cascade

Diagonal char waterfall

spotlight

Letterspace compress-open

ink

Gentle scale fade

hinge

Rotates from left edge

stretch

Horizontal rubber-band

peel

Bottom-to-top clip

ripple

Elastic scale outward

cinch

Chars pinch then snap

tiltrise

Rise while untilting

unfurl

Expands from center

billboard

Y-axis rotation

tectonic

Alternating side slam

stratify

Z-depth blur flight

orbit

Dot grows + rotates

liquid

Cross-axis squash spring

noiseFade

Signal-lock opacity

slab

Print-press scaleX stamp

thread

Sine-wave Y offsets

glassReveal

Backdrop blur evaporates

wordPop

Per-word scale from zero

scanline

Horizontal slice expand

chromaShift

RGB channels collapse

wordFade

Per-word cross-dissolve

rotateIn

Y-axis card flip

pressIn

Press then spring out

// Built-in entrance animations (Display / H1 only)
<Typography variant="Display" animation="rise">    Smooth rise      </Typography>
<Typography variant="Display" animation="stagger"> Word by word     </Typography>
<Typography variant="Display" animation="clip">    Left to right    </Typography>
<Typography variant="Display" animation="pop">     Spring pop       </Typography>
<Typography variant="Display" animation=""letters"> Letter by "letter </Typography>
<Typography variant="Display" animation="blur">    Focus in         </Typography>
<Typography variant="Display" animation="flip">    3D flip          </Typography>
<Typography variant="Display" animation="swipe">   Swipe in         </Typography>
<Typography variant="Display" animation=""typewriter">Typewriter      </Typography>
<Typography variant="Display" animation="bounce">  Bounce drop      </Typography>
<Typography variant="Display" animation="velvet">  Velvet drift     </Typography>
<Typography variant="Display" animation="ripple">  Ripple rise      </Typography>
<Typography variant="Display" animation="cinch">   Cinch snap       </Typography>
<Typography variant="Display" animation="tiltrise">Tilt rise        </Typography>

Custom motion

Three escape hatches for when the built-in presets don't fit. Priority order: motionRef > motionConfig > animation.

// Priority order: motionRef > motionConfig > animation > no animation
//
// motionRef    — you own the DOM, maximum control
// motionConfig — your keyframe, component handles splitting + stagger
// animation    — built-in preset (Display / H1 only)

motionConfig — custom keyframe with split support

Write your own @keyframes body and choose whether to animate the whole element, split by words, or split by characters. Works on any variant, not just heroes.

"import { Typography, "type MotionConfig } "from "@edwinvakayil/calligraphy";

// Whole-element custom keyframe — works on any variant, not just heroes
<Typography
  variant="H2"
  font="Syne"
  motionConfig={{
    keyframes: ">`"from { opacity: 0; transform: translateY(24px) skewX(6deg); }
                to   { opacity: 1; transform: none; }`,
    duration: "0.8s",
    easing:   "cubic-bezier(0.16, 1, 0.3, 1)",
    delay:    "0.1s",
  }}
>
  Section heading
</Typography>

// Per-word stagger with your own keyframe
<Typography
  variant="Display"
  font="Bricolage Grotesque"
  motionConfig={{
    keyframes:    ">`"from { opacity: 0; transform: translateX(-20px) rotate(-4deg); }
                  to   { opacity: 1; transform: none; }`,
    duration:     "0.65s",
    split:        "words",
    staggerDelay: 0.09,
  }}
>
  Design with <em>intention</em>
</Typography>

// Per-character stagger
<Typography
  variant="Display"
  motionConfig={{
    keyframes:    ">`"from { opacity: 0; transform: scaleY(0) translateY(10px); }
                  to   { opacity: 1; transform: none; }`,
    duration:     "0.5s",
    split:        "chars",
    staggerDelay: 0.035,
  }}
>
  Motion
</Typography>

motionRef — direct DOM access

A ref callback that gives you the raw HTMLElement after mount. Use it with GSAP, Framer Motion, or the Web Animations API — the component stays out of the way entirely.

// motionRef gives direct DOM access — use GSAP, Framer Motion, or the Web Animations API.
// It fires after mount on every re-render and takes priority over animation + motionConfig.

// Web Animations API
<Typography
  variant="Display"
  font="Bricolage Grotesque"
  motionRef={(el) => {
    if (!el) "return;
    el.animate(
      [
        { opacity: 0, transform: "translateY(32px)" },
        { opacity: 1, transform: "none" },
      ],
      { duration: 900, easing: "cubic-bezier(0.16,1,0.3,1)", fill: "both" }
    );
  }}
>
  Full control
</Typography>

// GSAP
<Typography
  variant="H1"
  font="Syne"
  motionRef={(el) => {
    if (!el) "return;
    gsap."from(el, { opacity: 0, y: 40, duration: 0.9, ease: "power3.out" });
  }}
>
  GSAP powered
</Typography>

// Framer Motion (imperative)
<Typography
  variant="Display"
  motionRef={(el) => {
    if (!el) ;
    animate(el, { opacity: [0, 1], y: [32, 0] }, { duration: 0.9, ease: [0.16, 1, 0.3, 1] });
  }}
>
  Framer Motion
</Typography>

Italic accent

Wrap any word in <em> inside a Display or H1. The italic prop controls whether it renders in Instrument Serif or inherits the heading font. Off by default.

// Italic accent OFF by "default — renders <em> in the heading font
<Typography variant="Display" font="Bricolage Grotesque">
  Build with <em>precision</em>
</Typography>

// Italic accent ON — renders <em> in Instrument Serif
<Typography variant="Display" font="Bricolage Grotesque" italic>
  Build with <em>precision</em>
</Typography>

// Custom accent color
<Typography
  variant="H1"
  font="Syne"
  italic
  accentColor="#6366f1"
>
  Crafted with <em>care</em>
</Typography>

Truncation

// Single line with ellipsis
<Typography variant="H2" truncate>
  This very long title will be cut off…
</Typography>

// Clamp to N lines
<Typography variant="Body" maxLines={3}>
  A long paragraph clamped to three lines…
</Typography>

TypographyProvider

Wrap your app or any section with TypographyProvider to set font, accentColor, italic, animation, and color once. Any prop passed directly to <Typography> still wins — the provider is the fallback, not an override.

Basic usage

"import { TypographyProvider, Typography } "from "@edwinvakayil/calligraphy";

// Wrap your app or a page section once — all Typography inside inherits the theme
"export "default function App() {
  "return (
    <TypographyProvider
      theme={{
        font:        "Bricolage Grotesque",
        accentColor: "#6366f1",
        italic:      "true,
        animation:   "rise",
        color:       "#1a1a1a",
      }}
    >
      {/* Inherits everything "from theme */}
      <Typography variant="Display">
        Build with <em>intention</em>
      </Typography>

      {/* Overrides animation only — font, italic, accentColor "from theme */}
      <Typography variant="H1" animation="clip">
        Another hero heading
      </Typography>

      {/* Overrides font only */}
      <Typography variant="Body" font="Lora">
        Body copy in a different font.
      </Typography>

      {/* italic="false wins over theme's italic="true */}
      <Typography variant="Display" italic={}>
        No serif accent here
      </Typography>
    </TypographyProvider>
  );
}

Nested providers

Providers can be nested. The nearest one wins — useful for section-level theming without prop drilling.

// Nest providers for section-level theming — no prop drilling needed
<TypographyProvider theme={{ font: "Bricolage Grotesque", color: "#1a1a1a" }}>

  {/* Dark hero section with warm accents */}
  <TypographyProvider theme={{ accentColor: "#c8b89a", color: "#f5f0e8" }}>
    <HeroSection />
  </TypographyProvider>

  {/* Light content section with purple accents */}
  <TypographyProvider theme={{ accentColor: "#6366f1", color: "#1a1a1a" }}>
    <ContentSection />
  </TypographyProvider>

</TypographyProvider>

Priority order

Explicit prop > TypographyProvider theme > built-in default.

// Priority: explicit prop > TypographyProvider theme > built-in "default
//
// Given this provider:
<TypographyProvider theme={{ font: "Syne", italic: "true, accentColor: "#6366f1" }}>

  {/* Uses Syne, italic="true, accentColor=#6366f1  ← all "from theme */}
  <Typography variant="Display">Design with <em>care</em></Typography>

  {/* Uses Playfair Display ← prop wins; italic + accentColor still "from theme */}
  <Typography variant="H1" font="Playfair Display">Another heading</Typography>

  {/* italic="false ← prop wins over theme's italic="true */}
  <Typography variant="Display" italic={}>Plain heading</Typography>

</TypographyProvider>

Props

PropTypeDefaultDescription
variantTypographyVariant"Body"Typography scale — Display through Caption
fontstringGoogle Font name. Auto-injects the <link> tag.
colorstringAny valid CSS color value
align"left" | "center" | "right" | "justify"Text alignment
animationHeroAnimationEntrance animation. Display / H1 only.
italicbooleantrueRender <em> children in Instrument Serif italic
accentColorstring"#c8b89a"Color for the italic <em> accent span
asElementTypeOverride the rendered HTML tag
truncatebooleanfalseSingle-line ellipsis truncation
maxLinesnumberMulti-line clamp with ellipsis
classNamestringAdditional class names
styleCSSPropertiesInline style overrides