Mantine Text Animate

Logo

@gfazioli/mantine-text-animate

A Mantine component that allows you to animate text with various effects. Additionally, it provides other sub components such as TextAnimate.TextTicker, TextAnimate.Typewriter, TextAnimate.NumberTicker, and TextAnimate.Spinner. You can also use three useful hooks: useTextTicker, useTypewriter, and useNumberTicker.

Update from v2 to v3

v3.0.0 introduces new features and breaking changes:

Breaking Changes

  • Spinner children type: Now accepts string | React.ReactNode[]. When passing a ReactNode[], text processing (repeat/reverse) is skipped and you must provide an explicit aria-label.
  • speed prop semantics: The speed prop on Typewriter, Gradient, and Highlight now works as a multiplier — higher values mean faster animation (consistent with all other components). Previous behavior was inverted (higher = slower). Default values changed: Typewriter 0.031, Gradient 31. If you were using custom speed values, invert them (e.g., old speed={0.01} on Typewriter ≈ new speed={3}).

New Features (v3.0)

  • onCharType callback on Typewriter — called for each character typed with (char, index)
  • pauseAt prop on Typewriter — map of char index → pause duration (ms) for custom pauses
  • prefix / suffix props on NumberTicker — render content before/after the number
  • onAnimationComplete callback on TextAnimate — fires when all segments finish animating
  • trigger prop on TextAnimate — 'mount' (default), 'inView' (IntersectionObserver), or 'manual'
  • useTextAnimate() hook — returns { animate, setAnimate, replay, isAnimating, key, onAnimationComplete }
  • TextAnimate.Gradient compound component — animated gradient text via background-clip: text
  • animate="loop" on TextAnimate — automatic in→pause→out→pause cycle with configurable loopDelay
  • withSound prop on Typewriter — synthesized mechanical keyboard click via Web Audio API
  • formatValue prop on NumberTicker — override the default Intl.NumberFormat formatting
  • Scramble mode on TextTicker — deterministic per-character scramble via scrambleDuration and staggerDelay
  • TextAnimate.Highlight compound component — animated highlighter marker effect (CSS-only)
  • TextAnimate.SplitFlap compound component — airport departure board (Solari board) 3D flip display
  • TextAnimate.Morphing compound component — fluid text transitions using LCS algorithm
  • TextAnimate.RotatingText compound component — animated text carousel with slide/fade/blur transitions

Installation

yarn add @gfazioli/mantine-text-animate

After installation import package styles at the root of your application:

import '@gfazioli/mantine-text-animate/styles.css';

You can import styles within a layer @layer mantine-text-animate by importing @gfazioli/mantine-text-animate/styles.layer.css file.

import '@gfazioli/mantine-text-animate/styles.layer.css';

TextAnimate

The TextAnimate component allows you to animate text with various effects. The animate prop controls the initial display and the direction of the animation. In the example below, try selecting none to display nothing. With static, the text will appear static. Selecting Animate In will display the entering animation, and with Animate Out, the exiting animation will be shown.

Mantine TextAnimate component
Loop delay
Duration
Delay
Segment delay
import { TextAnimate } from '@gfazioli/mantine-text-animate';

function Demo() {
  return (
    <TextAnimate animate="loop" animation="slideUp" by="character">
      Mantine TextAnimate component
    </TextAnimate>
  );
}

animateProps

The animateProps prop allows you to pass additional props to the animation.

interface AnimateProps {
  /**
   * Controls the distance for slide animations (in pixels)
   * @default 20
   */
  translateDistance?: MantineSize;

  /**
   * Controls the scale factor for scale animations
   * For scaleUp: initial scale = 1 - scaleAmount (e.g., 0.8 means start at 0.2)
   * For scaleDown: initial scale = 1 + scaleAmount (e.g., 0.8 means start at 1.8)
   * @default 0.8
   */
  scaleAmount?: number;

  /**
   * Controls the blur amount for blur animations (in pixels)
   * @default 10
   */
  blurAmount?: MantineSize;
}
Mantine TextAnimate component
import { TextAnimate } from '@gfazioli/mantine-text-animate';
import { Stack, Switch } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [animated, { toggle }] = useDisclosure();

  return (
    <Stack>
      <Switch size="xl" checked={animated} onLabel="ON" offLabel="OFF" onChange={toggle} />
      <TextAnimate
        animate={animated ? 'in' : 'static'}
        by="character"
        animation="scale"
        animateProps={{
          scaleAmount: 34,
        }}
      >
        Mantine TextAnimate component
      </TextAnimate>
    </Stack>
  );
}

by

The by prop allows you to animate text by the entire text, character by character, per word or line by line. By using the by="line" prop, you can animate text line by line.

import { TextAnimate } from '@gfazioli/mantine-text-animate';
import { Stack, Switch } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [animated, { toggle }] = useDisclosure();

  return (
    <Stack h={150}>
      <Switch size="xl" checked={animated} onLabel="ON" offLabel="OFF" onChange={toggle} />
      <TextAnimate
        animate={animated ? 'in' : 'none'}
        by="line"
        animation="scale"
        duration={1}
        segmentDelay={0.5}
        animateProps={{
          scaleAmount: 2,
        }}
      >
        {`
          Mantine TextAnimate component\n
          Can be used for multiline text\n
          This is the third line\n
          That's all!
          `}
      </TextAnimate>
    </Stack>
  );
}

