Language Flip Text
Flips one sentence between languages with a split-flap or vertical roll, and can settle on a chosen locale — demoed in Traditional Chinese, English and Japanese.
Cycles one sentence through its language versions — 繁中, English, 日本語 and so on. The default roll mode scrolls the old sentence up and springs the new one in from below while the container width eases between lengths; languages differ wildly in length, and that is what sets this apart from an ordinary text rotator. The flap mode flips a split-flap board character by character, padding shorter sentences with blank cells. Set settleOn and it plays one full round before stopping on the chosen language; pass "auto" to stop on the visitor's browser language — a good fit for a "now available in your language" announcement headline.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add language-flip-text.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<LanguageFlipText/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/language-flip-text.en.jsonOr, once registries are configured in components.json, install it as @webberui/language-flip-text.
Usage
import { LanguageFlipText } from "@/components/ui/language-flip-text";
const phrases = [
{ lang: "zh-Hant", label: "繁中", text: "現在,用你的語言" },
{ lang: "en", label: "English", text: "Now, in your language" },
{ lang: "ja", label: "日本語", text: "いま、あなたの言葉で" },
];
<LanguageFlipText
phrases={phrases}
settleOn="auto"
className="text-3xl font-semibold"
/>
<LanguageFlipText
variant="flap"
as="p"
phrases={[
{ lang: "zh-Hant", label: "繁中", text: "歡迎" },
{ lang: "en", label: "English", text: "Welcome" },
{ lang: "ja", label: "日本語", text: "ようこそ" },
]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
phrases | LanguageFlipPhrase[] | — | Sentences to cycle through, one per language |
variant | "roll" | "flap" | "roll" | Transition style: roll scrolls vertically (container width eases between lengths), flap flips character by character |
index | number | — | Controlled current phrase index; when omitted the component manages it (uncontrolled) |
defaultIndex | number | 0 | Initial index in uncontrolled mode |
onIndexChange | (index: number) => void | — | Called when the phrase changes, with the new index |
autoPlay | boolean | true | Whether to cycle automatically |
interval | number | 2000 | Autoplay interval (milliseconds) |
settleOn | string | — | Which language to stop on after one full round: a lang (e.g. "zh-Hant"), or "auto" to read the browser language |
loop | boolean | true | Whether to wrap back to the first phrase after the last; ignored when settleOn is set |
showLabel | boolean | true | Whether to show the language pill |
as | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "div" | "span" | "h2" | Tag of the root element |
className | string | — | Passed to the root element |
LanguageFlipPhrase
| Field | Type | Default | Description |
|---|---|---|---|
lang | string | — | BCP 47 language tag (e.g. zh-Hant, en, ja), written to the sentence node's lang attribute |
label | string | — | Name shown on the language pill (e.g. 「繁中」 (Traditional Chinese), "English") |
text | string | — | The sentence in this language |
matchPhraseLocale(phrases, locale) is also exported by name; it returns the index of the closest phrase for a locale (or -1 when none matches).
CSS variables
| Variable | Default | Description |
|---|---|---|
--wb-language-flip-duration | 420ms | Duration of the roll container's width/height easing and the pill crossfade |
--wb-language-flip-flap | 280ms | Duration of one cell flip (both halves together) |
--wb-language-flip-stagger | 30ms | Left-to-right delay between flap cells |
How it works
- Width easing:
rollmeasures the active sentence and tweens the container'swidth/heightdirectly rather than using a scale-based layout animation, so the text never squashes or stretches mid-transition. Sentences render on one line; shrink the type for long ones. - Flaps: each cell is split into two halves — the top half falls back and the bottom half drops from 90°. The cell count follows the longest phrase and shorter ones are padded with blank cells; strings are split with
Array.from, so CJK characters and emoji are never broken apart. - Settling:
settleOnwaits until every phrase has shown once before stopping on the chosen language."auto"readsnavigator.languagesinsideuseEffect, so the server render matches the first client render; browsers reportingzh-TW/zh-HKmap tozh-Hantandzh-CNmaps tozh-Hans, and with no match it settles back on the initial phrase. - Every sentence node carries a
langattribute so the browser picks the right fonts and line-breaking rules for CJK text.
Accessibility
- The root has
aria-live="off"so autoplay does not make screen readers announce every change; ansr-onlycopy of the current sentence, with the correctlang, provides the full text - The visual roll layer, flap cells and language pill are hidden from assistive technology to avoid double reading and letter-by-letter announcements
- Autoplay pauses on hover or keyboard focus; while autoplaying the root is focusable with Tab (with a focus-visible ring), and arrow keys then step to the previous/next phrase
- When the user's system has reduce motion enabled, nothing flips or rolls — the text switches directly with a 150ms fade-in
CJK Word Count Textarea
A React textarea with CJK-aware counting: switch live between word, character and byte modes, a limit progress ring, near-limit and over-limit warnings, frozen counts while an IME is composing, and an optional hard limit.
AI Prompt Input
An AI chat input — auto-growing height, IME-safe Enter to submit, and a submit arrow that flies out.