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.
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.
<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.
<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.
<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.
<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.
<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.
<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.
<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>