Styling

Of course, you can use the component with your own styles.

import { TextAnimate } from '@gfazioli/mantine-text-animate';
import { MantineSize, Stack, Switch } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [animated, { toggle }] = useDisclosure();

  return (
    <Stack>
      <Switch size="xl" checked={animated} onLabel="ON" offLabel="OFF" onChange={toggle} />
      <TextAnimate
        fz={48}
        fw={600}
        gradient={{ from: 'violet', to: 'yellow' }}
        variant="gradient"
        animate={animated ? 'in' : 'none'}
        by="word"
        animation="blur"
        duration={0.5}
        animateProps={{
          blurAmount: '20px' as MantineSize,
        }}
      >
        Mantine TextAnimate component
      </TextAnimate>
    </Stack>
  );
}

onAnimationStart and onAnimationEnd

You can use the onAnimationStart and onAnimationEnd props to handle animation events.

Mantine TextAnimate component
import { useState } from 'react';
import { TextAnimate } from '@gfazioli/mantine-text-animate';
import { Badge, Group, MantineSize, Stack, Switch } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [animated, { toggle }] = useDisclosure();
  const [event, setEvent] = useState('');

  return (
    <Stack>
      <Group>
        <Switch size="xl" checked={animated} onLabel="OUT" offLabel="IN" onChange={toggle} />
        <Badge color="lime" size="xl">
          {event}
        </Badge>
      </Group>
      <TextAnimate
        fz={48}
        fw={600}
        c="violet"
        animate={animated ? 'in' : 'out'}
        by="character"
        animation="slideRight"
        onAnimationStart={(animate) => setEvent(`Start ${animate}`)}
        onAnimationEnd={(animate) => setEvent(`End ${animate}`)}
        duration={0.5}
        animateProps={{
          translateDistance: '820px' as MantineSize,
        }}
      >
        Mantine TextAnimate component
      </TextAnimate>
    </Stack>
  );
}

trigger

The trigger prop controls when the animation starts. Use "inView" to animate when the element enters the viewport via IntersectionObserver. You can pass triggerOptions to customize threshold and rootMargin.

Scroll down to see the animation trigger...

import { TextAnimate } from '@gfazioli/mantine-text-animate';
import { Box, Stack, Text } from '@mantine/core';

function Demo() {
  return (
    <Box style={{ height: 400, overflow: 'auto' }}>
      <Stack>
        <Text c="dimmed">Scroll down to see the animation trigger...</Text>
        <Box style={{ height: 500 }} />
        <TextAnimate
          fz={32}
          fw={600}
          c="violet"
          trigger="inView"
          animate="in"
          animation="slideUp"
          by="word"
          triggerOptions={{ threshold: 0.5 }}
        >
          This text animates when it enters the viewport
        </TextAnimate>
        <Box style={{ height: 200 }} />
      </Stack>
    </Box>
  );
}

Loop mode

Set animate="loop" to automatically cycle through in→pause→out→pause→in. Use loopDelay to control the pause duration between phases (default: 2000ms).

Hello World!
Loop delay
import { TextAnimate } from '@gfazioli/mantine-text-animate';
import { Center } from '@mantine/core';

function Demo() {
  return (
    <Center h={200}>
      <TextAnimate
        fz={32}
        fw={700}
      >
        Hello World!
      </TextAnimate>
    </Center>
  );
}

useTextAnimate() hook

The useTextAnimate hook provides state and controls for managing TextAnimate animation direction, replay capability, and animation status tracking.

import { TextAnimate, useTextAnimate } from '@gfazioli/mantine-text-animate';
import { Button, Group, Stack } from '@mantine/core';

function Demo() {
  const { animate, setAnimate, replay, isAnimating, key, onAnimationComplete } = useTextAnimate();

  return (
    <Stack>
      <Group>
        <Button onClick={() => setAnimate('in')} disabled={isAnimating}>
          Animate In
        </Button>
        <Button onClick={() => setAnimate('out')} disabled={isAnimating}>
          Animate Out
        </Button>
        <Button onClick={replay} variant="outline">
          Replay
        </Button>
      </Group>
      <TextAnimate
        key={key}
        fz={32}
        fw={600}
        animate={animate}
        animation="blur"
        by="character"
        onAnimationComplete={onAnimationComplete}
      >
        Hello World
      </TextAnimate>
    </Stack>
  );
}

The return value of the useTextAnimate hook is the following:

export interface UseTextAnimateResult {
  /** The current animation direction */
  animate: TextAnimateAnimationDirection;
  /** Set the animation direction */
  setAnimate: (direction: TextAnimateAnimationDirection) => void;
  /** Replay the animation by forcing a remount via key change */
  replay: () => void;
  /** Whether the animation is currently running */
  isAnimating: boolean;
  /** Key to pass to TextAnimate for remount-based replay */
  key: number;
  /** Callback to pass to TextAnimate's onAnimationComplete prop */
  onAnimationComplete: (direction: 'in' | 'out') => void;
}

TextAnimate.Typewriter

The TextAnimate.Typewriter component allows you to animate text with a typewriter effect.

