Si ya sabes escribir HTML, sabes que una lista de productos con 20 elementos significa escribir 20 bloques de <div> a mano, o esperar a que alguien los escriba por ti en el backend. Pero cuando esos 20 productos vienen de una API y pueden ser 3 o pueden ser 200, escribir HTML fijo ya no sirve. Necesitas que el propio navegador construya esos elementos sobre la marcha, con los datos que le llegan en ese momento.
Para eso está document.createElement(): un método de JavaScript que construye un elemento HTML desde código, sin que tú lo escribas en el archivo .html.
const elemento = document.createElement('div'); // 1. crear
elemento.textContent = 'Hola'; // 2. rellenar
elemento.className = 'aviso'; // 3. dar clase (CSS, no estilos inline)
contenedor.append(elemento); // 4. insertar en el DOM
Esos cuatro pasos son el patrón que se repite en todo este tutorial. La diferencia con otras formas de meter HTML dinámico, como innerHTML, no es solo de estilo: tiene consecuencias de seguridad reales que vale la pena entender antes de escribir una sola línea.
createElement contra innerHTML: por qué importa la diferencia
innerHTML parece el camino corto: le pasas una cadena de texto con etiquetas y el navegador la convierte en HTML real. El problema aparece en cuanto esa cadena incluye algo que no controlas tú, como el nombre que ha escrito un usuario en un formulario.
const nombreUsuario = '<img src=x onerror="alert(1)">';
// ❌ MAL: innerHTML interpreta esa cadena como HTML real
// y ejecuta el onerror. Esto es una vulnerabilidad XSS.
contenedor.innerHTML = nombreUsuario;
// ✅ BIEN: createElement + textContent tratan el dato como texto plano.
// El navegador nunca lo interpreta como HTML, así que no hay ejecución posible.
const span = document.createElement('span');
span.textContent = nombreUsuario;
contenedor.appendChild(span);
Con innerHTML, esa cadena se interpreta literalmente como etiquetas HTML, y el atributo onerror se ejecuta en cuanto el navegador intenta cargar una imagen que no existe. Eso es un XSS: alguien metió código en un campo de texto y tu propia página lo ejecutó por él. Con textContent, el navegador nunca trata ese string como HTML, pase lo que pase dentro, así que se muestra tal cual como texto en pantalla, no como un script que se ejecuta.
Esto no significa que innerHTML esté prohibido siempre: si el HTML que insertas es fijo, escrito por ti y sin datos de usuario dentro, no hay riesgo. El problema empieza en el momento en que metes ahí una variable con datos que vienen de fuera (un formulario, una API, la URL). En ese caso, createElement con textContent es la opción segura por diseño, no porque lo digan las buenas prácticas, sino porque hace imposible que ese dato se interprete como código.
💡 ¿Cuándo usar createElement en vez de HTML fijo?
- Listas dinámicas: Mostrar productos desde una API
- Formularios dinámicos: Añadir campos según el usuario
- Interfaz reactiva: Cambiar contenido sin recargar la página
- Cualquier dato que no controles tú: nombres, comentarios, búsquedas
Sintaxis básica de createElement
const elemento = document.createElement('tagName');
Donde tagName es el tipo de elemento que quieres crear: 'div', 'p', 'button', 'img', etc.
Ejemplo básico: Crear un párrafo
// 1. Crear el elemento
const parrafo = document.createElement('p');
// 2. Añadir contenido
parrafo.textContent = '¡Hola mundo desde JavaScript!';
// 3. Añadir al DOM
document.body.appendChild(parrafo);
Resultado: Se añade un párrafo al final de la página con el texto "¡Hola mundo desde JavaScript!"
⚠️ Importante
Crear un elemento con createElement NO lo añade automáticamente a la página. Necesitas usar appendChild, insertBefore o similar para que aparezca.
Si alguna vez te pasa que el elemento "no aparece" y no sabes por qué, abre Chrome DevTools e inspecciona el DOM en directo: verás al momento si el nodo se creó pero nunca llegó a insertarse.
Añadir atributos y estilos
Un elemento sin atributos es como una casa sin puertas. Veamos cómo añadir clases, IDs, estilos y más.
Ejemplo: Crear un botón completo
// Crear el botón
const boton = document.createElement('button');
// Añadir texto
boton.textContent = 'Haz clic aquí';
// Añadir atributos
boton.id = 'mi-boton';
boton.className = 'btn btn-primary';
// Añadir estilos CSS
boton.style.backgroundColor = '#007bff';
boton.style.color = 'white';
boton.style.padding = '10px 20px';
boton.style.border = 'none';
boton.style.borderRadius = '5px';
// Añadir al DOM
document.body.appendChild(boton);
Métodos alternativos para atributos
const imagen = document.createElement('img');
// Método 1: Acceso directo
imagen.src = 'https://via.placeholder.com/200';
imagen.alt = 'Imagen de ejemplo';
// Método 2: setAttribute()
imagen.setAttribute('width', '200');
imagen.setAttribute('height', '200');
// Método 3: classList para clases
imagen.classList.add('imagen-responsive', 'borde-redondo');
Añadir eventos a elementos creados
Los elementos dinámicos pueden tener eventos como cualquier elemento HTML. Aquí te muestro las mejores formas:
Ejemplo: Botón con evento click
const botonContador = document.createElement('button');
let contador = 0;
botonContador.textContent = `Clicks: ${contador}`;
// Añadir evento
botonContador.addEventListener('click', function() {
contador++;
botonContador.textContent = `Clicks: ${contador}`;
if (contador === 10) {
alert('¡Has hecho 10 clicks!');
}
});
document.body.appendChild(botonContador);
Ejemplo: Input con validación en tiempo real
// Crear contenedor
const contenedor = document.createElement('div');
// Crear input
const input = document.createElement('input');
input.type = 'email';
input.placeholder = 'Escribe tu email';
// Crear mensaje de estado
const mensaje = document.createElement('p');
// Validación en tiempo real
input.addEventListener('input', function(e) {
const email = e.target.value;
const esValido = email.includes('@') && email.includes('.');
if (email.length === 0) {
mensaje.textContent = '';
} else if (esValido) {
mensaje.textContent = '✅ Email válido';
mensaje.style.color = 'green';
} else {
mensaje.textContent = '❌ Email inválido';
mensaje.style.color = 'red';
}
});
// Añadir al contenedor
contenedor.appendChild(input);
contenedor.appendChild(mensaje);
document.body.appendChild(contenedor);
append() y appendChild(): la diferencia práctica
Todos los ejemplos de arriba usan appendChild(), que es el método clásico y funciona en cualquier navegador. Pero desde hace años existe append(), más cómodo en dos cosas: acepta varios elementos a la vez (no uno por llamada) y también admite texto plano directamente, sin tener que crear un nodo de texto aparte.
const titulo = document.createElement('h3');
const precio = document.createElement('p');
// appendChild(): un nodo por llamada, dos líneas
tarjeta.appendChild(titulo);
tarjeta.appendChild(precio);
// append(): varios nodos en una sola llamada, y también admite texto plano
tarjeta.append(titulo, precio, ' (oferta)');
La diferencia real, más allá de la comodidad, es que append() no devuelve nada (undefined), mientras que appendChild() devuelve el nodo que acabas de insertar. Si en algún punto necesitas encadenar ese elemento recién insertado (guardarlo en una variable para usarlo después), appendChild() te lo da directamente en la misma línea; con append() necesitarías la referencia que ya tenías guardada antes.
DocumentFragment: para cuando insertas muchos elementos de golpe
Cada vez que haces contenedor.appendChild(elemento), el navegador tiene que recalcular el layout de la página (lo que se llama reflow). Si insertas un elemento cada vez, no se nota. Pero si generas 200 filas de una tabla en un bucle y haces un appendChild por cada una, el navegador repite ese recálculo 200 veces, una por inserción.
DocumentFragment es un contenedor invisible que vive en memoria, fuera del DOM real. Puedes meterle dentro todos los elementos que quieras con appendChild() normal, sin que eso dispare ningún reflow, porque el fragmento no forma parte de la página todavía. Cuando por fin insertas el fragmento completo en el DOM, el navegador hace un único recálculo para todo el lote:
const tareas = ['Tarea 1', 'Tarea 2', 'Tarea 3']; // imagina 200
const lista = document.getElementById('lista-tareas');
// ❌ LENTO con listas grandes: un reflow por cada appendChild
tareas.forEach(texto => {
const li = document.createElement('li');
li.textContent = texto;
lista.appendChild(li); // reflow en cada vuelta
});
// ✅ RÁPIDO: se construye todo en memoria, un único reflow al final
const fragmento = new DocumentFragment();
tareas.forEach(texto => {
const li = document.createElement('li');
li.textContent = texto;
fragmento.appendChild(li); // esto NO toca el DOM real todavía
});
lista.appendChild(fragmento); // un solo reflow, con las 3 (o 200) tareas ya dentro
❌ Cuándo NO hace falta DocumentFragment
Para 3 o 10 elementos, la diferencia de rendimiento es imperceptible, y añadir un DocumentFragment ahí solo complica el código sin necesidad. Resérvalo para listas largas (decenas o cientos de elementos) generadas de golpe, como resultados de búsqueda, tablas de datos o feeds. En la lista de tareas del siguiente apartado, con solo 3 elementos, no hace falta.
Crear listas dinámicas
Uno de los usos más comunes de createElement es generar listas desde arrays de datos. Ideal para mostrar productos, usuarios, tareas, etc.
Ejemplo: Lista de tareas interactiva
// Array de tareas
const tareas = [
{ id: 1, texto: 'Aprender createElement', completada: false },
{ id: 2, texto: 'Hacer la compra', completada: true },
{ id: 3, texto: 'Llamar al médico', completada: false }
];
// Crear lista
const lista = document.createElement('ul');
lista.style.listStyle = 'none';
lista.style.padding = '0';
// Función para crear cada item
function crearItemTarea(tarea) {
const li = document.createElement('li');
li.style.padding = '10px';
li.style.border = '1px solid #ddd';
li.style.marginBottom = '5px';
li.style.borderRadius = '5px';
li.style.display = 'flex';
li.style.justifyContent = 'space-between';
li.style.alignItems = 'center';
// Texto de la tarea
const texto = document.createElement('span');
texto.textContent = tarea.texto;
if (tarea.completada) {
texto.style.textDecoration = 'line-through';
texto.style.color = '#888';
}
// Botón de completar
const botonCompletar = document.createElement('button');
botonCompletar.textContent = tarea.completada ? '❌' : '✅';
botonCompletar.style.border = 'none';
botonCompletar.style.background = 'transparent';
botonCompletar.style.cursor = 'pointer';
botonCompletar.style.fontSize = '18px';
// Evento para cambiar estado
botonCompletar.addEventListener('click', function() {
tarea.completada = !tarea.completada;
actualizarLista();
});
li.appendChild(texto);
li.appendChild(botonCompletar);
return li;
}
// Función para actualizar la lista
function actualizarLista() {
lista.innerHTML = ''; // Limpiar lista
tareas.forEach(tarea => {
const item = crearItemTarea(tarea);
lista.appendChild(item);
});
}
// Cargar lista inicial
actualizarLista();
document.body.appendChild(lista);
Ejemplo práctico: generador de tarjetas de producto
Veamos un ejemplo más complejo: crear tarjetas de productos dinámicamente, como si fuera un e-commerce.
// Datos de productos (podrían venir de una API)
const productos = [
{
id: 1,
nombre: 'Laptop Gaming',
precio: 899.99,
imagen: 'https://via.placeholder.com/300x200?text=Laptop',
descuento: 15
},
{
id: 2,
nombre: 'Mouse Inalámbrico',
precio: 29.99,
imagen: 'https://via.placeholder.com/300x200?text=Mouse',
descuento: 0
},
{
id: 3,
nombre: 'Teclado Mecánico',
precio: 79.99,
imagen: 'https://via.placeholder.com/300x200?text=Teclado',
descuento: 25
}
];
// Función para crear una tarjeta de producto
function crearTarjetaProducto(producto) {
// Contenedor principal
const tarjeta = document.createElement('div');
tarjeta.style.border = '1px solid #e1e5e9';
tarjeta.style.borderRadius = '10px';
tarjeta.style.padding = '20px';
tarjeta.style.maxWidth = '300px';
tarjeta.style.margin = '10px';
tarjeta.style.boxShadow = '0 2px 10px rgba(0,0,0,0.1)';
tarjeta.style.transition = 'transform 0.3s';
// Efecto hover
tarjeta.addEventListener('mouseenter', () => {
tarjeta.style.transform = 'translateY(-5px)';
});
tarjeta.addEventListener('mouseleave', () => {
tarjeta.style.transform = 'translateY(0)';
});
// Imagen del producto
const imagen = document.createElement('img');
imagen.src = producto.imagen;
imagen.alt = producto.nombre;
imagen.style.width = '100%';
imagen.style.borderRadius = '5px';
imagen.style.marginBottom = '15px';
// Nombre del producto
const nombre = document.createElement('h3');
nombre.textContent = producto.nombre;
nombre.style.margin = '0 0 10px 0';
nombre.style.color = '#333';
// Contenedor de precios
const contenedorPrecio = document.createElement('div');
contenedorPrecio.style.marginBottom = '15px';
if (producto.descuento > 0) {
// Precio original tachado
const precioOriginal = document.createElement('span');
precioOriginal.textContent = `€${producto.precio.toFixed(2)}`;
precioOriginal.style.textDecoration = 'line-through';
precioOriginal.style.color = '#999';
precioOriginal.style.marginRight = '10px';
// Precio con descuento
const precioDescuento = producto.precio * (1 - producto.descuento / 100);
const precioFinal = document.createElement('span');
precioFinal.textContent = `€${precioDescuento.toFixed(2)}`;
precioFinal.style.color = '#e74c3c';
precioFinal.style.fontWeight = 'bold';
precioFinal.style.fontSize = '18px';
// Badge de descuento
const badgeDescuento = document.createElement('span');
badgeDescuento.textContent = `-${producto.descuento}%`;
badgeDescuento.style.backgroundColor = '#e74c3c';
badgeDescuento.style.color = 'white';
badgeDescuento.style.padding = '2px 8px';
badgeDescuento.style.borderRadius = '12px';
badgeDescuento.style.fontSize = '12px';
badgeDescuento.style.marginLeft = '10px';
contenedorPrecio.appendChild(precioOriginal);
contenedorPrecio.appendChild(precioFinal);
contenedorPrecio.appendChild(badgeDescuento);
} else {
// Solo precio normal
const precio = document.createElement('span');
precio.textContent = `€${producto.precio.toFixed(2)}`;
precio.style.fontSize = '18px';
precio.style.fontWeight = 'bold';
precio.style.color = '#27ae60';
contenedorPrecio.appendChild(precio);
}
// Botón de compra
const botonComprar = document.createElement('button');
botonComprar.textContent = '🛒 Añadir al carrito';
botonComprar.style.width = '100%';
botonComprar.style.padding = '12px';
botonComprar.style.backgroundColor = '#3498db';
botonComprar.style.color = 'white';
botonComprar.style.border = 'none';
botonComprar.style.borderRadius = '5px';
botonComprar.style.cursor = 'pointer';
botonComprar.style.fontSize = '14px';
botonComprar.style.fontWeight = 'bold';
botonComprar.style.transition = 'background-color 0.3s';
// Eventos del botón
botonComprar.addEventListener('mouseenter', () => {
botonComprar.style.backgroundColor = '#2980b9';
});
botonComprar.addEventListener('mouseleave', () => {
botonComprar.style.backgroundColor = '#3498db';
});
botonComprar.addEventListener('click', () => {
alert(`¡${producto.nombre} añadido al carrito!`);
});
// Ensamblar la tarjeta
tarjeta.appendChild(imagen);
tarjeta.appendChild(nombre);
tarjeta.appendChild(contenedorPrecio);
tarjeta.appendChild(botonComprar);
return tarjeta;
}
// Crear contenedor para las tarjetas
const contenedorProductos = document.createElement('div');
contenedorProductos.style.display = 'flex';
contenedorProductos.style.flexWrap = 'wrap';
contenedorProductos.style.justifyContent = 'center';
contenedorProductos.style.gap = '20px';
contenedorProductos.style.padding = '20px';
// Generar todas las tarjetas
productos.forEach(producto => {
const tarjeta = crearTarjetaProducto(producto);
contenedorProductos.appendChild(tarjeta);
});
// Añadir al DOM
document.body.appendChild(contenedorProductos);
Este ejemplo genera todas las tarjetas de golpe, pero si el catálogo tiene cientos de productos no querrás pintarlos todos a la vez. Ahí es donde entra la paginación: mostrar solo un grupo de tarjetas por página en vez de generarlas todas de una tacada.
El botón "Añadir al carrito" de arriba solo lanza un alert() de mentira. Si quieres ver cómo se hace de verdad, con los productos guardados y persistiendo al recargar la página, tenemos el tutorial completo del carrito de compra.
Sobre el style.propiedad de los ejemplos anteriores
Aquí toca ser honestos: todos los ejemplos de este tutorial usan elemento.style.propiedad = valor para dar color, padding y bordes. Lo hemos hecho así porque es más fácil seguir el ejemplo sin saltar constantemente a un archivo CSS aparte. Pero en un proyecto real, esos estilos inline son justo lo que hay que evitar: si mañana cambias el diseño, tienes que tocar el JavaScript en vez de solo el CSS, y cada elemento lleva su propio bloque de estilos repetido en vez de compartir una regla.
La forma correcta es tener la clase ya definida en tu CSS, y que JavaScript solo se encargue de aplicarla:
.tarjeta-producto {
border: 1px solid #e1e5e9;
border-radius: 10px;
padding: 20px;
transition: transform 0.3s;
}
.tarjeta-producto:hover {
transform: translateY(-5px);
}
const tarjeta = document.createElement('div');
tarjeta.classList.add('tarjeta-producto');
// el :hover del CSS ya se aplica solo, sin necesidad de mouseenter/mouseleave en JS
Fíjate en que el efecto de hover, que en el ejemplo de la tarjeta de producto de más arriba necesitaba dos addEventListener (mouseenter y mouseleave) para cambiar style.transform a mano, aquí lo resuelve el :hover del CSS solo. Es menos código JavaScript y el navegador lo gestiona de forma más eficiente que tú manejándolo con eventos.
✅ Haz esto
- Usa funciones reutilizables: Crea funciones que generen elementos similares
- Separa lógica y presentación: el JS crea y rellena datos, el CSS decide cómo se ve
- Usa classList.add(): mejor que
classNamepara múltiples clases, y es lo que conecta el elemento con tu CSS - Valida datos: Siempre verifica que los datos existen antes de usarlos
❌ Evita esto
- Estilos inline excesivos: cada
elemento.style.x = yes una regla CSS que no puedes reutilizar ni sobreescribir con una media query - Añadir elementos al body: Usa contenedores específicos
- No limpiar event listeners: Puede causar memory leaks
- Crear muchos elementos sin organizar: Estructura tu código en funciones
¿Dónde añadir los elementos creados?
Los elementos creados con createElement deben añadirse a un contenedor HTML existente. Nunca los añadas directamente al body porque se meten con todo lo demás.
Ejemplo: HTML + JavaScript trabajando juntos
<html>
<head>
<title>Mi página</title>
</head>
<body>
<h1>Mi aplicación</h1>
<!-- ✅ Contenedor donde añadir elementos dinámicos -->
<div id="contenedor-dinamico">
<!-- Aquí van los elementos creados por JavaScript -->
</div>
<p>Este párrafo no se ve afectado</p>
<script>
// JavaScript aquí
</script>
</body>
</html>
// 1. Buscar el contenedor donde queremos añadir
const contenedor = document.getElementById('contenedor-dinamico');
// 2. Crear elemento
const boton = document.createElement('button');
boton.textContent = 'Soy dinámico';
// 3. Añadir al contenedor (NO al body)
contenedor.appendChild(boton);
// ❌ MAL: document.body.appendChild(boton);
// ✅ BIEN: contenedor.appendChild(boton);
Diferentes formas de encontrar el contenedor
// Por ID (más común)
const contenedor1 = document.getElementById('mi-contenedor');
// Por clase (el primero que encuentre)
const contenedor2 = document.querySelector('.contenedor-productos');
// Por etiqueta
const contenedor3 = document.querySelector('main');
// Crear elemento y añadir
const elemento = document.createElement('div');
contenedor1.appendChild(elemento);
Ejemplo práctico: Página de productos
<body>
<header>
<h1>Mi tienda</h1>
</header>
<main>
<div id="productos">
<!-- Los productos se crean aquí -->
</div>
</main>
<footer>
<p>© 2025 Mi tienda</p>
</footer>
</body>
// Buscar contenedor
const contenedorProductos = document.getElementById('productos');
// Crear producto
const producto = document.createElement('div');
producto.className = 'producto';
const titulo = document.createElement('h3');
titulo.textContent = 'Laptop Gaming';
const precio = document.createElement('p');
precio.textContent = '€899';
// Anidar elementos
producto.appendChild(titulo);
producto.appendChild(precio);
// Añadir al contenedor específico
contenedorProductos.appendChild(producto);
// El header y footer no se ven afectados
Resumen y siguientes pasos
Con createElement ya puedes crear interfaces dinámicas completas. Los conceptos clave que hemos visto:
// 1. Crear elemento
const elemento = document.createElement('tagName');
// 2. Configurar propiedades
elemento.textContent = 'Contenido';
elemento.className = 'mi-clase';
// 3. Añadir eventos (opcional)
elemento.addEventListener('click', miFuncion);
// 4. Añadir al DOM
contenedor.appendChild(elemento);
Lo que puedes hacer ahora:
- 📋 Lista de tareas interactiva con añadir/eliminar
- 🛒 Carrito de compras dinámico
- 📊 Dashboard con gráficos generados por código
- 💬 Chat en tiempo real con mensajes dinámicos
- 🎮 Juegos simples como tic-tac-toe o memoria
Siguiente nivel: Aprende sobre Web Components, Virtual DOM y frameworks como React que hacen esto mismo pero de forma más avanzada.
Artículos relacionados
🚀 ¿Te gustó este tutorial?
Únete a StudyCode Pro para más tutoriales como este, roadmaps completos y acceso a nuestra comunidad de developers que te ayudará a conseguir tu primer trabajo en tech.
📬 Únete Gratis