# Kinetic Typography

> Type is the moving image: headlines reveal letter by letter, weights breathe on a variable font and marquee bands scroll — with a calm static fallback.

- Nickname: Moving type
- Also called: Motion Typography, Animated Type, Type in Motion
- Category: Editorial & Typographic
- Moods: Bold, Techy, Edgy, Luxury
- Industries: Media & Entertainment, Creative & Portfolio, Startup & Tech, Gaming, AI Products
- Style page: https://uilingo.dev/styles/kinetic-typography/

Kinetic Typography makes the words themselves the animation. Headlines rise in letter by letter, a variable display face pulses between hairline and black weights in a slow wave, and huge outlined lines of text scroll as marquee bands between sections. Everything else is quiet — a near-black stage, off-white type, one violet and one coral highlight, soft pill controls — so the motion reads as choreography rather than noise. Every effect sits behind prefers-reduced-motion and degrades to crisp static type.

## Best for
- Launch and campaign pages
- Music, film, festivals and events
- Agency and studio showreels
- Editorial features with one big idea

## Avoid for
- Long reading or documentation pages
- Audiences sensitive to motion (vestibular disorders) without a reduced-motion path
- Forms and checkout flows where motion distracts

## Do
- Reveal one headline at a time (32ms stagger per letter — per word in Thai — and 900ms per unit on cubic-bezier(.16,1,.3,1)); only the hero wave, the two marquees and the feature-index swell (while in view, with the hero wave paused) loop
- Use a true variable font and animate font-variation-settings (hero wave wght 800 ↔ 220, index swell 300 → 800), not font-weight keyframes (Latin only; the Thai fallback Prompt is static, so Thai hero headlines get a violet colour sweep instead and Thai index titles step Prompt 200 → 800)
- Wrap every animation in prefers-reduced-motion: no-preference and keep the static state fully legible
- Split Thai by word, never by character, so vowels and tone marks stay attached

## Don't
- Don’t animate body text or anything users must read to act
- Don’t run more than two marquees per screen (one outlined band, one slower logo row)
- Don’t clip letters with overflow:hidden reveals — it cuts off Thai tone marks
- Don’t carry the tight 1.02 headline line-height into Thai: it is for Latin only; with lang=th, hero, h1 and h2 use 1.3 and feature-index titles 1.4 so tone marks and upper vowels never collide

## Audit
Passed after 5 rounds · builder unknown · auditor unknown

## Prompt
Paste this into any coding agent. Composed for: Landing page, Plain HTML + CSS, Thai + English, dark mode. For another page, stack, language or mode, use the prompt builder on the style page.