Delay
Speed
import { TextAnimate } from '@gfazioli/mantine-text-animate';

function Demo() {
  return (
    <TextAnimate.Typewriter value="Hello, World! Say Hello to Mantine Typewriter component" animate={false} loop={false} />
  );
}

Multiline

By using the multiline prop, you can animate text line by line.

import { TextAnimate } from '@gfazioli/mantine-text-animate';

function Demo() {
  return (
    <TextAnimate.Typewriter
      multiline
      value={[
        'Hello, World! Mantine Typewriter component',
        'That was a long time ago',
        'But it was fun',
      ]}
    />
  );
}

leftSection

You may use the leftSection prop to display a section before the text.

>

👉
import { TextAnimate, type TypewriterProps } from '@gfazioli/mantine-text-animate';
import { Center, Stack, Text } from '@mantine/core';

function Demo() {
  return (
    <Center miw={400} h={150}>
      <Stack w="100%">
        <TextAnimate.Typewriter
          multiline
          leftSection={
            <Text c="red" mr={4}>
              &gt;{' '}
            </Text>
          }
          value={[
            'Hello, World! Mantine Typewriter component',
            'That was a long time ago',
            'But it was fun',
          ]}
        />

        <TextAnimate.Typewriter multiline leftSection={'👉'} value={['Another left section']} />
      </Stack>
    </Center>
  );
}

Styling

Of course, you can use the component with your own styles.

import { TextAnimate } from '@gfazioli/mantine-text-animate';

function Demo() {
  return (
    <TextAnimate.Typewriter
      multiline
      fz={24}
      c="red"
      ff="monospace"
      value={[
        'Hello, World! Mantine Typewriter component',
        'That was a long time ago',
        'But it was fun',
      ]}
    />
  );
}

onTypeEnd and onTypeLoop

You can use the onTypeEnd and onTypeLoop props to handle typewriter events.

onTypeEnd: 0
onTypeLoop: 0

import { useState } from 'react';
import { TextAnimate, type TypewriterProps } from '@gfazioli/mantine-text-animate';
import { Badge, Center, Stack } from '@mantine/core';

function Demo() {
  const [isTypeEnd, setIsTypeEnd] = useState(0);
  const [isTypeLoop, setIsTypeLoop] = useState(0);

  return (
    <Stack>
      <Badge color="red">onTypeEnd: {isTypeEnd}</Badge>
      <Badge color="lime">onTypeLoop: {isTypeLoop}</Badge>
      <TextAnimate.Typewriter
        onTypeEnd={() => {
          setIsTypeEnd((prev) => prev + 1);
        }}
        onTypeLoop={() => {
          setIsTypeLoop((prev) => prev + 1);
        }}
        value={[
          'Hello, World! Mantine Typewriter component',
          'That was a long time ago',
          'But it was fun',
        ]}
      />
    </Stack>
  );
}

withSound

Enable withSound to play a synthesized mechanical keyboard click on each character typed. The sound is generated via Web Audio API (no external files). Use soundVolume to control volume (0–1). The AudioContext is lazily initialized on the first sound, so a user gesture is required. Respects prefers-reduced-motion.

import { TextAnimate } from '@gfazioli/mantine-text-animate';
import { Center, Stack, Switch } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [playing, { toggle }] = useDisclosure(false);

  return (
    <Center h={200}>
      <Stack align="center">
        <Switch size="xl" checked={playing} onLabel="ON" offLabel="OFF" onChange={toggle} />
        <TextAnimate.Typewriter
          fz={24}
          fw={600}
          value={[
            'Click to hear the sound...',
            'Mechanical keyboard vibes!',
            'Typewriter with sound effects',
          ]}
          animate={playing}
          withSound
          soundVolume={0.3}
          speed={0.1}
        />
      </Stack>
    </Center>
  );
}

onCharType and pauseAt

The onCharType callback is called for each character typed, receiving the character and its index. Combined with pauseAt, you can create dramatic pauses at specific character positions — for example, pausing after punctuation for a more natural typing rhythm.

Chars typed: 0
Last char: -

Pauses 800ms after "Hello," and 1200ms after "World!"

import { useMemo, useState } from 'react';
import { TextAnimate } from '@gfazioli/mantine-text-animate';
import { Badge, Group, Stack, Text } from '@mantine/core';

function Demo() {
  const [lastChar, setLastChar] = useState('');
  const [charCount, setCharCount] = useState(0);

  // Memoize to keep a stable reference
  const pauseAt = useMemo(() => ({ 6: 800, 13: 1200 }), []);

  return (
    <Stack gap="sm">
      <Group>
        <Badge color="blue">Chars typed: {charCount}</Badge>
        <Badge color="grape">Last char: {lastChar || '-'}</Badge>
      </Group>
      <Text size="sm" c="dimmed">
        Pauses 800ms after &quot;Hello,&quot; and 1200ms after &quot;World!&quot;
      </Text>
      <TextAnimate.Typewriter
        value="Hello, World! Welcome to Mantine"
        loop
        onCharType={(char, index) => {
          setLastChar(char);
          setCharCount(index + 1);
        }}
        pauseAt={pauseAt}
      />
    </Stack>
  );
}

useTypewriter() hook

The TextAnimate.Typewriter is built by using the useTypewriter hook.

