Skip to content

Slot text

Slot text renders a single <span> that animates with a tactile per-character "roll": each character sits in its own clipped cell, and whenever the text changes the old glyph slides out while the new one chases it in with a springy overshoot. It is ideal for short labels, statuses, numbers and commands, such as the classic Copy → Copied → Copy button, a live status word or a changing counter.

Delightful

TIP

The roll respects prefers-reduced-motion: when reduced motion is requested the text simply swaps instead of rolling. The current text is always exposed as the accessible label, so screen readers read the value rather than the individual glyph cells.

The component also exposes two imperative methods through a template ref: set(text, options?) rolls to new text permanently, and flash(text, options?) rolls to temporary text and automatically rolls back, the spam-safe Copy → Copied → Copy pattern in one call.

Props

text: string
The text that is displayed. Whenever this value changes, the label rolls each character to its new glyph.

bounce?: number
Per-letter personality, between 0 and 1. 0 lands every glyph identically, 1 adds individual variation in speed and a little tilt-wobble as each settles.
Default: 0.6

chromatic?: boolean
Rolls every incoming glyph in with its own hue for a rainbow sweep across the line, then fades back to the resting color. Overrides color.

color?: string
A CSS color the incoming glyphs are tinted with as they roll in, fading back to the resting color once they land.

color-fade?: number
How long the tint takes to fade back to the resting color, in milliseconds.
Default: 280

direction?: "up" | "down"
The roll direction. down lets glyphs enter from the top, up from the bottom.
Default: down

duration?: number
The slide duration per character, in milliseconds.
Default: 300

easing?: string
The CSS easing function used for the roll. Defaults to a springy, overshooting curve.
Default: cubic-bezier(0.34, 1.56, 0.64, 1)

exit-offset?: number
How long the incoming glyph trails the outgoing one, in milliseconds.
Default: 50

interrupt?: boolean
When true, a new roll interrupts any roll in flight and starts fresh. When false, the current roll finishes and the latest request plays after it lands, dropping duplicates, ideal for spam-prone triggers.
Default: true

skip-unchanged?: boolean
Keeps characters that are identical at the same index static. Turn this off when the two strings are misaligned so the whole line rolls uniformly.
Default: true

stagger?: number
The delay between characters, in milliseconds.
Default: 45

Examples

Basic

Bind a reactive value to text and the label rolls every time it changes.

Hello

<template>
    <FluxFlex
        align="start"
        direction="vertical"
        :gap="12">
        <FluxVisualSlotText
            :text="greetings[index]"
            style="font-size: 24px; font-weight: 600;"/>

        <FluxSecondaryButton
            label="Next greeting"
            @click="next"/>
    </FluxFlex>
</template>

<script
    lang="ts"
    setup>
    import { FluxFlex, FluxSecondaryButton } from '@flux-ui/components';
    import { FluxVisualSlotText } from '@flux-ui/visuals';
    import { ref } from 'vue';

    const greetings = ['Hello', 'Bonjour', 'Hola', 'Ciao', 'Hallo', 'Olá'];
    const index = ref(0);

    function next(): void {
        index.value = (index.value + 1) % greetings.length;
    }
</script>

Counter

Roll digits up or down to animate a counter, a quantity or a live metric. Tabular numbers keep the value from shifting horizontally.

42

<template>
    <FluxFlex
        align="center"
        direction="horizontal"
        :gap="15">
        <FluxSecondaryButton
            icon-leading="minus"
            @click="step(-1)"/>

        <FluxVisualSlotText
            :direction="direction"
            :text="`${count}`"
            style="min-width: 48px; font-size: 33px; font-weight: 700; justify-content: center;"/>

        <FluxSecondaryButton
            icon-leading="plus"
            @click="step(1)"/>
    </FluxFlex>
</template>

