Text swaps between English and Japanese as a wave, not a per-character shuffle: the span renders as [resolved new text][churn band][untouched old text], and the front travels the string's length plus the band width over 320 + 9n ms, capped at 1.2 s. The band is 12% of the string clamped to 3-9 characters, reshuffled every 55 ms from a pool chosen by the TARGET language -- katakana plus Latin when the destination contains CJK, Latin alone otherwise. Head and churn carry the end-goal face from the first frame while the tail keeps the previous one, so no glyph ever passes through an intermediate typeface. React owns a single text node inside the span; the effect swaps in two styled children for the duration and restores that node on settle, so later React updates still land on a live node.
Before code, we tend the roots — people aligned, viability proven.
Tokyo based design studio, specialised in experience design & design interactions.
Let's work together
Source
"use client";
import { useEffect, useState } from "react";
import Scramble from "@/components/Scramble";
import ScrambleLink from "../ScrambleLink";
/* The site's EN ⇄ JP scramble. Not a per-character shuffle: a wave travels
front-to-back through the sentence as [resolved new text][churn band][old
text], and the churn pool is drawn from the TARGET language (kana once the
destination is Japanese). Head and churn render in the end-goal face from
frame one while the untouched tail keeps the previous one, so no glyph ever
passes through an intermediate typeface. Auto-toggles so the wave keeps
running; the buttons force a direction. */
type Lang = "en" | "jp";
const FLUX_FACE = "font-[family-name:var(--flux)]";
const MINCHO_FACE =
"font-[family-name:var(--mincho)] [font-variation-settings:normal] tracking-[0.04em]";
const CYCLE_MS = 3800;
const TAGLINE = {
en: "Tokyo based design studio, specialised in experience design & design interactions.",
jp: "東京のデザインスタジオ。エクスペリエンスデザインと、デザインインタラクションを専門としています。",
};
const LEAD = {
en: "Before code, we tend the roots — people aligned, viability proven.",
jp: "コードの前に、根をととのえる。人を揃え、勝算を確かめる。",
};
const LABEL = { en: "Let's work together", jp: "一緒に、はじめませんか" };
export default function WaveScramble() {
const [lang, setLang] = useState<Lang>("en");
const [auto, setAuto] = useState(true);
useEffect(() => {
if (!auto) return;
const id = setInterval(
() => setLang((l) => (l === "en" ? "jp" : "en")),
CYCLE_MS,
);
return () => clearInterval(id);
}, [auto]);
const face = (l: Lang) => (l === "jp" ? MINCHO_FACE : FLUX_FACE);
// end-goal face for the head + churn band, previous face for the old tail
const wave = {
headClass: face(lang),
tailClass: face(lang === "jp" ? "en" : "jp"),
};
const pick = (l: Lang) => {
setAuto(false);
setLang(l);
};
return (
<div
className="fixed inset-0 flex flex-col items-center justify-center overflow-hidden px-8"
style={{ background: "var(--paper)", color: "var(--ink)" }}
>
<div className="max-w-[46ch] text-center">
<p className="text-[clamp(17px,2.1vw,26px)] leading-[1.6]">
<Scramble {...wave} text={LEAD[lang]} />
</p>
<p className="mt-6 text-[clamp(12px,1.2vw,15px)] leading-[1.7] opacity-60">
<Scramble {...wave} text={TAGLINE[lang]} />
</p>
<p className="mt-8 text-[11px] tracking-[0.12em] uppercase opacity-40">
<Scramble {...wave} text={LABEL[lang]} />
</p>
</div>
<div className="mt-12 flex items-center gap-4 text-[11px] tracking-[0.1em] uppercase">
{(["en", "jp"] as const).map((l) => (
<button
key={l}
onClick={() => pick(l)}
style={{
color: lang === l ? "var(--ink)" : "var(--faint)",
fontFamily: "var(--flux)",
fontVariationSettings: '"wght" 400, "SRIF" 100',
}}
>
{l === "en" ? "EN" : "日本語"}
</button>
))}
<button
onClick={() => setAuto((a) => !a)}
style={{
color: auto ? "var(--ink)" : "var(--faint)",
fontFamily: "var(--flux)",
fontVariationSettings: '"wght" 400, "SRIF" 100',
}}
>
auto
</button>
</div>
<div className="fixed bottom-6 left-6 z-10">
<ScrambleLink
from="WAVE SCRAMBLE"
to="← LAB"
href="/lab"
className="text-[11px] tracking-[0.1em] text-foreground/30 uppercase hover:text-foreground/60 transition-colors"
style={{ fontVariationSettings: "'wght' 400, 'SRIF' 100" }}
/>
</div>
</div>
);
}