---
title: Flux
description: Letters peel out and the next phrase settles in.
group: Text
order: 15
new: true
---

Letters peel out and the next phrase settles in. Drive it with a button or your own state.

```tsx
import { useState } from "react"
import { Flux } from "@/components/motion/flux"
import { Button } from "@/components/ui/button"

export function Example() {
  const [index, setIndex] = useState(0)
  const texts = ["Token ready", "Copy and ship", "Compose freely"]

  return (
    <>
      <Flux texts={texts} index={index} />
      <Button onClick={() => setIndex((i) => (i + 1) % texts.length)}>
        Flux
      </Button>
    </>
  )
}
```

## Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add flux
```

## Usage

```tsx
import { useState } from "react"
import { Flux } from "@/components/motion/flux"
import { Button } from "@/components/ui/button"
```

```tsx
const [index, setIndex] = useState(0)

<>
  <Flux
    texts={["Token ready", "Copy and ship", "Compose freely"]}
    index={index}
  />
  <Button onClick={() => setIndex((i) => (i + 1) % 3)}>Flux</Button>
</>
```

## API Reference

### Flux

| Prop | Type | Default |
| --- | --- | --- |
| texts | string[] | — |
| index | number | 0 |
| duration | number | 520 |
| className | string | — |

Also accepts the other native `span` attributes.

## Source

```tsx
"use client";

import "@/lib/motion-styles";

import {
  useEffect,
  useRef,
  useState,
  type ComponentProps,
  type CSSProperties,
} from "react";
import { cn } from "@/lib/utils";

export type FluxProps = ComponentProps<"span"> & {
  /** Phrases to flux between. */
  texts: string[];
  /** Active phrase index. */
  index?: number;
  /** Full flux duration in ms. */
  duration?: number;
};

function Flux({
  className,
  texts,
  index = 0,
  duration = 520,
  style,
  ...props
}: FluxProps) {
  const safeTexts = texts.length > 0 ? texts : [""];
  const active = ((index % safeTexts.length) + safeTexts.length) % safeTexts.length;
  const nextText = safeTexts[active] ?? "";
  const [display, setDisplay] = useState(nextText);
  const [phase, setPhase] = useState<"idle" | "out" | "in">("idle");
  const displayRef = useRef(display);
  const timersRef = useRef<number[]>([]);
  const ms = Math.max(duration, 120);
  const half = Math.round(ms / 2);

  displayRef.current = display;

  useEffect(() => {
    if (nextText === displayRef.current) return;

    for (const id of timersRef.current) window.clearTimeout(id);
    timersRef.current = [];

    const reduced =
      typeof window !== "undefined" &&
      window.matchMedia("(prefers-reduced-motion: reduce)").matches;

    if (reduced) {
      setDisplay(nextText);
      setPhase("idle");
      return;
    }

    setPhase("out");
    const swapId = window.setTimeout(() => {
      setDisplay(nextText);
      setPhase("in");
      const idleId = window.setTimeout(() => {
        setPhase("idle");
      }, half);
      timersRef.current.push(idleId);
    }, half);
    timersRef.current.push(swapId);

    return () => {
      for (const id of timersRef.current) window.clearTimeout(id);
      timersRef.current = [];
    };
  }, [nextText, half]);

  const letters = Array.from(display);

  return (
    <span
      data-slot="flux"
      data-phase={phase}
      className={cn("arctis-flux inline-flex flex-wrap", className)}
      style={
        {
          "--flux-duration": `${half}ms`,
          ...style,
        } as CSSProperties
      }
      aria-label={nextText}
      {...props}
    >
      {letters.length === 0 ? (
        <span className="arctis-flux-letter">&nbsp;</span>
      ) : (
        letters.map((letter, i) => (
          <span
            key={`${display}-${i}-${letter}`}
            className="arctis-flux-letter"
            style={
              {
                "--flux-delay": `${Math.round((i / Math.max(letters.length, 1)) * (half * 0.45))}ms`,
              } as CSSProperties
            }
          >
            {letter === " " ? "\u00A0" : letter}
          </span>
        ))
      )}
    </span>
  );
}

export { Flux };
```