<script
    lang="ts"
    setup>
    import { FluxFlex, FluxSecondaryButton } from '@flux-ui/components';
    import { FluxVisualSlotText } from '@flux-ui/visuals';
    import { ref } from 'vue';

    const count = ref(42);
    const direction = ref<'up' | 'down'>('up');

    function step(delta: number): void {
        direction.value = delta > 0 ? 'up' : 'down';
        count.value += delta;
    }
</script>

Metric

A live KPI tile: the value streams in and rolls every time the data updates, so a dashboard stat feels alive without redrawing the card.

Active users12,483

<template>
    <FluxPane style="width: 261px;">
        <FluxPaneBody>
            <FluxFlex
                align="start"
                direction="vertical"
                :gap="3">
                <span style="color: var(--gray-500); font-size: 13px; font-weight: 500;">Active users</span>

                <FluxVisualSlotText
                    direction="up"
                    :text="value"
                    style="font-size: 33px; font-weight: 700; font-variant-numeric: tabular-nums;"/>
            </FluxFlex>
        </FluxPaneBody>
    </FluxPane>
</template>

<script
    lang="ts"
    setup>
    import { FluxFlex, FluxPane, FluxPaneBody } from '@flux-ui/components';
    import { FluxVisualSlotText } from '@flux-ui/visuals';
    import { onBeforeUnmount, onMounted, ref } from 'vue';

    const count = ref(12483);
    const value = ref(count.value.toLocaleString('en-US'));

    let timer: number;

    function tick(): void {
        count.value += Math.floor(Math.random() * 25) - 8;
        value.value = count.value.toLocaleString('en-US');
    }

    onMounted(() => {
        timer = window.setInterval(tick, 2000);
    });

    onBeforeUnmount(() => window.clearInterval(timer));
</script>

Trend

A delta indicator that rolls up and tints green when the reading rises, and rolls down and tints red when it falls.

Revenue MoM+4.2%

<template>
    <FluxFlex
        align="start"
        direction="vertical"
        :gap="15">
        <FluxFlex
            align="baseline"
            direction="horizontal"
            :gap="9">
            <span style="color: var(--gray-500); font-size: 13px; font-weight: 500;">Revenue MoM</span>

            <FluxVisualSlotText
                :color="trend.color"
                :direction="trend.direction"
                :text="trend.label"
                style="font-size: 21px; font-weight: 700; font-variant-numeric: tabular-nums;"/>
        </FluxFlex>

        <FluxSecondaryButton
            icon-leading="rotate"
            label="New reading"
            @click="next"/>
    </FluxFlex>
</template>

<script
    lang="ts"
    setup>
    import { FluxFlex, FluxSecondaryButton } from '@flux-ui/components';
    import { FluxVisualSlotText } from '@flux-ui/visuals';
    import { computed, ref } from 'vue';

    const readings = [4.2, -1.1, 8.7, -3.4, 2.6, -0.8];
    const index = ref(0);
    const current = computed(() => readings[index.value]);

    const trend = computed(() => {
        const up = current.value >= 0;

        return {
            label: `${up ? '+' : ''}${current.value.toFixed(1)}%`,
            color: up ? 'var(--success-500)' : 'var(--danger-500)',
            direction: (up ? 'up' : 'down') as 'up' | 'down'
        };
    });

    function next(): void {
        index.value = (index.value + 1) % readings.length;
    }
</script>

Status

Tint each new status as it rolls in with the color prop, fading back to the resting color, ideal for job, order or deployment states.

Queued

<template>
    <FluxFlex
        align="start"
        direction="vertical"
        :gap="15">
        <FluxVisualSlotText
            :color="current.color"
            direction="up"
            :text="current.label"
            style="font-size: 24px; font-weight: 600;"/>

        <FluxSecondaryButton
            icon-leading="rotate"
            label="Advance status"
            @click="advance"/>
    </FluxFlex>
</template>