Done

import { useTypewriter } from '@gfazioli/mantine-text-animate';
import { Badge, Button, Center, Divider, Group, Stack, Text } from '@mantine/core';

function Demo() {
  const { text, start, stop, reset, isTyping } = useTypewriter({
    animate: false,
    value: ['Hello', 'From', 'Mantine useTypewriter() hook'],
  });

  return (
    <Stack>
      <Text>{isTyping ? 'Typing...' : 'Done'}</Text>
      <Group>
        <Button size="xs" onClick={start}>
          Start
        </Button>
        <Button size="xs" onClick={stop}>
          Stop
        </Button>
        <Button size="xs" onClick={reset}>
          Reset
        </Button>
      </Group>
      <Divider />
      <Badge color="violet" size="xl">{text}</Badge>
    </Stack>
  );
}

The interface of the useTypewriter hook is the following:

export interface TypewriterBaseProps {
  /**
   * The text or array of texts to display in typewriter effect
   */
  value: string | string[];

  /**
   * Controls if the animation is running (true) or reset (false)
   * @default true
   */
  animate?: boolean;

  /**
   * Whether to display text in multiple lines
   * @default false
   */
  multiline?: boolean;

  /**
   * Animation speed multiplier (higher = faster)
   * @default 1
   */
  speed?: number;

  /**
   * The delay between text changes in milliseconds (when using multiple texts)
   * @default 2000
   */
  delay?: number;

  /**
   * Whether to loop through the texts
   * @default true
   */
  loop?: boolean;

  /**
   * Callback function to be called when the typing animation ends
   */
  onTypeEnd?: () => void;

  /**
   * Callback function to be called when the typing animation is looped
   * and the animation is about to start again
   */
  onTypeLoop?: () => void;

  /**
   * Callback function called for each character typed
   */
  onCharType?: (char: string, index: number) => void;

  /**
   * Map of character indices to custom pause durations (in milliseconds)
   */
  pauseAt?: Record<number, number>;
}

And the return value of the useTypewriter hook is the following:

export interface UseTypewriterResult {
  /**
   * The current text being displayed
   * If withMultiLine is true, this will be an array of strings
   */
  text: string | string[];

  /**
   * Whether the typewriter is currently typing
   */
  isTyping: boolean;

  /**
   * Start the typewriter animation
   */
  start: () => void;

  /**
   * Stop the typewriter animation
   */
  stop: () => void;

  /**
   * Reset the typewriter to its initial state
   */
  reset: () => void;
}

Multiline with useTypewriter()

You may use the multiline prop with the useTypewriter hook. In this case the text will be an array of strings.

import { useTypewriter, type TypewriterProps } from '@gfazioli/mantine-text-animate';
import { Badge, Center, Group, Stack } from '@mantine/core';

function Demo() {
  const { text } = useTypewriter({
    value: ['Hello', 'From', 'Mantine useTypewriter()'],
    multiline: true,
  });

  return (
    <Center h={200}>
      <Stack w="100%">
        <Stack>
          {(text as string[]).map((line) => (
            <Badge size="xl" key={line}>
              {line}
            </Badge>
          ))}
        </Stack>
        <Group>
          {(text as string[]).map((line) => (
            <Badge size="md" color="lime" key={line}>
              {line}
            </Badge>
          ))}
        </Group>
      </Stack>
    </Center>
  );
}

Example: Terminal

Below a fun example of a terminal.

import { TextAnimate, type TypewriterProps } from '@gfazioli/mantine-text-animate';
import { Center } from '@mantine/core';

function Demo() {
  return (
      <TextAnimate.Typewriter
        multiline
        c="green"
        ff="monospace"
        cursorChar="█"
        withBlink={true}
        delay={500}
        value={[
          '$ cd /home/user/projects',
          '$ ls -la',
          'total 32',
          'drwxr-xr-x  5 user user 4096 Mar 10 14:30 .',
          'drwxr-xr-x 18 user user 4096 Mar 10 14:28 ..',
          'drwxr-xr-x  8 user user 4096 Mar 10 14:30 .git',
          '-rw-r--r--  1 user user  948 Mar 10 14:30 README.md',
          'drwxr-xr-x  2 user user 4096 Mar 10 14:30 src',
          '$ npm run build',
          '> project@1.0.0 build',
          '> webpack --mode production',
          '✓ Compiled successfully in 2.36s',
        ]}
      />
  );
}

TextAnimate.Spinner

The TextAnimate.Spinner component allows you to animate text with a spinner effect. The children prop accepts either a string (for text processing) or an array of React.ReactNode (for custom content like icons).

Direction
Speed
Radius
Char offset
Repeat count
import { TextAnimate, type SpinnerProps } from '@gfazioli/mantine-text-animate';
import { Center } from '@mantine/core';

function Demo() {
  return (
    <TextAnimate.Spinner>
      ★ SPINNING TEXT EXAMPLE ★
    </TextAnimate.Spinner>
  );
}

repeatText and repeatCount

import { TextAnimate } from '@gfazioli/mantine-text-animate';

function Demo() {
  return (
    <TextAnimate.Spinner repeatText={true} repeatCount={3}>
      Hello *
    </TextAnimate.Spinner>
  );
}

