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/calligraphyQuick 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
Variants
All 12 variants mapped to their semantic HTML tag and type scale.
The craft of typography
Page-level heading
Section heading
Sub-section heading
Card heading
Small heading
Micro heading
Supporting subtitle for a section
Body copy for reading at length. Well-crafted typography improves comprehension.
<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.
riseSmooth upward fade-in
staggerWord-by-word entrance
clipUnmasked left to right
popSpring scale-in
lettersLetter-by-letter slide
blurEmerges from a blur
flip3D perspective rotate
swipeSlides from the right
typewriterCharacter reveal
bounceDrop with a bounce
velvetSoft skew drift
curtainPer-word upward clip
morphSquash-and-stretch spring
groundRises from baseline
cascadeDiagonal char waterfall
spotlightLetterspace compress-open
inkGentle scale fade
hingeRotates from left edge
stretchHorizontal rubber-band
peelBottom-to-top clip
rippleElastic scale outward
cinchChars pinch then snap
tiltriseRise while untilting
unfurlExpands from center
billboardY-axis rotation
tectonicAlternating side slam
stratifyZ-depth blur flight
orbitDot grows + rotates
liquidCross-axis squash spring
noiseFadeSignal-lock opacity
slabPrint-press scaleX stamp
threadSine-wave Y offsets
glassRevealBackdrop blur evaporates
wordPopPer-word scale from zero
scanlineHorizontal slice expand
chromaShiftRGB channels collapse
wordFadePer-word cross-dissolve
rotateInY-axis card flip
pressInPress 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
| Prop | Type | Default | Description |
|---|---|---|---|
variant | TypographyVariant | "Body" | Typography scale — Display through Caption |
font | string | — | Google Font name. Auto-injects the <link> tag. |
color | string | — | Any valid CSS color value |
align | "left" | "center" | "right" | "justify" | — | Text alignment |
animation | HeroAnimation | — | Entrance animation. Display / H1 only. |
italic | boolean | true | Render <em> children in Instrument Serif italic |
accentColor | string | "#c8b89a" | Color for the italic <em> accent span |
as | ElementType | — | Override the rendered HTML tag |
truncate | boolean | false | Single-line ellipsis truncation |
maxLines | number | — | Multi-line clamp with ellipsis |
className | string | — | Additional class names |
style | CSSProperties | — | Inline style overrides |