<script
    lang="ts"
    setup>
    import { FluxFlex, FluxSecondaryButton } from '@flux-ui/components';
    import { FluxVisualSlotText } from '@flux-ui/visuals';
    import { computed, ref } from 'vue';

    const statuses = [
        {label: 'Queued', color: 'var(--gray-500)'},
        {label: 'Running', color: 'var(--info-500)'},
        {label: 'Succeeded', color: 'var(--success-500)'},
        {label: 'Failed', color: 'var(--danger-500)'}
    ];

    const index = ref(0);
    const current = computed(() => statuses[index.value]);

    function advance(): void {
        index.value = (index.value + 1) % statuses.length;
    }
</script>

Clock

Frequent updates roll cleanly: only the digits that actually change re-roll, so a ticking clock animates just its seconds.

00:00:00

<template>
    <FluxVisualSlotText
        direction="up"
        :text="time"
        style="font-size: 39px; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: 0.02em;"/>
</template>

<script
    lang="ts"
    setup>
    import { FluxVisualSlotText } from '@flux-ui/visuals';
    import { onBeforeUnmount, onMounted, ref } from 'vue';

    const time = ref('00:00:00');

    let timer: number;

    function tick(): void {
        time.value = new Date().toLocaleTimeString('en-US', {hour12: false});
    }

    onMounted(() => {
        tick();
        timer = window.setInterval(tick, 1000);
    });

    onBeforeUnmount(() => window.clearInterval(timer));
</script>

Flash

Call flash() through a template ref to roll to temporary text that automatically rolls back, ideal for copy buttons.

<template>
    <FluxSecondaryButton
        icon-leading="copy"
        @click="copy">
        <template #label>
            <FluxVisualSlotText
                ref="label"
                text="Copy"/>
        </template>
    </FluxSecondaryButton>
</template>

<script
    lang="ts"
    setup>
    import { FluxSecondaryButton } from '@flux-ui/components';
    import { FluxVisualSlotText } from '@flux-ui/visuals';
    import { useTemplateRef } from 'vue';

    const labelRef = useTemplateRef<InstanceType<typeof FluxVisualSlotText>>('label');

    function copy(): void {
        labelRef.value?.flash('Copied', {enter: {direction: 'up'}});
    }
</script>

Direction

Roll glyphs upward or downward with the direction prop.

Up 0Down 0

<template>
    <FluxFlex
        align="start"
        direction="vertical"
        :gap="21">
        <FluxFlex
            align="center"
            direction="horizontal"
            :gap="24">
            <FluxVisualSlotText
                direction="up"
                :text="`Up ${count}`"
                style="font-size: 21px; font-weight: 600;"/>

            <FluxVisualSlotText
                direction="down"
                :text="`Down ${count}`"
                style="font-size: 21px; font-weight: 600;"/>
        </FluxFlex>

        <FluxSecondaryButton
            icon-leading="rotate"
            label="Increment"
            @click="count++"/>
    </FluxFlex>
</template>

<script
    lang="ts"
    setup>
    import { FluxFlex, FluxSecondaryButton } from '@flux-ui/components';
    import { FluxVisualSlotText } from '@flux-ui/visuals';
    import { ref } from 'vue';

    const count = ref(0);
</script>

Chromatic

Enable chromatic for a rainbow hue sweep that fades back to the resting color as the glyphs land.

Rainbow

<template>
    <FluxFlex
        align="start"
        direction="vertical"
        :gap="21">
        <FluxVisualSlotText
            chromatic
            :text="values[index]"
            style="font-size: 33px; font-weight: 700;"/>

        <FluxSecondaryButton
            icon-leading="rotate"
            label="Roll"
            @click="roll"/>
    </FluxFlex>
</template>

<script
    lang="ts"
    setup>
    import { FluxFlex, FluxSecondaryButton } from '@flux-ui/components';
    import { FluxVisualSlotText } from '@flux-ui/visuals';
    import { ref } from 'vue';

    const values = ['Rainbow', 'Spectrum', 'Chromatic', 'Vibrant'];
    const index = ref(0);

    function roll(): void {
        index.value = (index.value + 1) % values.length;
    }
</script>