Skip to content
Brok UI

Loading…

No results

Text Effects

Open source

Animated and decorative text treatments — staged reveals, typing, particle and cursor-repel effects, and clipped gradients.

Version
v1.1.2
Stability
stable
License
MIT
Related
Typewriter
Kinetic Text Reveal
Letter Cascade
Particle Typography
Text Repel
Text Gradient
Circular Text
Encrypted Text
1 of 9

Text Animate — A wrapper that reveals its text with a chosen animation (fade-up, fade, blur-in, slide-up, slide-left, slide-right, scale-in) when scrolled into view, optionally split and staggered per word or character, and able to re-trigger on every re-entry (once=false).

Preview

previews.components.text-animate.default.blade.php Blade
<x-ui.text-animate effect="fade-up" split-by="word" class="text-2xl font-semibold text-foreground">Animate your text on scroll</x-ui.text-animate>

Installation

terminal
php artisan ui:add text-animate

Note

This component ships an Alpine behavior module at resources/js/ui/text-animate.js. Import it once from your bundle so it registers on alpine:init:

resources/js/ui/index.js JS
import './text-animate.js';

Registry contract

php artisan ui:add text-animate writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.

  • blade resources/views/components/ui/text-animate.blade.php
  • js resources/js/ui/text-animate.js
Registry dependencies
None — installs on its own.
Packages
composer: jml/brok:^0.2
npm: alpinejs

Use with AI

A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.

text-animate.md
# Brok UI: Text Animate (`text-animate`)

A wrapper that reveals its text with a chosen animation (fade-up, fade, blur-in, slide-up, slide-left, slide-right, scale-in) when scrolled into view, optionally split and staggered per word or character, and able to re-trigger on every re-entry (once=false).

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add text-animate
```

## Usage

```blade
<x-ui.text-animate effect="fade-up" split-by="word" class="text-2xl font-semibold text-foreground">Animate your text on scroll</x-ui.text-animate>
```

## Props

- `effect` (string, default `fade-up`) — Reveal animation: fade-up, fade, blur-in, slide-up, slide-left, slide-right, or scale-in.
- `splitBy` (string, default `none`) — Segmentation for the stagger: none (whole block animates together), word, or char.
- `stagger` (int, default `60`) — Milliseconds between each segment's reveal when splitBy is word or char.
- `delay` (int, default `0`) — Milliseconds before the reveal starts.
- `as` (string, default `div`) — Root tag: div, span, p, or h1 through h6.
- `once` (bool, default `true`) — Plays the reveal only the first time it scrolls into view; false re-triggers it on every entry.

## Use when

- Use only when visual expression supports storytelling, brand differentiation, or a clear showcase moment.
- Revealing a heading or block of text with a chosen scroll-triggered animation (fade-up, fade, blur-in, slide-up, slide-left, slide-right, scale-in).
- Wanting the reveal split and staggered per word or character, and optionally re-triggered on every scroll re-entry (once=false).

## Avoid when

- Do not place heavy effects in task-dense workflows where clarity, speed, and readability matter more.
- Wanting a fixed directional slide with more stagger-origin control (start/end/center/edges/random); use kinetic-text-reveal instead.
- Needing a scramble/decrypt look instead of a fade or slide reveal; use encrypted-text instead.

## Anti-patterns

- Heavy effects in forms or task-dense workflows
- Motion that interferes with reading or focus

## Rules

- Use the `<brok:text-animate>` tag (or `<x-ui.text-animate>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.

## Links

- Docs: https://brokui.dev/docs/components/text-animate
- Registry JSON (files, props, contract): https://brokui.dev/r/open/text-animate.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
effect string fade-up Reveal animation: fade-up, fade, blur-in, slide-up, slide-left, slide-right, or scale-in.
splitBy string none Segmentation for the stagger: none (whole block animates together), word, or char.
stagger int 60 Milliseconds between each segment's reveal when splitBy is word or char.
delay int 0 Milliseconds before the reveal starts.
as string div Root tag: div, span, p, or h1 through h6.
once bool true Plays the reveal only the first time it scrolls into view; false re-triggers it on every entry.

