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.0.2
Stability
stable
License
MIT
Related
Text Animate
Typewriter
Letter Cascade
Particle Typography
Text Repel
Text Gradient
Circular Text
Encrypted Text
3 of 9

Kinetic Text Reveal — Splits text into word, character or line segments and reveals each with directional motion, a soft blur and configurable stagger; plays on mount or when scrolled into view.

Preview

previews.components.kinetic-text-reveal.default.blade.php Blade
<x-ui.kinetic-text-reveal text="Reveal in motion" by="word" class="text-3xl font-semibold text-foreground" />

Installation

terminal
php artisan ui:add kinetic-text-reveal

Note

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

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

Registry contract

php artisan ui:add kinetic-text-reveal 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/kinetic-text-reveal.blade.php
  • js resources/js/ui/kinetic-text-reveal.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.

kinetic-text-reveal.md
# Brok UI: Kinetic Text Reveal (`kinetic-text-reveal`)

Splits text into word, character or line segments and reveals each with directional motion, a soft blur and configurable stagger; plays on mount or when scrolled into view.

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

## Install

```bash
php artisan ui:add kinetic-text-reveal
```

## Usage

```blade
<x-ui.kinetic-text-reveal text="Reveal in motion" by="word" class="text-3xl font-semibold text-foreground" />
```

## Props

- `text` (string, default ``) — The text to reveal; falls back to the default slot when empty.
- `by` (string, default `word`) — Segmentation unit: word, char, or line.
- `direction` (string, default `up`) — Slide-in direction: up, down, left, or right.
- `stagger` (int, default `60`) — Milliseconds between each segment's reveal.
- `staggerFrom` (string, default `start`) — Stagger origin along the segment sequence: start, end, center, edges, or random.
- `delay` (int, default `0`) — Milliseconds before the first segment starts.
- `as` (string, default `div`) — Root tag: div, span, p, or h1 through h6.
- `autoPlay` (bool, default `true`) — Plays the reveal automatically; false requires a manual trigger from the consuming code.

## Use when

- Use only when visual expression supports storytelling, brand differentiation, or a clear showcase moment.
- Revealing a heading or short phrase with directional, staggered motion split by word, character, or line.
- Wanting the reveal to play on mount or when scrolled into view, with a configurable stagger origin (start, end, center, edges, random).

## Avoid when

- Do not place heavy effects in task-dense workflows where clarity, speed, and readability matter more.
- Revealing long paragraphs of body copy; the segmented reveal is best for short headlines.
- Needing a scramble/decrypt look instead of directional slide-and-blur motion; 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:kinetic-text-reveal>` tag (or `<x-ui.kinetic-text-reveal>`) 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/kinetic-text-reveal
- Registry JSON (files, props, contract): https://brokui.dev/r/open/kinetic-text-reveal.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
text string The text to reveal; falls back to the default slot when empty.
by string word Segmentation unit: word, char, or line.
direction string up Slide-in direction: up, down, left, or right.
stagger int 60 Milliseconds between each segment's reveal.
staggerFrom string start Stagger origin along the segment sequence: start, end, center, edges, or random.
delay int 0 Milliseconds before the first segment starts.
as string div Root tag: div, span, p, or h1 through h6.
autoPlay bool true Plays the reveal automatically; false requires a manual trigger from the consuming code.

Slots

  • default — Fallback text source when the text prop is empty.

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

Data slots

Stable hooks for CSS overrides and browser tests.

kinetic-text-reveal

Behavior

  • The real text is split into word, character, or line segments that slide in from the chosen direction with a soft blur and a per-segment stagger delay.
  • kinetic-text-reveal.js drives the play state; autoPlay controls whether the reveal starts automatically.
  • 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 short phrase with directional, staggered motion split by word, character, or line.
  • Wanting the reveal to play on mount or when scrolled into view, with a configurable stagger origin (start, end, center, edges, random).

Avoid when

  • Do not place heavy effects in task-dense workflows where clarity, speed, and readability matter more.
  • Revealing long paragraphs of body copy; the segmented reveal is best for short headlines.
  • Needing a scramble/decrypt look instead of directional slide-and-blur motion; 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
kinetic-text-reveal

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
End
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 visually segmented.
  • The direction, stagger origin, and unit values are validated against a fixed allow-list, so the rendered keyframe name is never user-injected.
  • 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.kinetic-text-reveal text="Reveal in motion" by="word" class="text-3xl font-semibold text-foreground" />
</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/kinetic-text-reveal.blade.php Blade
@props([
    'text' => '',
    'by' => 'word',
    'direction' => 'up',
    'stagger' => 60,
    'staggerFrom' => 'start',
    'delay' => 0,
    'as' => 'div',
    'autoPlay' => true,
])

@php
    // How the phrase is split into reveal segments.
    $allowedBy = ['word', 'char', 'line'];
    $by = in_array($by, $allowedBy, true) ? $by : 'word';

    // Slide-in direction → keyframe name (ui-kinetic-<direction>). Whitelisted so
    // the keyframe is always valid and never user-injected.
    $allowedDir = ['up', 'down', 'left', 'right'];
    $direction = in_array($direction, $allowedDir, true) ? $direction : 'up';

    // Where the stagger origin sits along the segment sequence.
    $allowedFrom = ['start', 'end', 'center', 'edges', 'random'];
    $staggerFrom = in_array($staggerFrom, $allowedFrom, true) ? $staggerFrom : 'start';

    // 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 prop wins; otherwise fall back to the default slot.
    $resolved = trim((string) ($text !== '' ? $text : ($slot ?? '')));

    $config = [
        'by' => $by,
        'direction' => $direction,
        'staggerFrom' => $staggerFrom,
        'autoPlay' => (bool) $autoPlay,
    ];
@endphp

<{{ $tag }}
    data-slot="kinetic-text-reveal"
    data-direction="{{ $direction }}"
    x-data="uiKineticTextReveal(@js($config))"
    role="img"
    aria-label="{{ $resolved }}"
    style="--delay: {{ (int) $delay }}; --stagger: {{ (int) $stagger }}"
    {{ $attributes->merge(['class' => 'text-foreground']) }}
>{{ $resolved }}</{{ $tag }}>
resources/js/ui/kinetic-text-reveal.js JS
/**
 * Kinetic Text Reveal behavior.
 *
 * Splits a block of text into segments (one <span> per word, character or line)
 * and reveals each with a directional slide + soft blur (8px → 0) + fade, with a
 * configurable per-segment stagger. The reveal itself is CSS-driven: this module
 * only splits the text, assigns each segment an `--i` stagger index (computed
 * from `staggerFrom`: start | end | center | edges | random), and toggles a
 * `data-shown` attribute that the stylesheet keys off. The directional keyframe
 * (ui-kinetic-up/down/left/right) is selected via the root's `data-direction`.
 *
 * The animation-delay is `calc((var(--delay) + var(--i) * var(--stagger)) * 1ms)`
 * so segments fan out from the chosen origin. Reveal plays on mount by default
 * (autoPlay) once the element is at least ~20% in view (IntersectionObserver),
 * mirroring text-animate.
 *
 * a11y: each segment 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` gate means
 * segments are only hidden when motion is allowed, so reduced-motion users
 * always see fully visible static text.
 *
 * Self-registers on `alpine:init` so import order does not matter.
 */
