¿De qué va?
Es un mini-proyecto donde escribes opciones separadas por comas dentro de un <textarea>. Al presionar Enter, el script hace un “shuffle” visual (resalta opciones al azar por unas décimas) y se detiene en la elegida. Sirve para encuestas rápidas, dinámicas en clase, o decidir el almuerzo sin pelear. Es uno de los proyectos del curso 50 Projects 50 Days de Traversy Media.
Qué practicas
-
Eventos de teclado (keydown) y lectura de event.key.
-
Manejo de strings: split, trim, filtros vacíos.
-
Micro-interacciones: resaltar con clases CSS y temporizadores.
Código (3 archivos)
1) index.html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Random Choice Picker</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="wrap">
<h1>Random Choice Picker</h1>
<p class="lead">
Escribe opciones separadas por comas (ej.: <em>tacos, pizza, sushi</em>).
Presiona <kbd>Enter</kbd> para elegir una al azar.
</p>
<label for="ta" class="sr-only">Opciones separadas por comas</label>
<textarea id="ta" rows="4" placeholder="Escribe aquí…"></textarea>
<section id="tags" class="tags" aria-live="polite" aria-atomic="true"></section>
</main>
<script src="script.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Random Choice Picker</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="wrap">
<h1>Random Choice Picker</h1>
<p class="lead">
Escribe opciones separadas por comas (ej.: <em>tacos, pizza, sushi</em>).
Presiona <kbd>Enter</kbd> para elegir una al azar.
</p>
<label for="ta" class="sr-only">Opciones separadas por comas</label>
<textarea id="ta" rows="4" placeholder="Escribe aquí…"></textarea>
<section id="tags" class="tags" aria-live="polite" aria-atomic="true"></section>
</main>
<script src="script.js"></script>
</body>
</html>
2) style.css
* { box-sizing: border-box; }
:root {
--bg: #0f1115; --txt: #f5f7fa; --muted: #a0a8b8;
--chip: #151a23; --accent: #7c5cff; --accent-2: #22d3ee;
--radius: 14px; --gap: 10px;
}
html, body { height: 100%; }
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
background: radial-gradient(1200px 600px at 20% -10%, #1b2030 0, #0f1115 45%, #0b0e13 100%);
color: var(--txt);
}
.wrap {
max-width: 860px;
margin: 48px auto;
padding: 0 16px 32px;
}
h1 { margin: 0 0 6px; font-size: clamp(28px, 4vw, 44px); }
.lead { margin: 0 0 16px; color: var(--muted); }
textarea {
width: 100%;
border: 1px solid rgba(255,255,255,.12);
background: #0d1422;
color: var(--txt);
border-radius: 12px;
padding: 14px;
resize: vertical;
font-size: 16px;
outline: none;
box-shadow: 0 8px 26px rgba(0,0,0,.35) inset;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: var(--gap);
margin-top: 14px;
min-height: 42px;
}
.tag {
padding: 8px 12px;
border-radius: var(--radius);
background: linear-gradient(180deg, #1b2130, var(--chip));
border: 1px solid rgba(255,255,255,.06);
font-weight: 600;
letter-spacing: .2px;
transition: transform .12s ease, box-shadow .18s ease;
}
.tag.highlight {
transform: translateY(-1px) scale(1.05);
box-shadow: 0 0 0 2px #000, 0 0 0 4px var(--accent);
}
.tag.final {
transform: translateY(0) scale(1.07);
box-shadow: 0 0 0 2px #000, 0 0 0 4px var(--accent-2);
}
kbd {
background: #141b2a;
border: 1px solid rgba(255,255,255,.12);
border-bottom-color: rgba(255,255,255,.2);
border-radius: 6px;
padding: 2px 6px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: .95em;
}
.sr-only {
position: absolute !important;
width: 1px; height: 1px;
margin: -1px; padding: 0; border: 0;
clip: rect(0 0 0 0); overflow: hidden;
white-space: nowrap;
}
* { box-sizing: border-box; }
:root {
--bg: #0f1115; --txt: #f5f7fa; --muted: #a0a8b8;
--chip: #151a23; --accent: #7c5cff; --accent-2: #22d3ee;
--radius: 14px; --gap: 10px;
}
html, body { height: 100%; }
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
background: radial-gradient(1200px 600px at 20% -10%, #1b2030 0, #0f1115 45%, #0b0e13 100%);
color: var(--txt);
}
.wrap {
max-width: 860px;
margin: 48px auto;
padding: 0 16px 32px;
}
h1 { margin: 0 0 6px; font-size: clamp(28px, 4vw, 44px); }
.lead { margin: 0 0 16px; color: var(--muted); }
textarea {
width: 100%;
border: 1px solid rgba(255,255,255,.12);
background: #0d1422;
color: var(--txt);
border-radius: 12px;
padding: 14px;
resize: vertical;
font-size: 16px;
outline: none;
box-shadow: 0 8px 26px rgba(0,0,0,.35) inset;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: var(--gap);
margin-top: 14px;
min-height: 42px;
}
.tag {
padding: 8px 12px;
border-radius: var(--radius);
background: linear-gradient(180deg, #1b2130, var(--chip));
border: 1px solid rgba(255,255,255,.06);
font-weight: 600;
letter-spacing: .2px;
transition: transform .12s ease, box-shadow .18s ease;
}
.tag.highlight {
transform: translateY(-1px) scale(1.05);
box-shadow: 0 0 0 2px #000, 0 0 0 4px var(--accent);
}
.tag.final {
transform: translateY(0) scale(1.07);
box-shadow: 0 0 0 2px #000, 0 0 0 4px var(--accent-2);
}
kbd {
background: #141b2a;
border: 1px solid rgba(255,255,255,.12);
border-bottom-color: rgba(255,255,255,.2);
border-radius: 6px;
padding: 2px 6px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: .95em;
}
.sr-only {
position: absolute !important;
width: 1px; height: 1px;
margin: -1px; padding: 0; border: 0;
clip: rect(0 0 0 0); overflow: hidden;
white-space: nowrap;
}
3) script.js
const ta = document.getElementById('ta');
const tags = document.getElementById('tags');
function parseChoices(value) {
return value
.split(',')
.map(t => t.trim())
.filter(Boolean);
}
function renderTags(list) {
tags.innerHTML = list.map(txt => `${txt}`).join('');
}
function pickRandomTag() {
const all = [...document.querySelectorAll('.tag')];
if (!all.length) return null;
const i = Math.floor(Math.random() * all.length);
return all[i];
}
function flashPick(times = 24, interval = 70) {
return new Promise(resolve => {
let count = 0, last;
const timer = setInterval(() => {
if (last) last.classList.remove('highlight');
const t = pickRandomTag();
if (t) {
t.classList.add('highlight');
last = t;
}
if (++count >= times) {
clearInterval(timer);
resolve(last || null);
}
}, interval);
});
}
ta.addEventListener('input', (e) => {
renderTags(parseChoices(e.target.value));
});
// Al presionar Enter, ejecuta el “shuffle” y resalta la elección final
window.addEventListener('keydown', async (e) => {
if (e.key === 'Enter') { // recomendado frente a keyCode (deprecado)
e.preventDefault(); // evita salto de línea en textarea
const choices = parseChoices(ta.value);
renderTags(choices);
if (!choices.length) return;
// Animación de resaltado aleatorio
document.querySelectorAll('.tag').forEach(t => t.classList.remove('final', 'highlight'));
const finalTag = await flashPick(24, 70); // ~1.6s “shuffle”
if (finalTag) {
document.querySelectorAll('.tag').forEach(t => t.classList.remove('highlight'));
finalTag.classList.add('final');
}
}
});
const ta = document.getElementById('ta');
const tags = document.getElementById('tags');
function parseChoices(value) {
return value
.split(',')
.map(t => t.trim())
.filter(Boolean);
}
function renderTags(list) {
tags.innerHTML = list.map(txt => `${txt}`).join('');
}
function pickRandomTag() {
const all = [...document.querySelectorAll('.tag')];
if (!all.length) return null;
const i = Math.floor(Math.random() * all.length);
return all[i];
}
function flashPick(times = 24, interval = 70) {
return new Promise(resolve => {
let count = 0, last;
const timer = setInterval(() => {
if (last) last.classList.remove('highlight');
const t = pickRandomTag();
if (t) {
t.classList.add('highlight');
last = t;
}
if (++count >= times) {
clearInterval(timer);
resolve(last || null);
}
}, interval);
});
}
ta.addEventListener('input', (e) => {
renderTags(parseChoices(e.target.value));
});
// Al presionar Enter, ejecuta el “shuffle” y resalta la elección final
window.addEventListener('keydown', async (e) => {
if (e.key === 'Enter') { // recomendado frente a keyCode (deprecado)
e.preventDefault(); // evita salto de línea en textarea
const choices = parseChoices(ta.value);
renderTags(choices);
if (!choices.length) return;
// Animación de resaltado aleatorio
document.querySelectorAll('.tag').forEach(t => t.classList.remove('final', 'highlight'));
const finalTag = await flashPick(24, 70); // ~1.6s “shuffle”
if (finalTag) {
document.querySelectorAll('.tag').forEach(t => t.classList.remove('highlight'));
finalTag.classList.add('final');
}
}
});
Ejemplo
Aquí hay un pequeño ejemplo de implementación
Extra: consejos rápidos
-
Entrada limpia: elimina espacios extra y evita tags vacíos con filter(Boolean).
-
Accesibilidad: anuncia cambios con aria-live y bloquea el salto de línea al detectar Enter (en textarea) con preventDefault().
-
Tecla correcta: usa e.key === 'Enter' (moderno) en lugar de keyCode.
Random Choice Picker