Slots

  • default — The text to reveal.

Renders as div, span, p, h1, h2, h3, h4, h5, h6.

Data slots

Stable hooks for CSS overrides and browser tests.

text-animate

Behavior

  • text-animate.js splits the text into per-character or per-word spans (or leaves it whole) and reveals them with the chosen keyframe effect when the element scrolls into view.
  • once=false replays the reveal animation every time the element re-enters the viewport, instead of only on first scroll-in.
  • Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
  • Declares registry capability flags: a11y, interactive, responsive, rtl, darkMode, localized, reducedMotion.

Guidance

Expressive and showcase effect

Support brand expression or a showcase moment.

Use when

  • Use only when visual expression supports storytelling, brand differentiation, or a clear showcase moment.
  • Revealing a heading or block of text with a chosen scroll-triggered animation (fade-up, fade, blur-in, slide-up, slide-left, slide-right, scale-in).
  • Wanting the reveal split and staggered per word or character, and optionally re-triggered on every scroll re-entry (once=false).

Avoid when

  • Do not place heavy effects in task-dense workflows where clarity, speed, and readability matter more.
  • Wanting a fixed directional slide with more stagger-origin control (start/end/center/edges/random); use kinetic-text-reveal instead.
  • Needing a scramble/decrypt look instead of a fade or slide reveal; use encrypted-text instead.

Use instead

  • Static content-first presentation

Anti-patterns

  • Heavy effects in forms or task-dense workflows
  • Motion that interferes with reading or focus
Anatomy
root
Theming hooks
text-animate

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
managed
Focus
none
  • The root carries role="img" and an aria-label of the resolved text, so screen readers get the whole phrase as one sentence regardless of how it is split for animation.
  • Splitting the text into spans is purely visual; the accessible name always reflects the complete, unsplit string.
  • Semantic HTML and a stable data-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, so keyboard focus is always visible.
  • Disabled and invalid states are conveyed to assistive tech, not by color alone.
  • Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
  • Labels go through __() and layout uses logical properties (ms-*, text-start), so it mirrors under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Needs wire:ignore

Add wire:ignore to the component root because its behavior owns rendered DOM.

livewire-component.blade.php Blade
<div wire:ignore>
    <x-ui.text-animate effect="fade-up" split-by="word" class="text-2xl font-semibold text-foreground">Animate your text on scroll</x-ui.text-animate>
</div>

Source

The exact, editable files ui:add writes into your app. Previews render this same code; there are no preview-only components.

resources/views/components/ui/text-animate.blade.php Blade
@props([
    'effect' => 'fade-up',
    'splitBy' => 'none',
    'stagger' => 60,
    'delay' => 0,
    'as' => 'div',
    'once' => true,
])

