← All components

Content · 11 of 13

TypingEffect

The two-line display heading. Its second line is one of the few sanctioned uses of #5F20FE on text — legitimate only because it runs at 32–56px, where the 3:1 large-text floor applies.

Vue island src/components/common/TypingEffect.vue rendered live from compiled/TypingEffect.js GSAP SplitText

Props

PropTypeNotes
text1 *stringFirst line, --color-heading.
text2 *stringSecond line, --color-action.

Stories

Live — this is the real component and the real GSAP SplitText (3.15, vendored). On mount, once document.fonts.ready resolves, it breaks the text into characters and staggers them from opacity: 0.5 at 0.1s intervals. Reload to replay.

Default text1="We build software" text2="that survives…" 2rem base, stepping to 3.5rem at the lg breakpoint. Bold, 130% leading.

Findings

The selector is global, so multiple instances collide. SplitText.create('.split-text span', …) queries the whole document, not the component root. A second TypingEffect on the same page causes each mount to re-split every instance's spans — including ones already split — and the later mount animates characters belonging to the earlier one. The fix is a template ref scoped to this component.

No reduced-motion guard. The stagger runs unconditionally from onMounted and again on every prop change. It is GSAP rather than CSS, and the current site ships no prefers-reduced-motion handling at all — this is the site's largest piece of moving text, so it is the animation most worth guarding.

gsap.registerPlugin runs on every call. It sits inside splitAnimation, so it re-registers on mount and on each watch firing rather than once at module scope. Harmless but repeated, and it makes the watch path heavier than it needs to be.

Waiting on document.fonts.ready is the right call. Splitting before Satoshi loads would measure fallback glyphs and produce character boxes that jump when the real font arrives. The component defers until fonts settle — worth preserving in any rewrite.

The heading is an <h2> with two block spans. Both lines live inside one heading element, so the accessible name is the full sentence — correct. The :key="text1" forces a fresh node when the copy changes, which is what makes the watch re-split work.