```markdown
# Build a landing web page in the "Kinetic Typography" UI style

Platform: Web (responsive, 1440px desktop down to 375px) · Stack: Plain HTML + CSS · Mode: dark

## The style in one line
Type is the moving image: headlines reveal letter by letter, weights breathe on a variable font and marquee bands scroll — with a calm static fallback.
Design keywords: letter-by-letter reveal, variable font weight wave, scrolling marquee bands, outlined display text, wide display type, dark stage, staggered entrances, reduced-motion fallback.

## Visual language
A quiet, near-black stage (#0C0C0D) where only the type moves. A wide variable display face (Unbounded) carries everything: headlines rise in letter by letter with a slight blur and tilt, then the letters pulse between hairline and black weight in a slow travelling wave; a giant outlined copy of the headline scrolls past as a marquee band. Colour is restrained — off-white type, one soft violet (#A391FF) for the key word and active states, coral (#FF6A45) for dots, counters and separators — and controls are soft pills so they never compete with the letters. With reduced motion everything sits still and still reads beautifully.

## Signature details (must be visible)
- Letter-by-letter reveal: JS wraps each Latin word in a nowrap span.w and each Latin grapheme in an inline-block span.char (Intl.Segmenter granularity 'grapheme'); Thai is split by word into span.word (granularity 'word'), never by character. Each unit animates from opacity 0, translateY(0.45em) rotate(4deg) blur(6px) to rest over 900ms cubic-bezier(.16,1,.3,1), staggered 32ms per unit
- Variable weight wave on the Latin hero headline: @keyframes animating font-variation-settings 'wght' 800 → 220, 2.6s ease-in-out infinite alternate; letter i starts at 1.2s + i × 110ms, i.e. only after its own reveal (i × 32ms + 900ms) has finished. The loop pauses (animation-play-state: running, paused — the one-time reveal always finishes) while the hero is off screen
- Thai version of the wave: the Thai fallback Prompt is a static font, so font-variation-settings does nothing on Thai. Thai hero headlines skip the wght wave; instead a violet colour sweep (color #F4F1EA → #A391FF → #F4F1EA, 2.6s ease-in-out, infinite) starts once the reveal has ended — at 1.2s, which is after the 900ms reveal plus the last word's 32ms-per-word stagger for headlines of up to 9 words — with each word a further 110ms later, and keeps looping while the hero is in view. The last word, already violet, stays violet and is excluded from the sweep. Never inject Latin into a Thai headline just to show the wght wave
- Outlined text recipe (bands, stat numerals, Latin pull quotes, profile counts): Unbounded is variable, so its overlapping contours show as stray inner strokes under a plain -webkit-text-stroke. Fill with the stage instead of transparent — color #0C0C0D, -webkit-text-stroke 3px #F4F1EA, paint-order: stroke fill, letter-spacing 0 — so only the outer 1.5px of the stroke shows and the internal overlaps are hidden; small mobile outlines use a 2px stroke (1px visible). Never track outlined letters (they merge)
- At most two marquees per screen, each built as two identical sibling groups side by side, each group animated translateX(0 → -100%) linear infinite so the second copy slides in seamlessly: (1) one outlined headline band directly under the hero — the hero headline repeated three times per group at clamp(3rem, 7vw, 6rem) Unbounded 800 in the outlined recipe, with coral ✦ separators, between 1px #2C2C31 rules, 40s per loop; (2) one slower logo marquee (60s) moving the opposite way (animation-direction: reverse) with edges faded by a mask-image linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent). Never a third
- Feature list as a typographic index of exactly 6 rows: full-width rows split by 1px #2C2C31 rules, a DM Mono '01'–'06' counter in coral, a clamp(1.7rem, 3.6vw, 3rem) Unbounded title at rest weight 300 that swells to 800 in turn (font-variation-settings, 6s ease-in-out infinite; the swell occupies the first 30% of the cycle and peaks at 12%; row n is delayed (n − 1) × 1s, so exactly one row is at its peak at a time) On hover set animation: none on the title (a running animation overrides normal declarations), then font-variation-settings 'wght' 800 (Thai: font-weight 800) and color #A391FF. Thai titles rest at Prompt 200 (base) and step to Prompt 800 between 8% and 24% of the same cycle with a font-weight keyframe and steps(1, end) — the one sanctioned exception to 'no font-weight keyframes', because Prompt is static. Mixed titles (Latin digits + a Thai word, e.g. '40' + the Thai word for 'hours') run both keyframes on the title (animation-timing-function: ease-in-out, steps(1, end)) so the digits swell and the Thai steps. The swell runs only while the list is in view
- Landing stats: exactly 4 stats in one row on desktop (wrapping onto further rows on narrow screens), outlined numerals at clamp(2.6rem, 5.2vw, 4.6rem) Unbounded 800 with white-space: nowrap, the first one filled violet #A391FF with no stroke. Compact units attached to the number (k, %, +) stay part of the numeral at the same size; a space-separated word unit (e.g. 'min' in '14 min') is rendered as a separate 1rem DM Mono 500 off-white suffix with no stroke, so a value never wraps. Mobile profile counts are 1.6rem outlined numerals with a 2px stroke, the first one filled violet
- Only these may loop: the hero wave (or Thai colour sweep), the outlined marquee band, the logo marquee, and the feature-index swell while the list is in view. JS toggles .is-live on .hero and .index with an IntersectionObserver; while .index.is-live, pause the hero loop (:has(.index.is-live) .hero :is(.char, .word) { animation-play-state: running, paused }). Everything else plays once
- Every animation inside @media (prefers-reduced-motion: no-preference). JS may wrap words and graphemes in spans and add classes, but must never hide text: the initial hidden state is applied only under a .kt-ready class on <html> inside prefers-reduced-motion: no-preference, so without JS or with reduced motion all text is visible and static. Scripting: one small inline <script> (no libraries, no frameworks) for the Intl.Segmenter splitting, the marquee duplication and the IntersectionObservers is part of the style, even on a plain HTML + CSS stack

## Design tokens — use these exact values
Colours (dark mode):
  - background #0C0C0D
  - surface #151517
  - surface-2 #1D1D20
  - text #F4F1EA
  - text-muted #A29E97
  - primary #F4F1EA (text on it #0C0C0D)
  - secondary #A391FF (text on it #0C0C0D)
  - accent #FF6A45
  - border #2C2C31
  - success #4ADE9B · warning #FFC24B · danger #FF6B6B
  - chart series #F4F1EA, #A391FF, #FF6A45, #4ADE9B, #5D5D66
Radius: sm 8px, md 14px, lg 20px, xl 28px, pill 999px
Border width: 1px
Shadows: sm `none`, md `0 20px 50px rgba(0,0,0,0.35)`, lg `0 30px 80px rgba(0,0,0,0.45)` (where shadows reference var(--c-border) use the border colour)
Spacing: 8px base unit × 1.05 density
Motion: 700ms cubic-bezier(.16,1,.3,1)

## Typography (Latin + Thai)
- Headings: Unbounded 200/300/400/500/600/700/800/900 with Thai fallback Prompt 200/300/400/800
- Body: Manrope 400/500/700 with Thai fallback Anuphan 400/500/700
- Mono: DM Mono
- Google Fonts URL: https://fonts.googleapis.com/css2?family=Unbounded:wght@200;300;400;500;600;700;800;900&family=Manrope:wght@400;500;700&family=DM+Mono:wght@400;500&family=Prompt:wght@200;300;400;800&family=Anuphan:wght@400;500;700&display=swap
- CSS stacks: heading `"Unbounded", "Prompt", system-ui, sans-serif`; body `"Manrope", "Anuphan", system-ui, sans-serif`; mono `"DM Mono", ui-monospace, "SFMono-Regular", Menlo, "Anuphan", monospace` (the mono font has no Thai glyphs, so Thai inside mono labels falls back to Anuphan — drop uppercase and tracking there) — Latin font first, Thai font second, so each script gets its own face.
- Display size clamp(2.75rem, 7.4vw, 6.75rem), heading weight 800, heading tracking -0.03em (Latin only), uppercase small labels (Latin only).
- Thai rules: body line-height ≥ 1.7, headings ≥ 1.3; never apply negative letter-spacing or condensed tracking to Thai; do not fake-bold or italicise Thai; keep Thai text at least 1px larger than the equivalent Latin size when they sit side by side.
- Why this pairing: Prompt is a wide, loopless geometric Thai that spans hairline to black like Unbounded; Anuphan’s open, friendly geometry sits naturally next to Manrope for UI text.

## Components
- Fonts: Unbounded is variable on Google Fonts — the generated Google Fonts URL requests every Unbounded instance from 200 to 900 (200;300;400;500;600;700;800;900), which is equivalent to the range wght@200..900; either form works, so font-variation-settings has every weight. Thai Prompt is static, so request 200;300;400;800 explicitly
- Headlines: the hero headline uses the display size clamp(2.75rem, 7.4vw, 6.75rem), Unbounded 800, last word violet; section h2s clamp(2rem, 4.4vw, 3.6rem) weight 800. Hero headline, h1 and h2 line-height 1.02 for Latin; with lang=th, hero, h1 and h2 headings use line-height 1.3 (feature-index titles 1.4, article title 1.32, the outlined Thai band 1.45). Section h2s reveal word by word once when they scroll into view; h2s already in view at load reveal immediately, and if IntersectionObserver is unavailable add .kt-in to all of them at load
- Buttons: pill (999px), weight 700; primary is off-white with black text and turns violet (#A391FF, black text) on hover, its arrow icon slides 4px right; secondary is a 35%-opacity off-white outline that brightens to full off-white on hover. Pressed: every button :active transform scale(.97); primary :active background #DCD7CE with black text; secondary :active background rgba(244,241,234,.08); tabs, FAQ rows and nav links :active color #A391FF
- Logo row: text wordmarks only (6 names, Unbounded 600 1.4rem, off-white) each led by a small 20px outlined violet hexagon mark; no remote logos or images. It is the slow marquee described above
- Cards: #151517 with a 1px #2C2C31 border, 20px radius, no shadow; hover changes only the border to violet
- Inputs: dark surface, 1px border, 14px radius (pill in search bars and the CTA); focus = violet border plus a 4px violet ring at 22%
- Labels, eyebrows, nav links, KPI labels and table heads in DM Mono uppercase 0.72–0.78rem with 0.06–0.14em tracking; eyebrows are violet and start with a 6px coral ● dot. With lang=th, eyebrows, nav links, KPI labels, badges and table heads use Anuphan 500 at 0.85–0.9rem, untracked, not uppercase (eyebrows keep the 6px coral dot); Thai form labels are Anuphan 600 0.9rem
- Badges: transparent mono pills with a tone-coloured border; solid badge is coral with black text
- Charts: 3px off-white main line, violet comparison, pill-shaped bars in chart-series order (off-white, violet, coral, green, grey; Thai bar labels nowrap at 0.68rem), circular heatmap cells in violet, pill funnel steps
- Floating mobile tab bar: dark translucent pill, the active icon sits inside a violet pill
- Pull quote: large display text with no border or background. Outlined text is only for large type: Latin ≥2.5rem on web (the article pull quote is clamp(2.5rem, 3.6vw, 2.75rem)) and Thai ≥3rem (Prompt 800), both in the outlined recipe; the only smaller outlines are the Latin mobile ticker (2.1rem) and profile counts (1.6rem), which use a 2px stroke. Smaller Thai is filled: a Thai pull quote is violet #A391FF at line-height 1.5, and the Thai mobile ticker is muted #A29E97
- Glows (the only gradients): the page glow radial-gradient(60% 40% at 50% 0, rgba(163,145,255,.18), transparent 70%) over #0C0C0D sits on a position: fixed; inset: 0; z-index: -1 background layer behind the page, so it stays pinned at the top of the viewport while scrolling (equivalent to background-attachment: fixed on body); CTA card glow radial-gradient(70% 60% at 50% 120%, rgba(163,145,255,.28), transparent 70%) over the #151517 card surface
- Showcase section (static, no loop): two columns — left: coral-dot eyebrow, h2 that reveals word by word once, muted lead and 3 check bullets; right: one flat #151517 card with tabs and a calm product preview (dark cards with a 3px line chart and big Unbounded numerals). No device frame, no screenshot image, no parallax
- Form, article and component-sheet titles get a one-time letter reveal (static under reduced motion) so every page has one kinetic moment

## Layout for this web page
Marketing landing page: top navigation, hero with headline/sub-headline/two CTAs and a product visual, then the content sections, and a footer.
Hero: full-viewport immersive hero with centred copy over the background effect. Navigation: minimal style. Sections in order: hero → logos → features → stats → showcase → testimonials → faq → cta. Feature section as list.
Use a full-viewport immersive, centred hero with no product shot or product visual — this overrides any generic 'product visual' in the page layout (min-height: min(100svh, 920px)): eyebrow, a headline at the display size clamp(2.75rem, 7.4vw, 6.75rem) whose last word is violet, a short lead and two pill buttons, all on the dark stage with a faint violet glow at the top. Follow it immediately with the outlined marquee band and the wordmark marquee row, then section heads that reveal word by word once when they scroll into view, the 6-row feature index, exactly 4 nowrap outlined stat numerals, the static showcase (copy + bullets beside a tabbed product-preview card), testimonials, an FAQ, and a CTA card with a violet glow rising from below. 'One headline at a time' means one reveal at a time: section heads reveal once on scroll while the hero wave keeps running (until the index is in view). Dashboards stay calm: the page title reveals once, KPI values are big Unbounded numerals (first in violet), widgets are flat dark cards.

## Content
Use realistic product copy (fictional brand) suited to: Launch and campaign pages. Make the UI bilingual: Thai and English, with a visible TH/EN switch that swaps every string. Set the lang attribute to match.

## Do
- Reveal one headline at a time (32ms stagger per letter — per word in Thai — and 900ms per unit on cubic-bezier(.16,1,.3,1)); only the hero wave, the two marquees and the feature-index swell (while in view, with the hero wave paused) loop
- Use a true variable font and animate font-variation-settings (hero wave wght 800 ↔ 220, index swell 300 → 800), not font-weight keyframes (Latin only; the Thai fallback Prompt is static, so Thai hero headlines get a violet colour sweep instead and Thai index titles step Prompt 200 → 800)
- Wrap every animation in prefers-reduced-motion: no-preference and keep the static state fully legible
- Split Thai by word, never by character, so vowels and tone marks stay attached

## Don't
- Don’t animate body text or anything users must read to act
- Don’t run more than two marquees per screen (one outlined band, one slower logo row)
- Don’t clip letters with overflow:hidden reveals — it cuts off Thai tone marks
- Don’t carry the tight 1.02 headline line-height into Thai: it is for Latin only; with lang=th, hero, h1 and h2 use 1.3 and feature-index titles 1.4 so tone marks and upper vowels never collide
- Animating body copy, form labels or anything users must read to act
- Splitting Thai text into characters or using overflow:hidden reveal masks that clip tone marks and upper vowels
- Acid neon colours, harsh borders or glitch cuts (that is Kinetic Brutalism)
- Animating cards, sections and parallax everywhere — the motion belongs to the letters only
- Font-weight keyframes on the variable Latin face (use font-variation-settings) and any smooth weight wave on the static Thai hero (use the colour sweep); the stepped Thai Prompt 200 → 800 index swell is the only font-weight keyframe
- Transparent-fill outlines or tracked outlined letters on Unbounded (stray inner contours show); always fill with the stage colour and use paint-order: stroke fill
- A third marquee, or hiding text by default so it stays invisible without JS
- Shipping without a prefers-reduced-motion static state

## Stack notes
One self-contained HTML file with a <style> block. Declare all tokens as CSS custom properties on :root and build every component from them. No frameworks.

## Acceptance checklist
- Someone who knows the "Kinetic Typography" style recognises it at a glance from the signature details.
- Every colour, radius, border and shadow comes from the tokens above.
- Thai and English text both render in their intended fonts with comfortable line-height.
- Text contrast meets WCAG AA (4.5:1 body, 3:1 large text) unless the style explicitly trades this off.
- Interactive elements have hover, focus-visible and pressed states in the style's language.
```