TextAnimate.NumberTicker

The TextAnimate.NumberTicker component allows you to animate text with a number ticker effect. You can use the prefix and suffix props to display content before and after the number.

0

Value
Start value
Delay
Decimal places
Speed
import { TextAnimate, type NumberTickerProps } from '@gfazioli/mantine-text-animate';
import { Center, Stack, Switch } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [animated, { close, toggle }] = useDisclosure();

  return (
    <Stack>
      <Switch size="xl" checked={animated} onLabel="ON" offLabel="OFF" onChange={toggle} />
      <TextAnimate.NumberTicker
        fz={64}
        c="violet"
        animate={animated}
        onCompleted={close}
      />
    </Stack>
  );
}

formatValue

Use the formatValue prop to override the default Intl.NumberFormat formatting. This is useful for custom currency, percentage, or unit formatting.

$0.00

import { TextAnimate } from '@gfazioli/mantine-text-animate';
import { Center, Stack, Switch } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [animated, { close, toggle }] = useDisclosure();

  return (
    <Stack>
      <Switch size="xl" checked={animated} onLabel="ON" offLabel="OFF" onChange={toggle} />
      <TextAnimate.NumberTicker
        fz={48}
        c="teal"
        value={1234.56}
        decimalPlaces={2}
        animate={animated}
        onCompleted={close}
        formatValue={(value) =>
          new Intl.NumberFormat('en-US', {
            style: 'currency',
            currency: 'USD',
          }).format(value)
        }
      />
    </Stack>
  );
}

useNumberTicker() hook

The TextAnimate.NumberTicker is built by using the useNumberTicker hook.

Done

0

0

import { useNumberTicker } from '@gfazioli/mantine-text-animate';
import { Badge, Button, Divider, Group, Stack, Text } from '@mantine/core';

function Demo() {
  const { text, isAnimating, start, stop, reset, displayValue } = useNumberTicker({
    value: 64,
    startValue: 0,
    delay: 0,
    decimalPlaces: 0,
    speed: 0.2,
    easing: 'ease-out',
    animate: false,
  });

  return (
    <Stack>
      <Text>{isAnimating ? 'Animating...' : 'Done'}</Text>
      <Group>
        <Button size="xs" onClick={start}>
          Start
        </Button>
        <Button size="xs" onClick={stop}>
          Stop
        </Button>
        <Button size="xs" onClick={reset}>
          Reset
        </Button>
      </Group>
      <Divider />
      <Badge color="violet" size="xl">
        {text}
      </Badge>
      <Text>{displayValue}</Text>
    </Stack>
  );
}

The interface of the useNumberTicker hook is the following:

export interface NumberTickerBaseProps {
  /**
   * The target value to animate to
   */
  value: number;

  /**
   * The initial value to start from
   * @default 0
   */
  startValue?: number;

  /**
   * Delay before starting the animation in seconds
   * @default 0
   */
  delay?: number;

  /**
   * Number of decimal places to display
   * @default 0
   */
  decimalPlaces?: number;

  /**
   * Animation speed multiplier (higher = faster)
   * @default 1
   */
  speed?: number;

  /**
   * Easing function for the animation
   * @default "ease-out"
   */
  easing?: NumberTickerEasing;

  /**
   * Whether the animation should start automatically
   * @default true
   */
  animate?: boolean;

  /**
   * Callback function called when animation completes
   */
  onCompleted?: () => void;
}

And the return value of the useNumberTicker hook is the following:

export interface UseNumberTickerResult {
  /**
   * The formatted text representation of the current value
   */
  text: string;

  /**
   * The current numeric value (not formatted)
   */
  displayValue: number;

  /**
   * Function to start the animation
   */
  start: () => void;

  /**
   * Function to stop the animation while keeping the current value
   */
  stop: () => void;

  /**
   * Function to reset the animation to the initial value
   */
  reset: () => void;

  /**
   * Whether the animation is currently running
   */
  isAnimating: boolean;
}

Example

Norway

Norway Fjord Adventures

On Sale

100.99

$

With Mountain Expeditions, you can immerse yourself in the breathtaking mountain scenery through tours and activities in and around the majestic peaks

Download

0

Norway

Norway Fjord Adventures

On Sale

200.00

$

With Fjord Tours you can explore more of the magical fjord landscapes with tours and activities on and around the fjords of Norway

Download

0

import { useEffect, useRef, useState } from 'react';
import { TextAnimate } from '@gfazioli/mantine-text-animate';
import { Badge, Button, Card, Group, Image, Text } from '@mantine/core';
import { useInViewport } from '@mantine/hooks';


