Guardar datos en el navegador con localStorage

localStorage guarda datos en el navegador del usuario y los conserva aunque cierre la pestaña o apague el equipo. Es lo más sencillo para recordar preferencias, como el tema oscuro o el último filtro elegido.

Guardar, leer y borrar

Para probarlo, guarda el código en un archivo .html, ábrelo en el navegador y abre la consola con F12 (en Mac, Cmd+Opción+J). Todos los ejemplos de esta página son un único archivo, sin nada más que instalar.

<!doctype html>
<html lang="es">
<head><meta charset="utf-8"><title>localStorage</title></head>
<body>
  <script type="module">
    localStorage.clear();

    // Guardar y leer
    localStorage.setItem("tema", "oscuro");
    console.log(localStorage.getItem("tema"));
    console.log(localStorage.getItem("no-existe"));

    // Solo guarda cadenas
    localStorage.setItem("visitas", 5);
    localStorage.setItem("usuario", { nombre: "Ana" });
    console.log(typeof localStorage.getItem("visitas"), localStorage.getItem("visitas") + 1);
    console.log(localStorage.getItem("usuario"));

    // Con JSON sí
    const prefs = { tema: "oscuro", tamano: 18, avisos: ["correo"] };
    localStorage.setItem("prefs", JSON.stringify(prefs));
    const leidas = JSON.parse(localStorage.getItem("prefs"));
    console.log(leidas.tamano + 1, leidas.avisos[0]);

    // Recorrer, borrar una, borrar todo
    console.log(localStorage.length, Object.keys(localStorage).sort().join(", "));
    localStorage.removeItem("usuario");
    console.log(localStorage.length);
  </script>
</body>
</html>
consolaoscuro
null
string 51
[object Object]
19 correo
4 prefs, tema, usuario, visitas
3

La API son cuatro métodos: setItem(clave, valor), getItem(clave), removeItem(clave) y clear(). Si la clave no existe, getItem devuelve null, no undefined.

Solo guarda texto

Es la sorpresa de las líneas tres y cuatro. setItem("visitas", 5) guarda la cadena "5", y al leerla y sumarle 1 sale "51", porque se pegan dos textos. Un objeto es peor: se guarda como "[object Object]" y el contenido se pierde para siempre. Para guardar números, listas u objetos hay que pasarlos por JSON.stringify al guardar y por JSON.parse al leer, como hace el ejemplo con prefs.

Cada origen tiene su propio localStorage, que solo guarda pares de cadenaslocalStorage: un almacén de cadenas por cada origenhttps://decodigo.comclavevalor (siempre texto)temaoscurovisitas5prefs{«tema»:»oscuro»}https://otro-sitio.comtiene su propio almacény no ve el de arribasetItem(«visitas», 5)guarda la cadena «5»:los objetos, con JSONUnos 5 millones de caracteres por origen. Persiste al cerrar el navegador;sessionStorage funciona igual pero se borra al cerrar la pestaña.

Cada sitio tiene su propio almacén, separado del resto, y en él solo hay pares de cadenas. Esquema: decodigo.com.

Dos funciones que conviene tener siempre

Leer puede fallar si alguien guardó algo que no es JSON, y guardar puede fallar si se llena el espacio o si el navegador bloquea el almacenamiento. Estas dos funciones lo resuelven de una vez:

<!doctype html>
<html lang="es">
<head><meta charset="utf-8"><title>localStorage con valores por omisión</title></head>
<body>
  <script type="module">
    function leer(clave, porOmision) {
      try {
        const texto = localStorage.getItem(clave);
        return texto === null ? porOmision : JSON.parse(texto);
      } catch {
        return porOmision;
      }
    }

    function guardar(clave, valor) {
      try {
        localStorage.setItem(clave, JSON.stringify(valor));
        return true;
      } catch (err) {
        console.log("no se pudo guardar:", err.name);
        return false;
      }
    }

    localStorage.clear();
    console.log(JSON.stringify(leer("prefs", { tema: "claro" })));
    guardar("prefs", { tema: "oscuro" });
    console.log(JSON.stringify(leer("prefs", { tema: "claro" })));
    localStorage.setItem("roto", "{esto no es json");
    console.log(JSON.stringify(leer("roto", [])));
    console.log(guardar("enorme", "x".repeat(6 * 1024 * 1024)));
  </script>
</body>
</html>
consola{"tema":"claro"}
{"tema":"oscuro"}
[]
no se pudo guardar: QuotaExceededError
false

El último intento guarda 6 MiB de texto, más de lo que cabe, y el navegador lanza un QuotaExceededError. Medimos el límite en Chromium: caben unos 5,242,000 caracteres por sitio, sumando todas las claves. Firefox y Safari tienen límites parecidos.

Enterarse de los cambios en otra pestaña

<!doctype html>
<html lang="es">
<head><meta charset="utf-8"><title>Evento storage</title></head>
<body>
  <script type="module">
    window.addEventListener("storage", (e) => {
      console.log(`otra pestaña cambió «${e.key}»: ${e.oldValue} -> ${e.newValue}`);
    });
  </script>
</body>
</html>

Con esta página abierta en dos pestañas, cambiamos el tema desde la segunda: primero «claro», luego «oscuro», y después lo borramos. En la consola de la primera aparece:

consolaotra pestaña cambió «tema»: null -> claro
otra pestaña cambió «tema»: claro -> oscuro
otra pestaña cambió «tema»: oscuro -> null

El evento storage salta en las demás pestañas del mismo sitio, nunca en la que hizo el cambio. Sirve, por ejemplo, para que todas cambien a la vez cuando el usuario activa el tema oscuro en una.

Cuál usar

Lo que necesitasLo que conviene
Preferencias pequeñas que durenlocalStorage
Algo que solo importa mientras la pestaña esté abiertasessionStorage, con la misma API
Que el servidor lo reciba en cada peticiónUna cookie
Muchos datos, archivos o búsquedasIndexedDB
Nunca guardes contraseñas ni tokens de sesión en localStorage. Cualquier script que se ejecute en tu página puede leerlo, incluido uno inyectado con un ataque XSS. Y el usuario puede borrarlo, o puede que el navegador no lo conserve en una ventana privada: trátalo como una comodidad, no como un sitio fiable para datos importantes.

Cada página de esta sección se abrió en Chromium 131, el motor de Chrome, controlado con Playwright, antes de publicarla; lo que aparece como consola es lo que imprimió de verdad. Documentación oficial: localStorage en MDN.