Cookies
Cookies are the oldest browser storage mechanism: small key/value pairs that the browser automatically sends back to the server on every matching HTTP request. That single property — automatic transport — is what makes them useful for authentication and what makes them very different from localStorage. The JavaScript API for cookies is also famously awkward, so most teams reach for a tiny helper or the newer CookieStore API.
document.cookie — the awkward classic API
// Read ALL cookies as one big semicolon-separated string: console.log(document.cookie); // "theme=dark; cart=42; consent=accepted" // Set or update ONE cookie by assigning a string with attributes: document.cookie = "theme=dark; Path=/; Max-Age=2592000; SameSite=Lax"; // Delete a cookie — set it with a past expiry / Max-Age=0: document.cookie = "theme=; Path=/; Max-Age=0";
A tiny parser/setter
cookies.js
export function getCookie(name) {
const target = name + "=";
for (const part of document.cookie.split("; ")) {
if (part.startsWith(target)) {
return decodeURIComponent(part.slice(target.length));
}
}
return null;
}
export function setCookie(name, value, opts = {}) {
const segments = [`${name}=${encodeURIComponent(value)}`];
if (opts.maxAge != null) segments.push(`Max-Age=${opts.maxAge}`);
if (opts.path) segments.push(`Path=${opts.path}`);
if (opts.domain) segments.push(`Domain=${opts.domain}`);
if (opts.secure) segments.push("Secure");
if (opts.sameSite) segments.push(`SameSite=${opts.sameSite}`);
document.cookie = segments.join("; ");
}
export function deleteCookie(name, path = "/") {
setCookie(name, "", { maxAge: 0, path });
}The attributes that actually matter
Path — limits the cookie to URLs starting with this path. Default is the current path; almost always set
Path=/.Domain — share the cookie across subdomains.
Domain=example.commakes it available onwww.example.com,api.example.com, etc. Omit to keep it on the exact host.Expires / Max-Age — when the cookie expires. Omit both to make a session cookie that dies with the browser session.
Secure — only sent over HTTPS. Required by modern browsers for
SameSite=None.HttpOnly — invisible to JavaScript. Cannot be set or read from
document.cookie— only the server (Set-Cookieheader) can mark a cookie HttpOnly. Use it for session tokens.SameSite — controls cross-site sending:
Strict(same-site only),Lax(same-site + top-level GET navigation; the modern default),None(any cross-site context — requiresSecure).Partitioned — newer flag for opt-in cross-site cookies under CHIPS. Niche, but increasingly relevant.
HttpOnly: the security big one
If your auth token lives in a cookie marked HttpOnly, no JavaScript can read it — not your own code, not a malicious dependency, not an XSS injection. The browser still sends it on every request to your API. That is the strongest reason to use cookies for sessions instead of localStorage.
Server-side response header
Set-Cookie: session=abc123; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=86400
SameSite: defending against CSRF
// Defaults to Lax in modern browsers if you omit SameSite. document.cookie = "consent=yes; Path=/; SameSite=Lax"; // Cross-site cookies (e.g. third-party widgets) need both: document.cookie = "tracker=abc; Path=/; SameSite=None; Secure";
The modern CookieStore API
if ("cookieStore" in window) {
await cookieStore.set({
name: "theme",
value: "dark",
path: "/",
sameSite: "lax",
expires: Date.now() + 30 * 24 * 60 * 60 * 1000,
});
const cookie = await cookieStore.get("theme");
console.log(cookie?.value);
await cookieStore.delete("theme");
cookieStore.addEventListener("change", (e) => {
console.log("changed:", e.changed, "deleted:", e.deleted);
});
}cookieStore is async, structured and event-driven — much nicer. Not supported on Safari/Firefox as of this writing; feature-detect.
Cookies vs localStorage — pick the right one
Need the server to know the value on every request? → Cookie.
Need it only on the client (theme, draft text)? → localStorage.
Storing an auth token? → HttpOnly cookie set by the server. Never
localStorage.More than ~4 KB of data? → localStorage or IndexedDB. Cookies are capped at ~4 KB each and sent on every request.
Need expiry control? → Either works; cookies have native
Max-Age.
Common gotchas
Each cookie is ~4 KB max. The total per domain is also limited (around 50 cookies / 4 KB each).
Every cookie is sent on every matching request. Big cookies = big request overhead.
Values must be URI-encoded if they contain
;,,, whitespace or non-ASCII.document.cookiedoes not see HttpOnly cookies. They are invisible to JavaScript on purpose.Safari ITP and Firefox ETP can evict third-party cookies aggressively; assume they will be missing in cross-site contexts.
document.cookie = "theme=dark; Path=/; Max-Age=2592000; SameSite=Lax" // reading back: document.cookie === "theme=dark"