Effetto Scramble Text: Testo che si Decodifica in JavaScript
Perché l’Effetto Scramble Text nel 2026
Quando un visitatore atterra sulla tua landing page, hai pochissimi secondi per catturare la sua attenzione. Nel 2026, le interfacce piatte e statiche non bastano più: gli utenti si aspettano microinterazioni che comunicano qualità e cura. L’effetto scramble text — quel meccanismo che mostra caratteri casuali prima di rivelare il testo definitivo — è diventato uno degli elementi visivi più riconoscibili nel design tecnico e cyberpunk.
Lo vedi ovunque: dashboard di startup, portfolio di developer, landing page di prodotti SaaS, hero section di siti tech. Il motivo è semplice: comunica velocità, controllo, precisione. è il linguaggio visivo del codice che prende vita.
In questo articolo costruiamo l’effetto da zero in JavaScript puro, senza librerie esterne. Partiremo da una versione base funzionante, la renderemo parametrica, aggiungeremo varianti avanzate e vedremo come integrarla in un progetto reale. Se hai già lavorato con animazioni JavaScript per UI e UX, troverai questo approccio familiare ma più chirurgico.
Come Funziona lo Scramble Text: la Logica di Base
Il meccanismo è elegante nella sua semplicità: ogni carattere del testo finale viene sostituito, per un breve periodo, da un carattere casuale estratto da un alfabeto definito. Man mano che il tempo passa, un numero crescente di caratteri si “bloccano” nella posizione corretta, fino a che il testo finale è completamente visibile.
Esistono due approcci principali:
- Sincrono da sinistra a destra: i caratteri si rivelano in ordine sequenziale, dando l’impressione di una macchina da scrivere avanzata.
- Asincrono con probabilità: ogni carattere ha una probabilità crescente di bloccarsi, creando un effetto di decodifica più organico e caotico.
Iniziamo con la versione più semplice usando setInterval e un contatore di iterazioni:
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%&*';
function scrambleText(element, finalText, duration = 1200) {
let iteration = 0;
const totalFrames = 30;
const interval = duration / totalFrames;
const timer = setInterval(() => {
element.textContent = finalText
.split('')
.map((char, index) => {
// I caratteri prima di `iteration` sono già risolti
if (index < iteration) return char;
// Spazi e punteggiatura rimangono invariati
if (char === ' ' || char === '.' || char === ',') return char;
// Gli altri caratteri sono ancora casuali
return chars[Math.floor(Math.random() * chars.length)];
})
.join('');
iteration += finalText.length / totalFrames;
if (iteration >= finalText.length) {
clearInterval(timer);
element.textContent = finalText;
}
}, interval);
}
// Utilizzo
const heading = document.querySelector('.hero-title');
scrambleText(heading, 'Hello, Developer.', 1500);Questo codice è già funzionante e produce un risultato visivamente interessante. Nota come gestiamo gli spazi e la punteggiatura: mantenerli fissi rende la decodifica molto più leggibile e riconoscibile come testo umano.
Versione Avanzata con Probabilità e RequestAnimationFrame
setInterval va bene per un prototipo, ma per un’animazione fluida a 60fps vogliamo usare requestAnimationFrame. Questo approccio è molto più efficiente perché si sincronizza con il ciclo di rendering del browser, evitando jank e consumo inutile di CPU.
La versione avanzata introduce anche un sistema a probabilità crescente: ogni carattere ha inizialmente 0% di probabilità di bloccarsi, che sale progressivamente fino al 100% man mano che l’animazione avanza. Il risultato è molto più organico:
class ScrambleText {
constructor(element, options = {}) {
this.element = element;
this.chars = options.chars || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&*!?';
this.duration = options.duration || 1000;
this.fps = options.fps || 30;
this.onComplete = options.onComplete || null;
this._raf = null;
this._resolved = [];
}
animate(finalText) {
const startTime = performance.now();
const frameInterval = 1000 / this.fps;
let lastFrame = 0;
// Inizializza lo stato: ogni carattere è inizialmente "non risolto"
this._resolved = new Array(finalText.length).fill(false);
const tick = (now) => {
const elapsed = now - startTime;
const progress = Math.min(elapsed / this.duration, 1);
// Aggiorna solo se è passato abbastanza tempo per un nuovo frame
if (now - lastFrame >= frameInterval) {
lastFrame = now;
this.element.textContent = finalText
.split('')
.map((char, i) => {
if (char === ' ') return ' ';
// Probabilità di risolversi aumenta con il progresso
if (!this._resolved[i] && Math.random() < progress * 1.5) {
this._resolved[i] = true;
}
return this._resolved[i]
? char
: this.chars[Math.floor(Math.random() * this.chars.length)];
})
.join('');
}
if (progress < 1) {
this._raf = requestAnimationFrame(tick);
} else {
// Assicurati che il testo finale sia esatto
this.element.textContent = finalText;
if (this.onComplete) this.onComplete();
}
};
this._raf = requestAnimationFrame(tick);
}
stop() {
if (this._raf) cancelAnimationFrame(this._raf);
}
}
// Utilizzo
const el = document.querySelector('h1');
const scrambler = new ScrambleText(el, {
duration: 1200,
fps: 24,
onComplete: () => console.log('Animazione completata!')
});
scrambler.animate('Frontend Developer');La classe ScrambleText è riutilizzabile e configurabile. Nota il moltiplicatore 1.5 nel calcolo della probabilità: accelera la convergenza finale evitando che gli ultimi caratteri impieghino troppo a risolversi.
Trigger con Intersection Observer: Anima al Momento Giusto
Un effetto scramble che parte immediatamente al caricamento della pagina può essere sprecato se l’elemento non è nella viewport. La soluzione professionale è combinarlo con Intersection Observer, esattamente come facciamo per le animazioni lazy load. L’effetto parte solo quando l’utente vede realmente l’elemento:
function initScrambleOnVisible(selector, options = {}) {
const elements = document.querySelectorAll(selector);
const observer = new IntersectionObserver(
(entries, obs) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const el = entry.target;
const finalText = el.dataset.scramble || el.textContent;
// Nasconde il testo originale e avvia l’animazione
el.textContent = '';
const scrambler = new ScrambleText(el, {
duration: options.duration || 1000,
fps: options.fps || 24,
chars: options.chars,
});
scrambler.animate(finalText);
// Non osservare più questo elemento
obs.unobserve(el);
}
});
},
{ threshold: 0.3 } // Parte quando il 30% dell’elemento è visibile
);
elements.forEach((el) => {
// Salva il testo originale nell’attributo data
if (!el.dataset.scramble) {
el.dataset.scramble = el.textContent;
}
observer.observe(el);
});
}
// HTML di esempio:
// <h2 class="scramble-title">Il Mio Titolo</h2>
// <p class="scramble-title" data-scramble="Testo Custom">Placeholder</p>
// Inizializzazione
document.addEventListener('DOMContentLoaded', () => {
initScrambleOnVisible('.scramble-title', {
duration: 1200,
fps: 30,
});
});L’attributo data-scramble permette di specificare un testo diverso da quello HTML iniziale: utile se vuoi mostrare un placeholder SEO-friendly nel markup e animare un testo diverso all’utente.
Effetto Multi-Linea e Stagger tra Elementi
L’effetto diventa ancora più potente quando applicato a gruppi di elementi in sequenza — un titolo, poi un sottotitolo, poi una descrizione. Questo è il pattern tipico nelle hero section di alto livello. Usiamo un stagger delay per creare una cascata narrativa:
async function scrambleSequence(elements, baseDelay = 200, duration = 900) {
for (let i = 0; i < elements.length; i++) {
const el = elements[i];
const finalText = el.dataset.scramble || el.textContent;
// Delay progressivo tra gli elementi
await new Promise((resolve) => setTimeout(resolve, i === 0 ? 0 : baseDelay));
const scrambler = new ScrambleText(el, {
duration,
fps: 24,
onComplete: resolve, // Questa riga non fa nulla qui, vedi sotto
});
// Avvia l’animazione e non aspetta il completamento
scrambler.animate(finalText);
}
}
// Versione che aspetta ogni elemento prima di passare al successivo
async function scrambleSequenceSerial(elements, pauseBetween = 100, duration = 900) {
for (const el of elements) {
const finalText = el.dataset.scramble || el.textContent;
el.textContent = '';
await new Promise((resolve) => {
const scrambler = new ScrambleText(el, { duration, onComplete: resolve });
scrambler.animate(finalText);
});
// Piccola pausa tra un elemento e l’altro
await new Promise((resolve) => setTimeout(resolve, pauseBetween));
}
}
// Utilizzo
const heroElements = document.querySelectorAll('.hero [data-scramble]');
scrambleSequenceSerial([...heroElements], 150, 800);
// CSS di supporto
// [data-scramble] { font-family: 'Courier New', monospace; }
// [data-scramble].animating { opacity: 0.8; }Questo pattern è molto simile a ciò che puoi costruire con librerie come GSAP, ma senza nessuna dipendenza esterna. Se invece sei già nel tuo progetto con GSAP, puoi leggere la nostra guida su animazioni JavaScript avanzate con GSAP per integrare i due approcci.
Styling CSS: Font Monospace e Colori Dinamici
Lo scramble text funziona meglio con font monospace: ogni carattere occupa la stessa larghezza, il che elimina il fastidioso layout shift durante l’animazione. Il CSS è minimo ma fondamentale:
/* Classe base per elementi scramble */
.scramble {
font-family: 'Courier New', 'Courier', monospace;
letter-spacing: 0.05em;
/* Previene il layout shift durante l'animazione */
min-height: 1em;
display: inline-block;
}
/* Caratteri "casuali" in colore accento - usa custom property */
.scramble[data-animating="true"] {
color: var(--scramble-random-color, #06b6d4);
}
/* Versione terminale: sfondo scuro + cursore lampeggiante */
.scramble--terminal {
background: #001525;
color: #06b6d4;
padding: 0.2em 0.5em;
border-radius: 4px;
position: relative;
}
.scramble--terminal::after {
content: '_';
animation: blink 0.8s step-end infinite;
margin-left: 2px;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* Hero section con effetto completo */
.hero-scramble {
font-size: clamp(2rem, 6vw, 5rem);
font-weight: 800;
font-family: 'Courier New', monospace;
line-height: 1.1;
color: #ffffff;
text-shadow: 0 0 30px rgba(6, 182, 212, 0.4);
transition: text-shadow 0.3s ease;
}
.hero-scramble[data-animating="true"] {
text-shadow: 0 0 40px rgba(6, 182, 212, 0.7);
}Il clamp() per il font-size è un’ottima pratica: il testo scala fluidamente senza media query. Se vuoi approfondire questa tecnica, leggi la nostra guida a clamp() per font responsive in CSS.
Un’accortezza importante: quando l’animazione è attiva, aggiungi un attributo data-animating="true" all’elemento per poter applicare stili distinti ai caratteri casuali rispetto al testo finale. Lo stile text-shadow glow dà l’impressione che i caratteri “brucino” prima di stabilizzarsi.
Variante Hack: Alphabeto Tecnico e Glitch
L’alfabeto di caratteri casuali è ciò che definisce il “feel” dell’effetto. Cambiarlo radicalmente trasforma il tono visivo. Ecco alcune varianti pronte all’uso:
// Configurazioni predefinite per stili diversi
const SCRAMBLE_PRESETS = {
// Stile hacker/cyberpunk (solo maiuscole + simboli)
hacker: {
chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%^&*!?><|/\',
duration: 1400,
fps: 20,
},
// Stile Matrix (katakana-like con caratteri speciali)
matrix: {
chars: 'アイウエオカキクケコ01010101サシスセソ',
duration: 1800,
fps: 15,
},
// Stile pulito (solo lowercase, testo che “emerge”)
clean: {
chars: 'abcdefghijklmnopqrstuvwxyz',
duration: 800,
fps: 30,
},
// Stile noise (caratteri di controllo ASCII visibili)
noise: {
chars: '!@#$%^&*()-_=+[]{}|;:,.<>?~`',
duration: 600,
fps: 40,
},
// Stile binario
binary: {
chars: '01',
duration: 1000,
fps: 25,
},
};
// Utilizzo con preset
function createScramblerWithPreset(el, finalText, presetName = 'hacker') {
const preset = SCRAMBLE_PRESETS[presetName] || SCRAMBLE_PRESETS.hacker;
const scrambler = new ScrambleText(el, preset);
scrambler.animate(finalText);
return scrambler;
}
// Effetto glitch: loop continuo con piccola probabilità di errore
function glitchLoop(element, text, glitchProbability = 0.05) {
let glitchTimer = null;
const chars = SCRAMBLE_PRESETS.noise.chars;
function maybeGlitch() {
if (Math.random() < glitchProbability) {
// Corrompe 1-3 caratteri per 100-300ms
const corrupted = text.split('').map((c) =>
Math.random() < 0.3 ? chars[Math.floor(Math.random() * chars.length)] : c
);
element.textContent = corrupted.join('');
setTimeout(() => {
element.textContent = text;
}, 80 + Math.random() * 200);
}
glitchTimer = setTimeout(maybeGlitch, 500 + Math.random() * 2000);
}
maybeGlitch();
// Restituisce una funzione per fermare il loop
return () => clearTimeout(glitchTimer);
}Il glitchLoop è particolarmente efficace per elementi decorativi come titoli di sezione o badge: dopo che lo scramble si risolve, il testo continua a “glitchare” occasionalmente, mantenendo viva la sensazione di un sistema attivo. Questa tecnica si sposa bene con le animazioni interattive CSS e JavaScript puro per creare UI memorabili.
FAQ e Domande Frequenti
L’effetto scramble text ha impatto sulle prestazioni?
Con requestAnimationFrame e un limite di fps a 24-30, l’impatto sulle prestazioni è trascurabile. Il punto critico è evitare di animare decine di elementi contemporaneamente: usa lo stagger o l’Intersection Observer per limitare le animazioni attive. Su mobile, considera di ridurre la durata o saltare l’effetto con matchMedia('(prefers-reduced-motion: reduce)').
Come gestisco l’accessibilità con questo effetto?
Gli screen reader leggono il testo nel DOM, non quello visivo durante l’animazione. Il problema è se l’utente arriva durante lo scramble: potrebbe leggere caratteri incomprensibili. La soluzione è usare aria-label sull’elemento con il testo finale, e aggiornare textContent solo come presentazione visiva. In alternativa, usa un elemento nascosto con aria-hidden="false" che contiene sempre il testo finale.
Posso usare l’effetto su testi con HTML interno (tag, link)?
La versione base usa textContent che distrugge il markup HTML interno. Per preservare tag come <strong> o <a> devi operare sui nodi di testo individualmente con un TreeWalker, oppure applicare lo scramble solo ai nodi foglia. È fattibile ma più complesso. La soluzione più pratica è applicare lo scramble solo a elementi con testo puro (titoli, badge, label) e lasciare i paragrafi formattati statici.
Come faccio ripartire l’effetto all’hover?
Basta chiamare di nuovo scrambler.animate(finalText) sull’evento mouseenter. Per evitare chiamate multiple rapide usa un debounce o un flag isAnimating che blocca nuovi trigger mentre l’animazione è in corso. Questo pattern è ottimo per button, card titolo e menu item: dà un feedback visivo immediato all’interazione.
Conclusione
L’effetto scramble text in JavaScript è uno di quegli elementi che separano un sito “fatto bene” da uno “fatto con cura”. Non richiede librerie, pesa meno di 2KB minificato e produce un impatto visivo sproporzionato rispetto alla sua complessità tecnica. Abbiamo visto come costruirlo da zero con requestAnimationFrame, come triggerarlo con Intersection Observer, come applicarlo in sequenza su più elementi e come personalizzarne il feel attraverso diversi alfabeti di caratteri.
Il passo successivo è integrarlo nel tuo progetto: una hero section, un counter di statistiche, un titolo di sezione. Una volta che lo vedi in produzione, difficilmente tornerai al testo statico. Se lavori su interfacce con molte animazioni coordinate, valuta di abbinarlo a una gestione avanzata degli eventi scroll — le tecniche di view-timeline e animation-timeline si integrano perfettamente con questo approccio.
Suggerimenti e Risorse
🔧 Pro tip: Usa sempre
prefers-reduced-motionper disabilitare o ridurre l’animazione per gli utenti che hanno questa preferenza impostata nel sistema operativo. Aggiungi all’inizio del tuo script:const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;e salta l’animazione se ètrue.
💡 Idea avanzata: Combina lo scramble text con le View Transitions API per creare transizioni tra pagine dove il titolo della nuova pagina “si decodifica” in entrata. L’effetto è cinematografico e totalmente nativo nel browser moderno.
🎯 Performance: Per applicazioni con molti elementi scramble attivi simultaneamente, considera di spostare il calcolo dei caratteri casuali in un Web Worker. Il main thread resta libero per il rendering e l’interazione utente.

