Decoded Passage
Una singola etichetta di navigazione editoriale attraversa per un istante un rumore tipografico controllato prima di risolversi nello stato attivo. Il gesto di decodifica dà a un link testuale compatto una risposta memorabile senza cambiarne l’ingombro né ricorrere al movimento delle singole lettere.
Prova l’esperimento
Cos’è Decoded Passage?
Un’etichetta di navigazione editoriale passa brevemente attraverso glifi instabili e torna a una frase leggibile quando lo stato si assesta. L’attivazione conferma la selezione nella stessa area, mantenendo la composizione essenziale e concreta.
Come funziona
L’etichetta resta un unico pulsante semantico. Al passaggio del mouse e al focus da tastiera, i caratteri attraversano brevemente un insieme discreto di glifi prima di ricomporsi nella frase corrente. L’attivazione alterna lo stato locale, aggiorna l’etichetta e mostra un messaggio sintetico. La transizione usa requestAnimationFrame e si interrompe correttamente sul testo finale.
Dove usarlo
- Etichette compatte per la navigazione editoriale
- Indici di portfolio o raccolte con feedback locale sulla selezione
- Specimen di design system per stati interattivi basati solo sul testo
Accessibilità
Il punto focale è un pulsante nativo con etichetta accessibile e supporto completo da tastiera. Lo stato selezionato è comunicato attraverso colore, testo e un messaggio visibile annunciato con aria-live. Le preferenze di movimento ridotto eliminano le transizioni decorative, lasciando disponibile l’azione semantica.
Prestazioni
Il componente usa un pulsante, un nodo di stato, un piccolo ciclo requestAnimationFrame e nessuna risorsa o libreria esterna. Ogni nuova decodifica annulla l’animazione precedente.
Il codice
Esplora e copia il codice originale. Nell’anteprima le etichette vengono tradotte, mentre nel sorgente restano quelle originali (di solito inglesi). Per i testi dinamici usa AuroraLabI18n.t(chiave, testoPredefinito).
<main class="stage">
<button class="decode-label" type="button" data-decode data-ali-i18n="copy_1" data-ali-i18n-aria-label="copy_1" aria-label="Browse collection">Browse collection</button>
<p class="decode-status" data-status data-ali-i18n="copy_2" aria-live="polite" aria-atomic="true"> </p>
</main>
:root{
--bg:#0D0B0F;
--surface:#16131A;
--fg:#EEEAE4;
--muted:#A49DAB;
--violet:#62447D;
--raspberry:#DF284D;
--gold:#F5BA09;
--lilac:#B79BD4;
}
*{box-sizing:border-box}
html,body{margin:0;min-width:100%;min-height:100%;background:var(--bg)}
body{
color:var(--fg);
font-family:system-ui,-apple-system,"Segoe UI",sans-serif;
}
.stage{
width:100%;
min-height:100vh;
min-height:100svh;
display:flex;
align-items:center;
justify-content:flex-start;
padding:clamp(28px,9vw,116px) clamp(24px,13vw,190px);
background:var(--bg);
}
.decode-label{
position:relative;
display:inline-block;
max-width:100%;
margin:0;
padding:0 0 .08em;
border:0;
border-bottom:1px solid color-mix(in srgb,var(--fg) 30%,transparent);
color:var(--fg);
background:transparent;
cursor:pointer;
font-family:Georgia,"Times New Roman",serif;
font-size:clamp(2.15rem,6vw,5.7rem);
font-weight:400;
letter-spacing:-.055em;
line-height:.98;
text-align:left;
transition:color .25s ease,border-color .25s ease;
}
.decode-label::before{
content:"";
position:absolute;
left:0;
bottom:-2px;
width:2.6rem;
height:3px;
background:var(--gold);
transform-origin:left center;
transition:width .45s cubic-bezier(.2,.8,.2,1),background-color .25s ease;
}
.decode-label:hover,
.decode-label:focus-visible{color:var(--lilac);border-bottom-color:var(--lilac)}
.decode-label:hover::before,
.decode-label:focus-visible::before{width:100%;background:var(--raspberry)}
.decode-label:focus-visible{outline:1px solid var(--gold);outline-offset:12px}
.decode-label.is-active{color:var(--gold)}
.decode-label.is-active::before{width:100%;background:var(--raspberry)}
.decode-status{
position:absolute;
margin:0;
margin-left:clamp(0px,31vw,430px);
align-self:center;
max-width:12rem;
color:var(--muted);
font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
font-size:clamp(.68rem,1.2vw,.82rem);
letter-spacing:.08em;
line-height:1.35;
text-transform:uppercase;
opacity:0;
transform:translateY(5px);
transition:opacity .35s ease,transform .35s ease;
}
.decode-status.is-visible{opacity:1;transform:none}
@media (max-width:620px){
.stage{display:block;padding:43vh 24px 0}
.decode-label{font-size:clamp(2.25rem,12vw,4rem)}
.decode-status{
position:static;
margin:1.45rem 0 0;
max-width:10rem;
}
}
@media (prefers-reduced-motion:reduce){
.decode-label,.decode-label::before,.decode-status{transition:none}
}
var trigger = document.querySelector('[data-decode]');
var status = document.querySelector('[data-status]');
if (trigger && status) {
var glyphs = '!<>-_/?[]{}=+*^#%&';
var active = false;
var frameId = 0;
var timerId = 0;
var translate = function (key, fallback) {
return window.AuroraLabI18n && typeof window.AuroraLabI18n.t === 'function'
? window.AuroraLabI18n.t(key, fallback)
: fallback;
};
var decode = function (target) {
cancelAnimationFrame(frameId);
window.clearTimeout(timerId);
var source = trigger.textContent;
var length = Math.max(source.length, target.length);
var frame = 0;
var total = 16;
var tick = function () {
var progress = Math.min(frame / total, 1);
var resolved = Math.floor(progress * length);
var output = '';
for (var i = 0; i < length; i += 1) {
if (i < resolved) {
output += target.charAt(i) || '';
} else if (target.charAt(i) === ' ') {
output += ' ';
} else {
output += glyphs.charAt((i * 7 + frame * 3) % glyphs.length);
}
}
trigger.textContent = output;
frame += 1;
if (progress < 1) {
frameId = window.requestAnimationFrame(tick);
} else {
trigger.textContent = target;
}
};
tick();
};
trigger.addEventListener('mouseenter', function () {
decode(active ? translate('copy_2', 'Collection selected') : translate('copy_1', 'Browse collection'));
});
trigger.addEventListener('focus', function () {
decode(active ? translate('copy_2', 'Collection selected') : translate('copy_1', 'Browse collection'));
});
trigger.addEventListener('click', function () {
active = !active;
var next = active ? translate('copy_2', 'Collection selected') : translate('copy_1', 'Browse collection');
decode(next);
trigger.classList.toggle('is-active', active);
status.classList.toggle('is-visible', active);
status.textContent = active ? translate('copy_2', 'Collection selected') : '\u00a0';
trigger.setAttribute('aria-label', next);
});
}