Block Wordmark
A wordmark that builds itself out of solid blocks — a deck shuffles, collapses, splits one block per letter, and each block flashes and cuts to its real letterform
Installation
$ pnpm dlx shadcn@latest add @snapcn/block-wordmarkComponent code
The exact file shadcn add copies into your project.
Usage
A wordmark build in six beats:
- Seed — a single rounded square scales up off the baseline (
growDuration). - Shuffle — coloured cards fan out from behind it and wind back in, one full turn
each about a shared pivot on the baseline. The fan is one S-curve sampled at
cardStaggeroffsets, so it opens and closes exactly once (cards,spinTurns,spinDuration,liftRatio). - Collapse — the deck lands back on the seed square and dips once, which is the only frame in the piece where the ink is a single block.
- Split — the square stretches horizontally and splits into one block per letter,
outermost blocks leading and the inner ones lagging by
splitStagger. Any letter whose ink rises above the x-height grows a tab on the correct side — the "b" ofbasesprouts a left stem, a "d" a right one, an "A" an apex. - Hold — the block row sits and scales up slowly for
holdDurationframes. This is the beat that reads as a logo rather than as an animation. - Resolve — each block flashes a colour from
colors, returns to ink, and is hard-cut to its letterformflashToSwapframes later. The first letter goes last, so the word finishes where it started.
Nothing crossfades and nothing morphs. At each swap frame the block is already within a few pixels of the settled letter, so the cut is invisible — and a spring or a crossfade "to improve it" is the one change that breaks the effect.
// src/Root.tsx
import { Composition } from "remotion";
import { BlockWordmark } from "@/components/snap-cn/block-wordmark";
const Sting = () => (
<BlockWordmark text="snapcn" color="#0000FF" fontSize={160} />
);
export const RemotionRoot = () => (
<Composition
id="BlockWordmark"
component={Sting}
durationInFrames={150}
fps={30}
width={1280}
height={720}
/>
);Any word, not just this one
Nothing about the geometry is hardcoded to a four-letter lowercase word. Every glyph's
advance width, x-height and ink top are measured at runtime behind delayRender(), so
the blocks are sized to the letters they become and the ascender tabs are found by
measurement rather than a letter list. Emoji survive (the text is split by code point)
and spaces get a gap, not a block.
Two knobs decide how literal the morph is:
blockSizing="square"(default) makes every block a square the height of the x-height, so the row reads as an even rhythm and each block visibly changes into its letter.blockSizing="glyph"sizes each block to its letter's advance width, which makes the resolve nearly in-place.
Long words outrun the 150-frame default: the last letter resolves at roughly
94 + flashStagger × letters. Drop flashStagger to 2 or raise durationInFrames.
Colour
The deck and the flash colours are a prop with our defaults — the first card follows
your primary token, the rest are a decorative set.
The ink defaults to your foreground, so an installed copy paints in whatever
palette it lands in. The preview above passes color="#0000FF" because that is
snapcn's own mark; yours is one prop away.
A full brand look — ink, deck, and a display face's heavier ascender — is one line.
base rather than snapcn here because the ascender props only show on a word that
has one:
<BlockWordmark
text="base"
color="#0000FF"
colors="#ABF25B,#FA361A,#FCCA28,#337DFE,#F99BCA"
ascenderRatio={0.4}
ascenderStemRatio={0.415}
/>ascenderRatio and ascenderStemRatio are the two places a display face differs most
from Inter: leave them out and they are measured per glyph, set them and you get the
chunky flag a geometric sans draws.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
text | string | "base" | The word to build. Split by code point, so emoji stay whole; spaces get a gap, not a block |
fontSize | number | 160 | Size the finished wordmark is set at, in px |
fontFamily | string | Inter | Loaded through @remotion/google-fonts, so the Player and the render agree |
fontWeight | number | string | 700 | Weight of the finished letterforms and of the probe they are measured from |
color | string | theme.foreground | The wordmark's ink, and the colour of the lead card in the deck |
colors | string[] | string | theme.primary + four accents | The deck's cards and the flash colours, in order. Accepts an array or a comma-separated string |
background | string | theme.background | Stage behind the build |
blockGap | number | 0.093 | Gap between blocks, as a fraction of a block's side |
cornerRadius | number | 0.07 | Corner radius of a block, as a fraction of its side |
ascenderRatio | number | measured | Tab height ÷ block height. Omitted, it is measured per glyph — how far that letter actually rises above the x-height |
ascenderStemRatio | number | measured | Tab width ÷ block width. Omitted, measured too: the stem of a "b", the whole width of a "B" |
blockSizing | "square" | "glyph" | "square" | Uniform squares (an even rhythm), or each block sized to the letter it becomes (a near in-place morph) |
maxWidth | number | 0.92 | Fraction of the frame width the row may occupy before it is scaled to fit |
cards | number | 6 | Cards in the deck, including the lead one |
cardStagger | number | 2 | Frames between one card's turn starting and the next card's |
spinTurns | number | 1 | Full turns the deck makes while it fans and winds back in |
liftRatio | number | 0.665 | How far the deck lifts off the baseline as it fans, as a fraction of a block's side |
growDuration | number | 9 | Frames the first square takes to scale in |
spinDuration | number | 33 | Frames the deck's turn lasts |
splitDuration | number | 5 | Frames a block takes to travel from the collapsed square to its slot |
splitStagger | number | 1 | Frames the inner blocks lag the outer ones by — the split runs outside-in |
ascenderDuration | number | 12 | Frames an ascender's tab takes to grow |
holdDuration | number | 54 | Frames the finished block row holds before the letters resolve |
flashStagger | number | 4 | Frames between one letter's flash and the next. Lower it for long words |
flashDuration | number | 3 | Frames a block stays lit in its flash colour |
flashToSwap | number | 5 | Frames from a block lighting up to it being hard-cut to its letterform |
speed | number | 1 | Global playback multiplier applied to the whole sequence |
className | string | — | Optional className passed to the outer wrapper |
theme | Partial<SnapCnTheme> | — | Design-system token overrides. Anything you leave out falls back to the shadcn defaults |
mode | "light" | "dark" | — | Which end of the design system to resolve tokens against |