function Demo() {
  const { ref, inViewport } = useInViewport();

  const [downloadCount, setDownloadCount] = useState<number>(1266);
  const timerRef = useRef<NodeJS.Timeout | null>(null);

  useEffect(() => {
    if (inViewport) {
      timerRef.current = setTimeout(() => {
        setDownloadCount((prev) => prev + Math.floor(Math.random() * 10));
      }, 3000);
    }
  });

  return (
    <Group grow ref={ref}>
      <Card shadow="sm" padding="lg" radius="md" withBorder>
        <Card.Section>
          <Image
            src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-8.png"
            height={160}
            alt="Norway"
          />
        </Card.Section>

        <Group justify="space-between" mt="md" mb="xs">
          <Text fw={500}>Norway Fjord Adventures</Text>
          <Badge color="pink">
            On Sale{' '}
            <TextAnimate.NumberTicker
              animate={inViewport}
              startValue={100.99}
              value={88.99}
              speed={0.15}
              decimalPlaces={2}
            />
            $
          </Badge>
        </Group>

        <Text size="sm" c="dimmed">
          With Mountain Expeditions, you can immerse yourself in the breathtaking mountain scenery
          through tours and activities in and around the majestic peaks
        </Text>

        <Text size="md" ml="auto" mt={8}>
          Download{' '}
          <TextAnimate.NumberTicker
            animate={inViewport}
            fw={900}
            value={downloadCount}
            speed={0.2}
          />
        </Text>

        <Button color="blue" fullWidth mt="md" radius="md">
          Book classic tour now
        </Button>
      </Card>

      <Card shadow="sm" padding="lg" radius="md" withBorder>
        <Card.Section>
          <Image
            src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png"
            height={160}
            alt="Norway"
          />
        </Card.Section>

        <Group justify="space-between" mt="md" mb="xs">
          <Text fw={500}>Norway Fjord Adventures</Text>
          <Badge color="pink">
            On Sale{' '}
            <TextAnimate.NumberTicker
              animate={inViewport}
              startValue={200}
              value={99.99}
              speed={0.15}
              decimalPlaces={2}
            />
            $
          </Badge>
        </Group>

        <Text size="sm" c="dimmed">
          With Fjord Tours you can explore more of the magical fjord landscapes with tours and
          activities on and around the fjords of Norway
        </Text>

        <Text size="md" ml="auto" mt={8}>
          Download{' '}
          <TextAnimate.NumberTicker animate={inViewport} fw={900} value={984777} speed={0.12} />
        </Text>

        <Button color="blue" fullWidth mt="md" radius="md">
          Book classic tour now
        </Button>
      </Card>
    </Group>
  );

TextAnimate.TextTicker

The TextAnimate.TextTicker component allows you to animate text with a text ticker effect.

Delay
Speed
Random change speed
import { TextAnimate, type TextTickerProps } from '@gfazioli/mantine-text-animate';
import { Center, Stack, Switch } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo(props: TextTickerProps) {
  const [animated, { close, toggle }] = useDisclosure();

  return (
    <Center>
      <Stack>
        <Switch size="xl" checked={animated} onLabel="ON" offLabel="OFF" onChange={toggle} />
        <TextAnimate.TextTicker fz="xl" {...props} animate={animated} onCompleted={close} />
      </Stack>
    </Center>
  );
}

Scramble mode

When scrambleDuration is set, TextTicker switches to a deterministic "scramble" mode where each character cycles through random characters for a fixed duration before settling on the target. Use staggerDelay to control the delay between each character starting its animation.

import { TextAnimate } from '@gfazioli/mantine-text-animate';
import { Center, Stack, Switch } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [animated, { close, toggle }] = useDisclosure();

  return (
    <Center>
      <Stack align="center">
        <Switch size="xl" checked={animated} onLabel="ON" offLabel="OFF" onChange={toggle} />
        <TextAnimate.TextTicker
          fz="xl"
          value="Scramble Mode"
          animate={animated}
          scrambleDuration={800}
          staggerDelay={50}
          revealDirection="left-to-right"
          onCompleted={close}
        />
      </Stack>
    </Center>
  );
}

useTextTicker() hook

The TextAnimate.TextTicker is built by using the useTextTicker hook.

Done

import { useTextTicker } from '@gfazioli/mantine-text-animate';
import { Badge, Button, Divider, Group, Stack, Text } from '@mantine/core';

function Demo() {
  const { text, isAnimating, start, stop, reset } = useTextTicker({
    value: 'Mantine useTextTicker',
    delay: 0,
    speed: 0.2,
    easing: 'ease-out',
    animate: false,
  });

  return (
    <Stack w={'100%'}>
      <Text>{isAnimating ? 'Animating...' : 'Done'}</Text>
      <Group>
        <Button size="xs" onClick={start}>
          Start
        </Button>
        <Button size="xs" onClick={stop}>
          Stop
        </Button>
        <Button size="xs" onClick={reset}>
          Reset
        </Button>
      </Group>
      <Divider />
      <Badge color="violet" size="xl">
        {text}
      </Badge>
    </Stack>
  );
}

The interface of the useTextTicker hook is the following:

export interface TextTickerBaseProps {
  /**
   * The target text to animate to
   */
  value: string;

  /**
   * Initial text display option
   * - "none": Display nothing until animation starts
   * - "random": Display random characters until animation starts
   * - "target": Display the target text immediately
   * @default "random"
   */
  initialText?: TextTickerInitialDisplay;

  /**
   * Whether the animation should start automatically
   * @default true
   */
  animate?: boolean;

  /**
   * Character set to use for random characters
   * @default "alphanumeric"
   */
  characterSet?: TextTickerCharacterSet;

  /**
   * Custom characters to use when characterSet is "custom"
   * @default ""
   */
  customCharacters?: string;

  /**
   * Delay before starting the animation in seconds
   * @default 0
   */
  delay?: number;