@php
    // Whitelist the effect so the keyframe name (ui-text-<effect>) is always valid.
    $allowedEffects = ['fade-up', 'fade', 'blur-in', 'slide-up', 'slide-left', 'slide-right', 'scale-in'];
    $effect = in_array($effect, $allowedEffects, true) ? $effect : 'fade-up';

    $allowedSplit = ['none', 'word', 'char'];
    $splitBy = in_array($splitBy, $allowedSplit, true) ? $splitBy : 'none';

    // Restrict the root tag to a safe, known set of inline/block containers.
    $allowedTags = ['div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
    $tag = in_array($as, $allowedTags, true) ? $as : 'div';

    $text = trim((string) ($slot ?? ''));

    $config = [
        'effect' => $effect,
        'splitBy' => $splitBy,
        'once' => (bool) $once,
    ];
@endphp

<{{ $tag }}
    data-slot="text-animate"
    x-data="uiTextAnimate(@js($config))"
    {{-- The behaviour layer splits the text into per-character spans; role=img +
         aria-label keeps it announced as one phrase and makes the label valid on
         this otherwise-generic element (a11y: aria-prohibited-attr). --}}
    role="img"
    aria-label="{{ $text }}"
    style="--delay: {{ (int) $delay }}; --stagger: {{ (int) $stagger }}"
    {{ $attributes->merge(['class' => 'text-foreground']) }}
>{{ $text }}</{{ $tag }}>
resources/js/ui/text-animate.js JS
/**
 * Text Animate behavior.
 *
 * Reveals a block of text with a chosen CSS keyframe (ui-text-<effect>) once it
 * scrolls into view (via IntersectionObserver, threshold ~0.2). The text can be
 * split into one <span> per word or per character; each token carries an `--i`
 * index so the central keyframe can stagger its reveal via animation-delay
 * `calc((var(--delay) + var(--i) * var(--stagger)) * 1ms)`.
 *
 * The reveal itself is CSS-driven: this module only toggles a `data-shown`
 * attribute that the stylesheet keys off. Each token span is aria-hidden and
 * the root carries an aria-label with the full original text, so screen readers
 * read the phrase exactly once. Reduced-motion is handled entirely in CSS — the
 * `motion-safe`/`no-preference` gate means tokens are only hidden when motion is
 * allowed, so reduced-motion users always see fully visible text.
 *
 * Self-registers on `alpine:init` so import order does not matter.
 */
document.addEventListener('alpine:init', () => {
    window.Alpine.data('uiTextAnimate', (config = {}) => ({
        effect: config.effect ?? 'fade-up',
        splitBy: config.splitBy ?? 'none',
        once: config.once ?? true,

        shown: false,
        _observer: null,

        init() {
            this.split();

            this._observer = new IntersectionObserver((entries) => {
                entries.forEach((entry) => {
                    if (entry.isIntersecting) {
                        this.shown = true;
                        this.$el.setAttribute('data-shown', '');
                        if (this.once) {
                            this._observer.disconnect();
                            this._observer = null;
                        }
                    } else if (!this.once) {
                        this.shown = false;
                        this.$el.removeAttribute('data-shown');
                    }
                });
            }, { threshold: 0.2 });

            this._observer.observe(this.$el);
        },

        // Capture the original text and rebuild it as token spans. Each token
        // gets `--i` (its stagger index) and is aria-hidden; spaces are kept as
        // their own (non-animated) text so wrapping/spacing is preserved.
        split() {
            const text = this.$el.textContent;
            const effectClass = 'ui-text-token--' + this.effect;

            // splitBy === 'none' → one span around the whole phrase.
            if (this.splitBy === 'none') {
                this.$el.textContent = '';
                this.$el.appendChild(this.token(text, 0, effectClass));
                return;
            }

            // Split into words (keeping the spaces) or individual characters.
            const tokens = this.splitBy === 'char'
                ? Array.from(text)
                : text.match(/\S+|\s+/g) || [];

            this.$el.textContent = '';
            let i = 0;
            tokens.forEach((tok) => {
                // Whitespace stays as a plain text node so it never animates and
                // never consumes a stagger index.
                if (/^\s+$/.test(tok)) {
                    this.$el.appendChild(document.createTextNode(tok));
                    return;
                }
                this.$el.appendChild(this.token(tok, i, effectClass));
                i += 1;
            });
        },

        token(content, index, effectClass) {
            const span = document.createElement('span');
            span.className = 'ui-text-token ' + effectClass;
            span.setAttribute('aria-hidden', 'true');
            span.style.setProperty('--i', String(index));
            // char/word tokens are inline-block so transforms (translateY/scale)
            // apply without affecting line layout.
            if (this.splitBy !== 'none') {
                span.style.display = 'inline-block';
            }
            span.textContent = content;
            return span;
        },

        destroy() {
            if (this._observer) {
                this._observer.disconnect();
                this._observer = null;
            }
        },
    }));
});

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.1.2
Status
Stable
License
open
Accessibility reviewed
No review date recorded
Last breaking change
No date recorded
Deprecation
Not deprecated
Contract
v6
Foundation
≥ 1.0.0