Una página web la escriben tres lenguajes, cada uno con su encargo:
JavaScript es un lenguaje de programación completo: calcula, valida formularios, guarda datos, pide información a un servidor. En estos apuntes se emplea solo para una parte de lo que sabe hacer: modificar la página una vez cargada.
navigator, window y documentAntes de tocar la página con JavaScript conviene saber qué partes del navegador están a su alcance. Son tres objetos, uno dentro de otro:
navigatornavigator.language
devuelve "es-ES". Es el mismo para todas las pestañas abiertas.windowwindow.innerWidth devuelve el
ancho del viewport en píxeles: es la medida que consultan las media queries.documentwindow con su propio document: el navigator es el mismo para las dos.Cada pestaña tiene su propio window y su propio document, y lo que
se escribe en una no afecta a la otra. Por eso, al escribir document.getElementById(…)
no hace falta decir de qué página se habla: siempre es la que contiene el script.
Al abrir una página, el navegador lee el archivo HTML y construye con él un árbol de objetos en memoria. Ese árbol es el DOM, y es lo que el navegador dibuja en pantalla.
El archivo y el árbol no son lo mismo. El archivo se lee una vez y se acabó; el árbol está vivo mientras la página esté abierta, y puede cambiar sin que el archivo cambie.
En la asignatura ya hemos trabajado sobre el DOM en dos ocasiones:
display: none. El elemento desaparece de la pantalla
pero sigue en el árbol. Por eso el submenú de un desplegable puede
volver a mostrarse sin tener que crearlo otra vez.Este documento HTML
<html>
<head>
<title></title>
<meta>
<style></style>
</head>
<body>
<header></header>
<main></main>
<footer></footer>
</body>
</html>
produce este árbol:
html
│
┌────────────┴────────────┐
head body
│ │
┌────────┼────────┐ ┌────────┼────────┐
title meta style header main footer
Sobre él se definen los términos:
html.head es padre de
title, meta y style; esos tres son sus hijos.head y body son hermanos, y
también lo son header, main y footer. En cambio
title y header no lo son: están al mismo nivel pero tienen
padres distintos.footer es hijo de body y descendiente de body y de
html.Un selector descendiente de CSS es un recorrido por ese árbol.
nav ul li a designa los nodos a que descienden de un li,
que desciende de un ul, que desciende de un nav. El selector
.con-submenu:hover .submenu parte de un nodo y desciende hasta otro.
Cada relación del árbol tiene su selector:
| Relación | En el árbol de arriba | Selector |
|---|---|---|
| Descendiente | footer desciende de html |
html footer |
| Hijo | header es hijo de body |
body > header |
| Hermano siguiente | main viene detrás de header |
header + main |
| Raíz | html |
:root |
El CSS lee el árbol y le aplica estilos. JavaScript puede además modificarlo y reaccionar a lo que ocurre. Son tres capacidades:
Tres vías, que corresponden a las tres formas de identificar un elemento en HTML:
por id, por etiqueta y por clase.
getElementById("menu")id es ese, o null
si no existe. El id es único en el documento, así que solo puede devolver
uno. No lleva almohadilla.getElementsByTagName("p")getElementsByClassName("foto")Las dos últimas llevan plural —getElementsBy…— y
devuelven siempre una colección, aunque solo encuentren un elemento.
document.getElementById("menu") // id, sin #
document.getElementsByTagName("p") // todos los párrafos
document.getElementsByClassName("foto") // clase, sin punto
Las tres parten de document, que representa el documento entero, pero
también pueden partir de un elemento ya seleccionado. En ese caso la búsqueda se limita a
sus descendientes:
const galeria = document.getElementById("galeria");
const fotos = galeria.getElementsByTagName("img"); // solo las de dentro de la galería
getElementById devuelve un elemento, con el que se puede trabajar
directamente. Los otros dos devuelven una colección, y una colección no es
un elemento: no se le puede cambiar el texto ni la clase. Hay que entrar en ella por el
número de orden, que empieza en cero, o recorrerla entera.
const fotos = document.getElementsByClassName("foto");
fotos[0].classList.add("vista"); // solo la primera
for (let i = 0; i < fotos.length; i++) { // todas, una por una
fotos[i].classList.add("vista");
}
fotos.length dice cuántas hay. Y la colección está viva: si
más tarde se añade al documento otro elemento con esa clase, aparece en ella sin volver a
buscar, y length cambia solo.
Teniendo un elemento se puede llegar a sus parientes, con los nombres del vocabulario de antes:
caja.parentElement // su padre
caja.children // sus hijos, en una colección
caja.firstElementChild // el primer hijo
caja.lastElementChild // el último
caja.nextElementSibling // el hermano siguiente
caja.previousElementSibling // el hermano anterior
caja.closest(".card") // el antepasado más cercano que cumpla ese selector
closest sube por el árbol, que es justo lo que los selectores de CSS no
pueden hacer.
Sobre un elemento seleccionado se puede modificar su contenido, sus clases y sus atributos.
El contenido:
caja.textContent = "Otro texto"; // el texto, sin interpretar etiquetas caja.innerHTML = "<strong>Otro</strong>"; // interpretando el HTML
Entre los dos, textContent es el seguro. innerHTML interpreta lo
que se le pase como HTML, de modo que si ese texto procede de lo que alguien ha escrito en un
formulario, se estaría ejecutando código ajeno dentro de la página.
Las clases:
caja.classList.add("activa"); // añadir
caja.classList.remove("activa"); // quitar
caja.classList.toggle("activa"); // alternar
caja.classList.contains("activa") // consultar: true o false
Los atributos:
enlace.getAttribute("href") // leer
enlace.setAttribute("href", "work.html"); // escribir
boton.removeAttribute("disabled"); // eliminar
imagen.src = "IMG/otra.jpg"; // los más usados tienen atajo
imagen.alt = "Otra foto";
campo.value = "";
También se puede escribir directamente en el estilo del elemento:
caja.style.backgroundColor = "red"; // se desaconseja
Se desaconseja porque equivale a un atributo style escrito a mano: gana a
todo lo de la hoja de estilos, dispersa la apariencia entre dos ficheros y obliga a
desandarlo para quitarlo. Lo que corresponde es cambiar la clase y dejar la apariencia
en el CSS, como se explica más abajo.
Un evento es un suceso que el navegador detecta: un clic, una pulsación de tecla, el
final de una animación. addEventListener registra una función que se ejecutará
cada vez que ese suceso se produzca en ese elemento.
boton.addEventListener("click", function () {
// lo que hay que hacer cuando ocurra
});
La función no se ejecuta al declararla: queda registrada y el navegador la invoca cuando el suceso ocurre.
En los ejemplos que siguen, el código lleva dos colores:
id de un elemento y el nombre de una clase. Salen en verde en cada pestaña
donde aparecen —en el HTML donde se ponen, en el CSS donde se usan como selector, en el
JavaScript donde se buscan—, de modo que se ve que son el mismo nombre. Cambiarlo en un
sitio obliga a cambiarlo en los otros.El resto —document, addEventListener, function, los
nombres de etiqueta— son palabras del lenguaje o del navegador, y van en negro.
un clic que cambia el texto
<div class="row"> <button id="boton">Púlsame</button> <div class="caja" id="aviso">Todavía no me han pulsado</div> </div>
.caja { background: #dcdcea; border: 2px solid #8686a8; padding: 1rem 1.2rem; font-family: Verdana, sans-serif; font-size: .85rem; } .row { display: flex; gap: 10px; align-items: flex-start; } button { font: inherit; font-size: .85rem; padding: .4rem .9rem; border: 1px solid #8686a8; background: #e8e8f2; cursor: pointer; }
const boton = document.getElementById("boton"); const aviso = document.getElementById("aviso"); boton.addEventListener("click", function () { aviso.textContent = "Me han pulsado"; });
Hay muchos; estos son los que se encuentran siempre.
click, dblclick, mouseenter,
mouseleave, mousemove.keydown, keyup.submit al enviarlo, input mientras se escribe,
change al terminar de cambiar un campo, focus y
blur al entrar y salir.DOMContentLoaded cuando el árbol está construido,
load cuando además han llegado las imágenes, scroll,
resize.transitionend y animationend, para encadenar algo justo
cuando termina el movimiento.En W3Schools y en mucho código antiguo se ve esto:
<button onclick="alert('hola')">Púlsame</button>
Funciona, pero es la forma antigua y tiene dos problemas: mezcla el comportamiento con
la estructura, que es justo lo que se lleva separando todo el curso, y solo admite
una reacción por evento, porque la segunda pisa a la primera. Con
addEventListener se pueden apuntar las que haga falta.
El navegador trae tres ventanas de aviso hechas. No hay que construirlas ni darles estilo: se invocan y aparecen.
alert("texto")confirm("pregunta")true o
false.prompt("pregunta")null si se cancela.Las tres detienen la página mientras están abiertas: no se puede hacer nada más hasta cerrarlas.
alert: un mensaje y un botón
<div class="row"> <button id="oferta">Ver la oferta de hoy</button> </div>
.row { display: flex; gap: 10px; align-items: flex-start; }
button {
font: inherit;
font-size: .85rem;
padding: .4rem .9rem;
border: 1px solid #8686a8;
background: #e8e8f2;
cursor: pointer;
}
const boton = document.getElementById("oferta"); boton.addEventListener("click", function () { alert("Bienvenido a la tienda. Hoy tiene un 5 % de descuento en todo el catálogo."); });
Aquí no basta con informar: hay que saber qué ha contestado. Para eso sirve
confirm, cuyo valor de retorno se guarda en una variable y se consulta con un
if.
confirm: la respuesta se guarda y se consulta
<div class="row"> <button id="cookies">Preguntar por las cookies</button> <div class="caja" id="resultado">Sin respuesta todavía</div> </div>
.caja { background: #dcdcea; border: 2px solid #8686a8; padding: 1rem 1.2rem; font-family: Verdana, sans-serif; font-size: .85rem; } .row { display: flex; gap: 10px; align-items: flex-start; } button { font: inherit; font-size: .85rem; padding: .4rem .9rem; border: 1px solid #8686a8; background: #e8e8f2; cursor: pointer; }
const boton = document.getElementById("cookies"); const resultado = document.getElementById("resultado"); boton.addEventListener("click", function () { const acepta = confirm("Respetamos tu privacidad. Acepta las cookies y continúa navegando la web sin límites."); if (acepta) { resultado.textContent = "Cookies aceptadas"; } else { resultado.textContent = "Cookies rechazadas"; } });
Las tres tienen tres inconvenientes: no admiten estilo alguno, detienen la página, y el navegador permite silenciarlas. Un aviso de cookies hecho así sería ilegible en un móvil y el usuario podría bloquearlo.
Lo que se hace es escribir el aviso como un elemento más del documento, con su CSS, y quitarlo del árbol o taparlo con una clase cuando se responde. El aspecto del aviso está en la hoja de estilos; JavaScript solo le cambia la clase.
el mismo aviso, hecho como elemento del documento
<div class="cookie-banner" id="aviso"> <p>Respetamos tu privacidad. Acepta las cookies y continúa navegando la web sin límites.</p> <div class="row"> <button id="aceptar">Aceptar</button> <button id="rechazar">Rechazar</button> </div> </div> <p class="caja" id="estado">Sin respuesta todavía</p>
.caja { background: #dcdcea; border: 2px solid #8686a8; padding: 1rem 1.2rem; font-family: Verdana, sans-serif; font-size: .85rem; } .row { display: flex; gap: 10px; align-items: flex-start; } button { font: inherit; font-size: .85rem; padding: .4rem .9rem; border: 1px solid #8686a8; background: #e8e8f2; cursor: pointer; } .cookie-banner { padding: 1rem 1.2rem; background: #2a2a55; color: #fff; font-family: Verdana, sans-serif; font-size: .85rem; transition: opacity 300ms; } .cookie-banner p { margin: 0 0 .7rem; } /* el estado respondido: lo único que JavaScript añade */ .cookie-banner--answered { opacity: 0; visibility: hidden; }
const aviso = document.getElementById("aviso"); const estado = document.getElementById("estado"); function responder(texto) { estado.textContent = texto; aviso.classList.add("cookie-banner--answered"); } document.getElementById("aceptar").addEventListener("click", function () { responder("Cookies aceptadas"); }); document.getElementById("rechazar").addEventListener("click", function () { responder("Cookies rechazadas"); });
Las dos funciones del final llaman a una tercera, responder, en vez de
repetir las dos líneas cada una. Lo que cambia entre ambas es un texto, y eso es
exactamente lo que se le pasa como argumento.
El aspecto lo define el CSS. Para cambiarlo con la página ya abierta, JavaScript opera sobre el DOM: selecciona un elemento y le añade, le quita o le modifica una clase o un atributo. El significado de esa clase está escrito en la hoja de estilos, y es la hoja de estilos la que produce el cambio.
Así se trabajará en estos apuntes. JavaScript puede además escribir valores de estilo directamente sobre un elemento —y en algún ejemplo hará falta—, pero entonces el aspecto queda repartido entre dos archivos y hay que buscarlo en dos sitios.
JavaScript pone la clase; el movimiento lo hace el CSS
<div class="row"> <button id="interruptor">Pulsa el botón para aceptar</button> </div> <div class="panel" id="panel"> Muchas gracias por aceptar </div>
.row { display: flex; gap: 10px; align-items: flex-start; } button { font: inherit; font-size: .85rem; padding: .4rem .9rem; border: 1px solid #8686a8; background: #e8e8f2; cursor: pointer; } .panel { margin-top: 10px; padding: 1rem 1.2rem; background: #dcdcea; border: 2px solid #8686a8; font-family: Verdana, sans-serif; font-size: .85rem; /* el estado cerrado */ opacity: 0; transform: translateY(-10px); transition: opacity 350ms, transform 350ms; } /* el estado abierto: lo único que JavaScript añade o quita */ .panel.abierto { opacity: 1; transform: translateY(0); }
const boton = document.getElementById("interruptor"); const panel = document.getElementById("panel"); boton.addEventListener("click", function () { panel.classList.toggle("abierto"); boton.textContent = panel.classList.contains("abierto") ? "Cerrar" : "Abrir"; });
El JavaScript del ejemplo no contiene ningún color, medida ni duración. Modificar la forma en que el panel aparece exige cambiar el CSS y nada más.
Los disparadores de CSS tienen dos limitaciones. :hover no existe en una
pantalla táctil, y :checked exige que el elemento disparador preceda en el
HTML al elemento afectado, porque los selectores no ascienden ni retroceden.
JavaScript no está sujeto a esa restricción: alcanza cualquier nodo del árbol con independencia de su posición. De ahí que el menú de tipo hamburguesa, en el que un botón situado en la cabecera despliega una lista que puede estar en cualquier punto del documento, se resuelva por esta vía.
un menú que se abre con un botón, no con el ratón
<button id="hamburguesa" aria-expanded="false">Menú</button> <nav class="dropdown" id="menu"> <ul> <li><a href="#">Inicio</a></li> <li><a href="#">Work</a></li> <li><a href="#">About me</a></li> <li><a href="#">Contact</a></li> </ul> </nav>
button {
font: inherit;
font-size: .85rem;
padding: .4rem .9rem;
border: 1px solid #8686a8;
background: #e8e8f2;
cursor: pointer;
}
.dropdown {
overflow: hidden;
max-height: 0; /* cerrado */
transition: max-height 400ms ease-out;
}
.dropdown--open {
max-height: 12rem; /* abierto */
}
.dropdown ul {
display: flex;
flex-direction: column;
gap: 2px;
list-style: none;
margin: 10px 0 0;
padding: 0;
}
.dropdown a {
display: block;
padding: 8px 14px;
background: #4a4a85;
color: #fff;
text-decoration: none;
font-family: Verdana, sans-serif;
font-size: .85rem;
}
.dropdown a:hover { background: #6a6aa5; }
const boton = document.getElementById("hamburguesa"); const menu = document.getElementById("menu"); boton.addEventListener("click", function () { const abierto = menu.classList.toggle("dropdown--open"); // se le dice también a quien navega sin ver la pantalla boton.setAttribute("aria-expanded", abierto); });
aria-expanded no altera la presentación: es la información que recibe
quien navega con un lector de pantalla. Un control que despliega contenido debe declarar su
estado.
Hasta aquí JavaScript ha cambiado texto y clases. También puede cambiar
atributos, y el caso más frecuente es el src de una imagen:
sustituir un archivo por otro sin recargar la página.
Hacen falta dos cosas: una variable que recuerde en qué estado está, y un
if que decida qué archivo poner. La variable es necesaria porque el navegador
no guarda nada entre un clic y el siguiente: cada vez que la función se ejecuta, empieza sin
memoria de lo que hizo la vez anterior.
Los dos archivos de este ejemplo (bombilla-on.svg y
bombilla-off.svg) están en la carpeta IMG.
cambiar el src de una imagen
<div class="row"> <img src="IMG/bombilla-off.svg" alt="Bombilla apagada" id="bombilla" class="bombilla"> <button id="interruptor-luz">Encender</button> </div>
.row { display: flex; gap: 10px; align-items: flex-start; } button { font: inherit; font-size: .85rem; padding: .4rem .9rem; border: 1px solid #8686a8; background: #e8e8f2; cursor: pointer; } .bombilla { width: 5rem; height: auto; }
const bombilla = document.getElementById("bombilla"); const boton = document.getElementById("interruptor-luz"); let encendida = false; // el estado: empieza apagada boton.addEventListener("click", function () { encendida = !encendida; // el ! invierte: true pasa a false y al contrario if (encendida) { bombilla.src = "IMG/bombilla-on.svg"; bombilla.alt = "Bombilla encendida"; boton.textContent = "Apagar"; } else { bombilla.src = "IMG/bombilla-off.svg"; bombilla.alt = "Bombilla apagada"; boton.textContent = "Encender"; } });
El alt se cambia junto con el src. Si no, quien no ve la
imagen seguiría leyendo que la bombilla está apagada.
let, y no const: el valor de encendida cambia en
cada clic, y lo que se declara con const no se puede volver a asignar. Las
referencias a los elementos sí van con const, porque se buscan una vez y no se
reasignan nunca.
Con dos archivos basta un if. Con cinco imágenes haría falta un
if de cinco ramas, y con veinte sería inviable. El planteamiento que escala es
distinto: se ponen todas las imágenes en el documento y se oculta todas
menos una.
Eso exige dos piezas:
getElementsByClassName. Sus elementos se numeran desde cero:
la primera es la 0, la segunda la 1, y
fotos.length dice cuántas hay.Y una función que, dado el índice, recorra la colección y deje visible solo esa. El
recorrido se hace con un bucle for que va de 0 a
fotos.length, tomando en cada vuelta la imagen que ocupa el lugar
i.
dos imágenes, un índice y display: none
<div class="slideshow"> <img class="slide" src="IMG/naturaleza-abetos.jpg" alt="Arroyo entre abetos bajo un cielo nublado"> <img class="slide" src="IMG/naturaleza-valle.jpg" alt="Valle con niebla al amanecer"> </div> <div class="row"> <button id="pasar">Pasar a la siguiente</button> <div class="caja" id="contador">1 de 2</div> </div>
.caja { background: #dcdcea; border: 2px solid #8686a8; padding: 1rem 1.2rem; font-family: Verdana, sans-serif; font-size: .85rem; } .row { display: flex; gap: 10px; align-items: flex-start; } button { font: inherit; font-size: .85rem; padding: .4rem .9rem; border: 1px solid #8686a8; background: #e8e8f2; cursor: pointer; } .slideshow { max-width: 26rem; margin-bottom: .8rem; } .slideshow img { display: block; width: 100%; aspect-ratio: 3 / 2; /* todas con la misma, para que no salte el alto */ object-fit: cover; }
const fotos = document.getElementsByClassName("slide"); const boton = document.getElementById("pasar"); const contador = document.getElementById("contador"); let indice = 0; // la primera de la colección function mostrar(n) { for (let i = 0; i < fotos.length; i++) { fotos[i].style.display = (i === n) ? "block" : "none"; } contador.textContent = (n + 1) + " de " + fotos.length; } mostrar(indice); // el estado inicial, antes de cualquier clic boton.addEventListener("click", function () { indice = indice + 1; if (indice >= fotos.length) { indice = 0; // pasada la última, vuelve a la primera } mostrar(indice); });
Tres detalles que conviene retener, porque la clase siguiente se construye sobre ellos.
Las imágenes se ocultan desde JavaScript, no desde el CSS. Si el estado
inicial se pusiera en la hoja de estilos habría que escribir una regla distinta para cada
imagen. La llamada a mostrar(indice) antes de registrar el evento deja la
primera visible y las demás ocultas, sea cual sea su número.
El i === n devuelve true o false,
y el operador ? : elige entre dos valores según cuál sea. Equivale a un
if de dos ramas escrito en una línea.
Este ejemplo usa style.display, que es la excepción al
criterio de dejar la apariencia en el CSS. Se admite porque mostrar y ocultar no es
apariencia, sino estado, y porque no hay clase que sirva igual para un número indeterminado
de imágenes. Si hiciera falta una transición de entrada, sí habría que pasar a una clase,
porque un display no se puede transicionar.
Un slideshow se gobierna con dos flechas. No hace falta ninguna imagen para dibujarlas: son caracteres, y se escriben de tres maneras.
<button>←</button> <button>→</button> <button>‹</button> <button>›</button>
Funciona si el archivo está en UTF-8, como corresponde. Es lo más legible al leer el HTML.
Se escribe &#, el número del carácter y un punto y coma. Es lo que se
encuentra en los manuales, porque no depende de poder teclear el carácter:
← ← flecha a la izquierda → → flecha a la derecha ❮ ❮ ángulo izquierdo (el de los pases de imágenes) ❯ ❯ ángulo derecho
Un elemento sin contenido, de ancho y alto cero, al que se le dan tres bordes transparentes y uno con color: lo que queda a la vista es un triángulo. Es un recurso antiguo y hoy no se justifica, porque un carácter hace lo mismo y se puede leer.
Las dos flechas hacen lo mismo con un dato distinto. Lo que corresponde entonces no es escribir dos funciones, sino una función con un argumento, llamada dos veces con valores distintos. El argumento es el sentido.
dos flechas que llaman a la misma función
<div class="row"> <button class="arrow" id="izquierda" aria-label="Anterior">❮</button> <button class="arrow" id="derecha" aria-label="Siguiente">❯</button> <div class="caja" id="aviso-flecha">Todavía no se ha pulsado ninguna flecha</div> </div>
.caja { background: #dcdcea; border: 2px solid #8686a8; padding: 1rem 1.2rem; font-family: Verdana, sans-serif; font-size: .85rem; } .row { display: flex; gap: 10px; align-items: flex-start; } button { font: inherit; font-size: .85rem; padding: .4rem .9rem; border: 1px solid #8686a8; background: #e8e8f2; cursor: pointer; } .arrow { font-size: 1.3rem; line-height: 1; padding: .3rem .8rem; }
const aviso = document.getElementById("aviso-flecha"); function mover(sentido) { aviso.textContent = "a la " + sentido; } document.getElementById("izquierda").addEventListener("click", function () { mover("izquierda"); }); document.getElementById("derecha").addEventListener("click", function () { mover("derecha"); });
El aria-label es necesario: el contenido del botón es un signo de
puntuación, y un lector de pantalla leería «ángulo izquierdo» en vez de «anterior».
Repetir el addEventListener por cada flecha funciona con dos, no con
muchas. La alternativa es escribir el dato en el propio HTML, con un
atributo data-, y registrar el evento una sola vez recorriendo la colección:
<button class="arrow" data-sentido="izquierda">❮</button> <button class="arrow" data-sentido="derecha">❯</button>
const flechas = document.getElementsByClassName("arrow");
for (let i = 0; i < flechas.length; i++) {
flechas[i].addEventListener("click", function () {
mover(flechas[i].dataset.sentido);
});
}
Los atributos que empiezan por data- son de uso libre: el navegador no les
da ningún significado y se leen desde JavaScript con dataset, quitando el
data- del nombre. Así el HTML declara qué hace cada botón y el JavaScript no
contiene ninguna lista de identificadores.
Si el navegador encuentra el <script> antes que los elementos, el
árbol aún no los contiene y getElementById devuelve null. Hay dos
soluciones equivalentes:
<!-- al final del body, cuando ya está todo -->
<script src="main.js"></script>
</body>
<!-- o en el head, pidiendo que espere -->
<script src="main.js" defer></script>
addEventListener registra la función que lo atiende.body o con defer.alert, confirm y prompt son avisos del
navegador: no admiten estilo y detienen la página. Un aviso de verdad se escribe como
elemento del documento.let declarada fuera de la función.if; con muchas, una
colección y un índice.Las fotos son de Pexels, con licencia libre: se pueden usar y modificar sin pagar y sin citar la fuente. Citarla no es obligatorio, pero es lo correcto, y aquí sirve además de ejemplo de cómo se hace.
naturaleza-abetos.jpg — Arroyo entre abetos bajo un cielo nublado. Foto de Snapwire, ficha en Pexels.naturaleza-valle.jpg — Valle con niebla al amanecer. Foto de Johannes Plenio, ficha en Pexels.