03Cipher alphabet cycle
Each letter starts a few positions ahead in the alphabet and steps backward one letter at a time until it lands on the real character — like dialing in a combination lock.
Best forPuzzle or escape-room UIs, security verification screen titles
charCodeAt 연산JS 10줄textContent 교체

<main class="stage">
<p class="ca-text" aria-live="polite">
<span class="ca-c">U</span><span class="ca-c">N</span><span class="ca-c">L</span><span class="ca-c">O</span><span class="ca-c">C</span><span class="ca-c">K</span>
</p>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop;
$easing: $ease-out;
$color: $subject-cream;
@include stage($stage-orange);
.ca-text {
margin: 0;
display: flex;
gap: 0;
font: 800 clamp(24px, 8vw, 38px)/1.2 "Space Mono", "Courier New", monospace;
}
.ca-c {
display: inline-block;
min-width: 1ch;
color: $color;
}
// 이 데모는 CSS keyframes 가 아니라 짧은 JS 로 각 글자를 알파벳 역순으로 되감아 원래 글자를 찾는다 —
// reduced-motion 은 스크립트 안의 matchMedia 체크로 처리한다 (index.html 참고)
@media (prefers-reduced-motion: reduce) {
.ca-c { color: $color; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.ca-text {
margin: 0;
display: flex;
gap: 0;
font: 800 clamp(24px, 8vw, 38px)/1.2 "Space Mono", "Courier New", monospace;
}
.ca-c {
display: inline-block;
min-width: 1ch;
color: #fff7e6;
}
@media (prefers-reduced-motion: reduce) {
.ca-c {
color: #fff7e6;
}
}var target = "UNLOCK";
var spans = document.querySelectorAll(".ca-c");
var reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var offs = [5, 6, 4, 7, 5, 6], cyc = 40;
var n = 0;
setInterval(function () {
n++;
spans.forEach(function (s, i) {
var t = reduced ? 0 : (((n - i * 2) % cyc) + cyc) % cyc;
var left = reduced ? 0 : Math.max(0, offs[i] - Math.floor(t / 2));
var base = target.charCodeAt(i) - 65;
s.textContent = String.fromCharCode(65 + (base + left) % 26);
});
}, 130);import { useEffect, useRef, useState } from "react";
import styles from "./CipherAlphabetCycle.module.scss";
export interface CipherAlphabetCycleProps {
text?: string;
interval?: number;
}
// 실사용: 뷰포트에 들어오면 각 글자가 알파벳 몇 칸 뒤에서 한 칸씩 되감기며 원래 글자를 찾아간다. 다 맞으면 멈춘다.
export default function CipherAlphabetCycle({ text = "UNLOCK", interval = 130 }: CipherAlphabetCycleProps) {
const ref = useRef<HTMLParagraphElement>(null);
const [display, setDisplay] = useState(text);
useEffect(() => {
const el = ref.current;
if (!el) return;
let timer: ReturnType<typeof setInterval> | null = null;
const io = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting || timer) return;
io.unobserve(entry.target);
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduced) { setDisplay(text); return; }
const offs = text.split("").map((_, i) => 4 + ((i * 2) % 4));
let n = 0;
timer = setInterval(() => {
n += 1;
let done = true;
let out = "";
for (let i = 0; i < text.length; i++) {
const left = Math.max(0, offs[i] - Math.floor(Math.max(0, n - i * 2) / 2));
if (left > 0) done = false;
const base = text.toUpperCase().charCodeAt(i) - 65;
out += String.fromCharCode(65 + ((base + left) % 26));
}
setDisplay(out);
if (done && timer) { clearInterval(timer); timer = null; }
}, interval);
});
}, { threshold: 0.3 });
io.observe(el);
return () => { io.disconnect(); if (timer) clearInterval(timer); };
}, [text, interval]);
return <p ref={ref} className={styles.text} aria-live="polite">{display}</p>;
}03 Cipher alphabet cycle — Measured area 2.021% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · 9 CSS Text Reveal Effects — Hex, Cipher, Radar
The post this demo lives in
9 CSS Text Reveal Effects — Hex, Cipher, RadarA css text reveal effect is a short animation for text appearing on screen, ready to paste in with no coding needed. All See all demos in this post