  /**
   * Animation speed multiplier (higher = faster)
   * @default 1
   */
  speed?: number;

  /**
   * Easing function for the animation
   * @default "ease-out"
   */
  easing?: TextTickerEasing;

  /**
   * Speed multiplier for random character changes (higher = more frequent changes)
   * @default 1
   */
  randomChangeSpeed?: number;

  /**
   * Direction for revealing the target text
   * @default "left-to-right"
   */
  revealDirection?: TextTickerRevealDirection;

  /**
   * Duration in milliseconds for each character's scramble phase (deterministic mode).
   * When set, switches from probabilistic to deterministic per-character timing.
   */
  scrambleDuration?: number;

  /**
   * Delay in milliseconds between each character starting its animation.
   * Only used when scrambleDuration is set.
   * @default 50
   */
  staggerDelay?: number;

  /**
   * Callback function called when animation completes
   */
  onCompleted?: () => void;
}

And the return value of the useTextTicker hook is the following:

export interface UseTextTickerResult {
  /**
   * The current text being displayed
   */
  text: string;

  /**
   * Function to start the animation
   */
  start: () => void;

  /**
   * Function to stop the animation while keeping the current text
   */
  stop: () => void;

  /**
   * Function to reset the animation to the initial text
   */
  reset: () => void;

  /**
   * Whether the animation is currently running
   */
  isAnimating: boolean;
}

Example

Welcome

import { TextAnimate } from '@gfazioli/mantine-text-animate';
import { Stack, Title } from '@mantine/core';
import { useInViewport } from '@mantine/hooks';

function Demo() {
  const { ref, inViewport } = useInViewport();

  return (
    <Stack w={'100%'} align="center" ref={ref}>
      <Title order={1}>Welcome</Title>
      <TextAnimate.TextTicker
        fs="italic"
        c="violet"
        style={{
          textShadow: '0 0 10px rgba(255, 255, 255, 0.5)',
        }}
        initialText="random"
        speed={0.05}
        characterSet="custom"
        customCharacters="*"
        revealDirection="center-out"
        value="Amazing TextTicker component for Mantine UI Library"
        animate={inViewport}
      />
    </Stack>
  );
}

TextAnimate.Gradient

The TextAnimate.Gradient component displays text with an animated gradient background. It accepts an array of Mantine colors and animates the gradient via background-clip: text.

Mantine Gradient Text
Speed
Direction
Color1
Color2
Color3
Color4
Color5
import { TextAnimate } from '@gfazioli/mantine-text-animate';
import { Center } from '@mantine/core';

function Demo() {
  return (
    <Center>
      <TextAnimate.Gradient
        fz={48}
        fw={700}
      >
        Mantine Gradient Text
      </TextAnimate.Gradient>
    </Center>
  );
}

TextAnimate.Highlight

The TextAnimate.Highlight component creates an animated highlighter marker effect on text. It uses a CSS animation to sweep a colored background from left to right, simulating a text highlighter pen.

Highlighted Text
Color
Speed
import { TextAnimate } from '@gfazioli/mantine-text-animate';
import { Center } from '@mantine/core';

function Demo() {
  return (
    <Center>
      <TextAnimate.Highlight
        fz={48}
        fw={700}
      >
        Highlighted Text
      </TextAnimate.Highlight>
    </Center>
  );
}

TextAnimate.RotatingText

The TextAnimate.RotatingText component cycles through an array of text strings with smooth enter/exit animations. It supports multiple transition variants (slide, fade, blur) and is designed to be used inline alongside static text.

I love React
Interval
Speed
import { TextAnimate } from '@gfazioli/mantine-text-animate';
import { Center } from '@mantine/core';

function Demo() {
  return (
    <Center h={100}>
      <span style={{ fontSize: 32, fontWeight: 700 }}>
        I love{' '}
        <TextAnimate.RotatingText
          values={["React","Mantine","TypeScript"]}
          fz={32}
          fw={700}
          c="blue"
        />
      </span>
    </Center>
  );
}

useRotatingText() hook

The TextAnimate.RotatingText is built by using the useRotatingText hook.

Hello

Index: 0 | Transitioning: no

import { useRotatingText } from '@gfazioli/mantine-text-animate';
import { Button, Center, Group, Stack, Text } from '@mantine/core';

const values = ['Hello', 'Bonjour', 'Ciao', 'Hola', 'Hallo'];

function Demo() {
  const { currentText, currentIndex, isTransitioning, start, stop, reset } = useRotatingText({
    values,
    interval: 2000,
    animate: false,
  });

  return (
    <Center h={120}>
      <Stack align="center">
        <Group>
          <Button onClick={start}>Start</Button>
          <Button onClick={stop}>Stop</Button>
          <Button onClick={reset}>Reset</Button>
        </Group>
        <Text fz={32} fw={700}>
          {currentText}
        </Text>
        <Text size="sm" c="dimmed">
          Index: {currentIndex} | Transitioning: {isTransitioning ? 'yes' : 'no'}
        </Text>
      </Stack>
    </Center>
  );
}

The interface of the useRotatingText hook is the following:

export interface RotatingTextBaseProps {
  /**
   * Array of text strings to rotate through
   */
  values: string[];

  /**
   * Whether the rotation animation is active
   * @default true
   */
  animate?: boolean;