document.addEventListener('alpine:init', () => {
    window.Alpine.data('uiKineticTextReveal', (config = {}) => ({
        by: config.by ?? 'word',
        direction: config.direction ?? 'up',
        staggerFrom: config.staggerFrom ?? 'start',
        autoPlay: config.autoPlay ?? true,

        shown: false,
        _observer: null,

        init() {
            this.split();

            if (!this.autoPlay) {
                // Not auto-playing: reveal immediately and statically.
                this.shown = true;
                this.$el.setAttribute('data-shown', '');
                return;
            }

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

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

        // Capture the original text and rebuild it as segment spans. Each segment
        // gets `--i` (its stagger index, derived from staggerFrom) and is
        // aria-hidden; whitespace between word segments is kept as plain text so
        // wrapping/spacing is preserved and never consumes a stagger index.
        split() {
            const text = this.$el.textContent;

            // Build the raw list of (segment, isWhitespace) entries.
            let entries;
            if (this.by === 'char') {
                entries = Array.from(text).map((c) => ({ content: c, ws: /^\s+$/.test(c) }));
            } else if (this.by === 'line') {
                entries = text.split(/(\n)/).map((c) => ({ content: c, ws: c === '\n' }));
            } else {
                // word: keep runs of whitespace as their own (non-animated) chunk.
                entries = (text.match(/\S+|\s+/g) || []).map((c) => ({ content: c, ws: /^\s+$/.test(c) }));
            }

            // Count the visible (animated) segments to compute stagger indices.
            const total = entries.filter((e) => !e.ws).length;
            const order = this.order(total);

            this.$el.textContent = '';
            let seq = 0;
            entries.forEach((entry) => {
                if (entry.ws) {
                    this.$el.appendChild(document.createTextNode(entry.content));
                    return;
                }
                this.$el.appendChild(this.segment(entry.content, order[seq]));
                seq += 1;
            });
        },

        // Map sequence position (0..total-1) → stagger index per staggerFrom.
        // Returns an array `order` where order[seq] is the `--i` for that segment.
        order(total) {
            const out = new Array(total);
            for (let seq = 0; seq < total; seq++) {
                out[seq] = this.indexFor(seq, total);
            }
            // random: assign a shuffled permutation of 0..total-1.
            if (this.staggerFrom === 'random') {
                const idx = Array.from({ length: total }, (_, i) => i);
                for (let i = idx.length - 1; i > 0; i--) {
                    const j = Math.floor(Math.random() * (i + 1));
                    [idx[i], idx[j]] = [idx[j], idx[i]];
                }
                return idx;
            }
            return out;
        },

        indexFor(seq, total) {
            switch (this.staggerFrom) {
                case 'end':
                    return total - 1 - seq;
                case 'center':
                    return Math.abs(seq - (total - 1) / 2);
                case 'edges':
                    return Math.min(seq, total - 1 - seq);
                case 'start':
                default:
                    return seq;
            }
        },

        segment(content, index) {
            const span = document.createElement('span');
            span.className = 'ui-kinetic-seg';
            span.setAttribute('aria-hidden', 'true');
            span.style.setProperty('--i', String(index));
            // inline-block so translate/blur apply without breaking line layout.
            span.style.display = 'inline-block';
            // line segments behave as blocks so each occupies its own row.
            if (this.by === 'line') {
                span.style.display = '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.0.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