Una guía práctica para entender cómo funciona localStorage en el navegador mientras construyes una app de notas persistente con JavaScript puro.
Si prefieres seguir el proceso visualmente, empieza con el vídeo completo y usa la guía escrita como referencia cuando la necesites.
Al actualizar una página, los valores normales creados por JavaScript se reinician. En esta guía aprenderás a conservar pequeños datos del navegador después de actualizar la página mientras construyes una app completa de notas con HTML, CSS y JavaScript.
El proyecto comienza con una interfaz de notas preparada. Añadiremos JavaScript poco a poco para que cada paso tenga un resultado visible: primero guardaremos un valor, después lo restauraremos y finalmente administraremos una colección de notas. Al terminar, la app podrá crear, editar, eliminar, guardar, restaurar y recuperar notas de forma segura.
En el capítulo 1 del video, el HTML y el CSS ya están preparados para que podamos concentrarnos en JavaScript. Puedes copiar el punto de partida completo de abajo en tu propio proyecto. Crea estos dos archivos, index.html y style.css, en la misma carpeta y después crea un archivo index.js vacío. El HTML ya conecta el archivo JavaScript al final.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Focus Notes</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="app-shell">
<header class="hero">
<div>
<p class="eyebrow">Your quiet space to think</p>
<h1 class="heading">Focus Notes</h1>
<p class="info-text">
Capture an idea, keep it automatically, and double-click any note
when you are done with it.
</p>
</div>
<div class="status-pill">Saved locally</div>
</header>
<section class="app" id="app" aria-label="Your notes">
<button class="btn" id="btn" type="button" aria-label="Add a note">
+ New note
</button>
</section>
</main>
<script src="index.js"></script>
</body>
</html>
Este HTML y este CSS son solamente el punto de partida. El botón New Note todavía no funcionará porque index.js está vacío. Es intencional: desde este punto del video, el objetivo es añadir el comportamiento con JavaScript y hacer que los datos sobrevivan a una actualización.
Comienza con una variable normal de JavaScript:
let nota = "Planifica las tareas de mañana";
console.log(nota);
El valor existe en la memoria de la página actual, por eso aparece en la Consola. Si cambias la variable manualmente en DevTools, el cambio solo dura hasta que actualizas la página. Al actualizar, el script vuelve a ejecutarse y crea la variable original otra vez.
Ese es el problema que resuelve localStorage. Proporciona a cada sitio web un pequeño almacén de datos de tipo clave-valor que permanece en el navegador.
Usa setItem() para guardar un valor con una clave:
let nota = "Planifica las tareas de mañana";
localStorage.setItem("nota", nota);
El primer argumento, "nota", es la clave. El segundo argumento es el valor. Más tarde puedes usar la misma clave con getItem():
const notaGuardada = localStorage.getItem("nota");
console.log(notaGuardada);
El navegador conserva este valor para el origen actual del sitio, por lo que puede leerse después de actualizar la página. En DevTools, abre el panel Application, expande Local Storage y selecciona el sitio actual para ver la clave y el valor.
La página no tiene que cambiar visualmente para que esto funcione. El almacenamiento ocurre en segundo plano; el siguiente paso es conectar esos datos con la interfaz de notas.
Cuando la app puede tener más de una nota, representa cada nota como un objeto. Una nota sencilla necesita un ID estable y su texto:
{
id: "nota-1",
text: "Planifica las tareas de mañana"
}
Durante la ejecución, guarda la colección en un array:
const notas = [
{ id: "nota-1", text: "Planifica las tareas de mañana" },
{ id: "nota-2", text: "Comprar alimentos" },
];
El ID es importante porque permite encontrar el objeto correcto cuando una nota se edita o se elimina. Después, la interfaz puede convertir cada objeto en un textarea dentro del contenedor de notas preparado.
localStorage no guarda arrays ni objetos de JavaScript directamente. Convierte el array de notas en una cadena JSON antes de guardarlo:
function guardarNotas(notas) {
localStorage.setItem("notas", JSON.stringify(notas));
}
Cuando se carga la página, lee la cadena y conviértela otra vez en datos de JavaScript:
function obtenerNotas() {
const notasGuardadas = localStorage.getItem("notas");
return notasGuardadas ? JSON.parse(notasGuardadas) : [];
}
El flujo completo es:
Array de JavaScript → JSON.stringify() → cadena de localStorage
Cadena de localStorage → JSON.parse() → array de JavaScript
Llama a guardarNotas() después de cada cambio. Llama a obtenerNotas() cuando comienza la app y muestra el array devuelto para que las notas guardadas aparezcan después de actualizar la página.
La interfaz preparada ya contiene un contenedor de notas y un botón New Note. El botón debe crear un objeto nuevo, añadirlo al array, mostrar su textarea y guardar el array actualizado.
La idea principal es actualizar juntos el modelo de datos y la página:
function crearNota() {
const nota = {
id: crypto.randomUUID(),
text: "",
};
notas.push(nota);
mostrarNota(nota);
guardarNotas(notas);
}
Cuando cambia el textarea, actualiza el objeto que tiene el mismo ID y vuelve a guardar:
element.addEventListener("input", () => {
const nota = notas.find((item) => item.id === id);
nota.text = element.value;
guardarNotas(notas);
});
Así, las ediciones sobreviven a una actualización de la página en lugar de existir solamente en el DOM actual.
Para eliminar una nota, busca su posición en el array, elimina el objeto, elimina su elemento de la página y guarda el array más corto:
function eliminarNota(id, element) {
const index = notas.findIndex((nota) => nota.id === id);
notas.splice(index, 1);
element.remove();
guardarNotas(notas);
}
En el tutorial, esta función se conecta a un doble clic sobre cada nota. El mismo ID se utiliza para el objeto y para su textarea, así que la nota correcta se elimina de ambos lugares.
Cuando se elimina la última nota, elimina también la clave de almacenamiento en lugar de dejar un array vacío innecesario:
function guardarNotas(notas) {
if (notas.length === 0) {
localStorage.removeItem("notas");
return;
}
localStorage.setItem("notas", JSON.stringify(notas));
}
Los datos guardados pueden no existir, ser inválidos o ser JSON válido con una estructura incorrecta. Una llamada directa a JSON.parse() puede lanzar un error y detener el resto de la app.
Protege la función de carga con una comprobación de datos faltantes, try...catch y una validación del array:
function obtenerNotas() {
const notasGuardadas = localStorage.getItem("notas");
if (!notasGuardadas) return [];
try {
const notasParseadas = JSON.parse(notasGuardadas);
return Array.isArray(notasParseadas) ? notasParseadas : [];
} catch (error) {
localStorage.removeItem("notas");
return [];
}
}
Si el valor está dañado, la app elimina la entrada incorrecta y comienza con una colección vacía en lugar de fallar por completo. Array.isArray() es importante porque un JSON válido también puede ser un objeto, una cadena o un número, no necesariamente el array que espera la app.
localStorage es útil para datos pequeños, cómodos y no sensibles, como:
No es un lugar seguro para contraseñas, tokens de autenticación, mensajes privados ni otros datos sensibles. La información guardada allí pertenece al perfil del navegador y el usuario puede inspeccionarla o modificarla.
Tu app de notas está completa cuando puede:
El patrón principal es sencillo: mantén los datos en JavaScript, conviértelos a JSON al guardar, vuelve a convertirlos al cargar y actualiza la interfaz cada vez que cambien los datos.
:root {
color-scheme: dark;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
background: #090b16;
color: #f7f7ff;
}
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
padding: 72px 24px;
background:
radial-gradient(circle at 12% 10%, rgba(124, 92, 255, 0.3), transparent 34%),
radial-gradient(circle at 88% 18%, rgba(0, 209, 255, 0.2), transparent 30%),
linear-gradient(145deg, #090b16 0%, #11142a 50%, #080a14 100%);
}
body::before {
position: fixed;
inset: 0;
z-index: -1;
background-image: linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
background-size: 42px 42px;
content: "";
mask-image: linear-gradient(to bottom, black, transparent 82%);
}
.app-shell {
width: min(1120px, 100%);
margin: 0 auto;
padding: 38px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 32px;
background: rgba(15, 18, 37, 0.72);
box-shadow: 0 30px 90px rgba(0, 0, 0, 0.38);
backdrop-filter: blur(24px);
}
.hero {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 32px;
padding: 6px 4px 34px;
}
.eyebrow {
margin: 0 0 12px;
color: #a99cff;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.heading {
margin: 0;
font-size: clamp(2.5rem, 6vw, 4.7rem);
letter-spacing: -0.055em;
line-height: 0.96;
}
.info-text {
max-width: 610px;
margin: 18px 0 0;
color: #aeb2ca;
font-size: 1rem;
line-height: 1.7;
}
.status-pill {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
gap: 9px;
padding: 10px 14px;
border: 1px solid rgba(122, 255, 196, 0.18);
border-radius: 999px;
background: rgba(52, 211, 153, 0.08);
color: #9cf5cc;
font-size: 0.78rem;
font-weight: 750;
}
.status-pill::before {
content: "";
width: 8px;
height: 8px;
border-radius: 50%;
background: #4ade80;
box-shadow: 0 0 14px #4ade80;
}
.app {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 18px;
}
.note,
.btn {
min-height: 230px;
border-radius: 22px;
}
.note {
width: 100%;
padding: 24px;
resize: none;
border: 1px solid rgba(255, 255, 255, 0.1);
outline: none;
background: linear-gradient(145deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.045));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
color: #f5f3ff;
font: inherit;
font-size: 1rem;
line-height: 1.65;
transition: border-color 180ms ease, transform 180ms ease, box-shadow 180ms ease;
}
.note::placeholder {
color: #737994;
}
.note:hover,
.note:focus {
border-color: rgba(154, 134, 255, 0.55);
box-shadow: 0 18px 40px rgba(4, 6, 18, 0.34), 0 0 0 4px rgba(124, 92, 255, 0.08);
transform: translateY(-3px);
}
.btn {
display: grid;
place-content: center;
gap: 12px;
border: 1px dashed rgba(169, 156, 255, 0.48);
background: rgba(124, 92, 255, 0.07);
color: #b9adff;
cursor: pointer;
font-size: 0.82rem;
font-weight: 800;
letter-spacing: 0.1em;
text-transform: uppercase;
transition: border-color 180ms ease, background 180ms ease, transform 180ms ease;
}
.btn:hover {
border-color: #a99cff;
background: rgba(124, 92, 255, 0.16);
transform: translateY(-3px);
}
@media (max-width: 680px) {
body {
padding: 28px 14px;
}
.app-shell {
padding: 26px 20px;
border-radius: 24px;
}
.hero {
flex-direction: column;
}
}