  /**
   * Time in milliseconds each text stays visible before rotating
   * @default 3000
   */
  interval?: number;

  /**
   * Animation speed multiplier (higher = faster transition)
   * @default 1
   */
  speed?: number;

  /**
   * Callback fired when the text rotates to a new index
   */
  onCycle?: (index: number) => void;
}

And the return value of the useRotatingText hook is the following:

export interface UseRotatingTextResult {
  /**
   * Index of the currently displayed text
   */
  currentIndex: number;

  /**
   * The currently displayed text
   */
  currentText: string;

  /**
   * Index of the next text to display
   */
  nextIndex: number;

  /**
   * The next text to display
   */
  nextText: string;

  /**
   * Whether a transition animation is in progress
   */
  isTransitioning: boolean;

  /**
   * Callback to attach to the entering element's onAnimationEnd
   */
  onTransitionEnd: () => void;

  /**
   * Start the rotation
   */
  start: () => void;

  /**
   * Stop the rotation
   */
  stop: () => void;

  /**
   * Reset to the first text
   */
  reset: () => void;
}

TextAnimate.SplitFlap

The TextAnimate.SplitFlap component recreates the classic airport/train station split-flap display (Solari board). Each character flips through the character set in order until reaching its target, with a 3D CSS flip animation.

Speed
Flip duration
Stagger delay
Bg
Text color
Divider color
Radius
import { TextAnimate } from '@gfazioli/mantine-text-animate';
import { Center, Stack, Switch } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [animated, { toggle }] = useDisclosure();

  return (
    <Center h={200}>
      <Stack align="center">
        <Switch size="xl" checked={animated} onLabel="ON" offLabel="OFF" onChange={toggle} />
        <TextAnimate.SplitFlap
          fz={32}
          animate={animated}
        />
      </Stack>
    </Center>
  );
}

useSplitFlap() hook

The TextAnimate.SplitFlap is built by using the useSplitFlap hook.

Done

import { useSplitFlap } from '@gfazioli/mantine-text-animate';
import { Button, Divider, Group, Stack, Text } from '@mantine/core';

function Demo() {
  const { characters, isAnimating, start, stop, reset } = useSplitFlap({
    value: 'HELLO',
    speed: 1,
    animate: false,
  });

  return (
    <Stack w={'100%'}>
      <Text>{isAnimating ? 'Flipping...' : 'Done'}</Text>
      <Group>
        <Button size="xs" onClick={start}>Start</Button>
        <Button size="xs" onClick={stop}>Stop</Button>
        <Button size="xs" onClick={reset}>Reset</Button>
      </Group>
      <Divider />
      <Text fz={32} ff="monospace" fw={700}>
        {characters.map((c) => c.current).join('')}
      </Text>
    </Stack>
  );
}

Styles API

Component Styles API

Hover over selectors to highlight corresponding elements

/*
 * Hover over selectors to apply outline styles
 *
 */

TextAnimate.Morphing

The TextAnimate.Morphing component creates fluid text transitions between two strings. It uses the Longest Common Subsequence (LCS) algorithm to identify shared characters — common characters smoothly move to their new positions, while removed characters fade out and new characters fade in. Requires a monospace font for accurate positioning (uses ch units).

import { TextAnimate } from '@gfazioli/mantine-text-animate';
import { Center, Stack, TextInput } from '@mantine/core';
import { useState } from 'react';

function Demo() {
  const [text, setText] = useState('Hello World');

  return (
    <Center h={200}>
      <Stack align="center" w="100%">
        <TextInput
          label="Type to morph"
          value={text}
          onChange={(e) => setText(e.currentTarget.value)}
          w={300}
        />
        <TextAnimate.Morphing fz={32} fw={700} value={text} speed={1} />
      </Stack>
    </Center>
  );
}

useMorphing() hook

The TextAnimate.Morphing is built by using the useMorphing hook.

Done

import { useMorphing } from '@gfazioli/mantine-text-animate';
import { Button, Divider, Group, Stack, Text, Box } from '@mantine/core';
import { useState } from 'react';

const words = ['Hello', 'World', 'Mantine', 'Morphing'];

function Demo() {
  const [index, setIndex] = useState(0);
  const value = words[index];
  const { characters, width, isAnimating } = useMorphing({
    value,
    speed: 1,
  });

  return (
    <Stack w={'100%'}>
      <Text>{isAnimating ? 'Morphing...' : 'Done'}</Text>
      <Group>
        <Button size="xs" onClick={() => setIndex((i) => (i + 1) % words.length)}>
          Next Word
        </Button>
      </Group>
      <Divider />
      <Box
        style={{
          position: 'relative',
          display: 'inline-block',
          width: `${width}ch`,
          height: '1.2em',
          fontFamily: 'monospace',
          fontSize: 32,
          fontWeight: 700,
        }}
      >
        {characters.map((char) => (
          <Text
            key={char.key}
            component="span"
            style={{
              position: 'absolute',
              left: `${char.toX}ch`,
              top: 0,
              transition: 'all 1s ease',
              opacity: char.state === 'exiting' ? 0 : 1,
            }}
          >
            {char.char}
          </Text>
        ))}
      </Box>
    </Stack>
  );
}