How to Use the Cookie Store API

How to Use the Cookie Store API

The Cookie Store API is the cleaner way to work with script-visible browser cookies. It gives you promise-based reads, object-shaped writes, deletes, and change events instead of forcing you to parse and assign document.cookie strings.

Use it for preferences, consent flags, experiment buckets, and service-worker-aware cookie changes. Keep real session cookies server-owned and HttpOnly.

The old cookie API is easy to dislike. Reading document.cookie gives you one semicolon-delimited string. Writing to it means assigning another cookie-shaped string. If the browser rejects the write, the usual check is to read the string again and hope you matched the right path, domain, and attributes.

The Cookie Store API fixes the part of that problem that app JavaScript can fix. It exposes cookieStore.get(), cookieStore.getAll(), cookieStore.set(), cookieStore.delete(), and cookie change events.

The API requires a secure context, works in windows and service workers, and MDN marks it as Baseline 2025, newly available across latest browser versions since June 2025.

I would reach for Cookie Store API when the cookie is intentionally visible to JavaScript:

  • Theme, language, or display preferences.
  • Cookie consent or analytics opt-out flags.
  • Lightweight experiment assignments.
  • Service worker logic that needs to react when a script-visible cookie changes.

I would not move authentication token handling into this API. If a cookie protects a signed-in session, it should usually be set by the server with HttpOnly, Secure, and an appropriate SameSite policy.

JavaScript should not be able to read that value. MDN's document.cookie reference notes that HttpOnly cookies cannot be set or modified through Document.cookie or the Cookie Store API.

Start with feature detection

The API is newer than document.cookie, so put a feature check around it. In a real project, keep cookie logic in a small browser-only helper instead of scattering it through components and route handlers. That is the same reason I like keeping clear boundaries in a Vite folder structure: browser-only code should be easy to find, test, and replace.

export const hasCookieStore =
  typeof window !== "undefined" && "cookieStore" in window;

If you use Vite, avoid importing this helper into server-only code. I covered the config side of those boundaries in my guide on how to configure your Vite config file.

With document.cookie, a simple read usually turns into string splitting:

function readCookieFromDocument(name) {
  return document.cookie
    .split("; ")
    .find((row) => row.startsWith(`${name}=`))
    ?.split("=")[1];
}

With Cookie Store API, ask for the cookie by name and handle the missing-cookie case directly:

async function getThemeCookie() {
  if (!("cookieStore" in window)) {
    return readCookieFromDocument("theme");
  }

  const cookie = await cookieStore.get("theme");
  return cookie?.value ?? null;
}

That object shape is the improvement. You ask for one cookie and get one cookie-like object, not a single string containing every script-visible cookie currently in scope.

For simple cookies, cookieStore.set(name, value) works:

await cookieStore.set("theme", "dark");

For production code, use the options object so the attributes are visible at the write site:

async function saveThemePreference(theme) {
  if (!["light", "dark", "system"].includes(theme)) {
    throw new Error("Unknown theme preference");
  }

  await cookieStore.set({
    name: "theme",
    value: theme,
    path: "/",
    sameSite: "lax",
    maxAge: 60 * 60 * 24 * 30,
  });
}

maxAge is in seconds. expires is a timestamp in milliseconds. Do not set both on the same cookieStore.set() call; MDN documents that as a TypeError.

Deleting has the same matching problem cookies always had: the browser needs the cookie name and the relevant scope attributes.

await cookieStore.delete({
  name: "theme",
  path: "/",
});

If you created a cookie with partitioned: true, include partitioned: true when deleting that cookie.

Also, do not treat a resolved delete promise as proof that a cookie used to exist. MDN documents that cookieStore.delete() resolves when deletion completes or when no matching cookie is found.

In a page, you can listen for changes:

if ("cookieStore" in window) {
  cookieStore.addEventListener("change", (event) => {
    for (const cookie of event.changed) {
      if (cookie.name === "theme") {
        applyTheme(cookie.value);
      }
    }

    for (const cookie of event.deleted) {
      if (cookie.name === "theme") {
        applyTheme("system");
      }
    }
  });
}

Two details matter.

First, event.changed and event.deleted are separate arrays. Do not assume a single old-value/new-value object.

Second, MDN notes that replacing a cookie with another cookie that has the same name, domain, and path does not trigger a change event. Use change events for coordination, not as a complete audit log.

Use service worker subscriptions carefully

The service worker side is more selective. A service worker can be woken up by cookie changes, so the API makes you subscribe to the cookies you care about.

self.addEventListener("activate", (event) => {
  event.waitUntil(
    self.registration.cookies.subscribe([{ name: "theme" }]),
  );
});

self.addEventListener("cookiechange", (event) => {
  for (const cookie of event.changed) {
    if (cookie.name === "theme") {
      // Refresh cached shell, update an offline response, or record state.
    }
  }
});

registration.cookies.subscribe() accepts an array of subscription objects. You can subscribe by name, by url, or with an empty object for every cookie in scope. Be careful with that last form. Waking a service worker for every cookie change is rarely what you want.

Keep the fallback narrow

You probably do not need to recreate the whole API for old browsers. If your app only reads one preference cookie on startup, keep the fallback read-only and small:

function getDocumentCookie(name) {
  const encodedName = `${encodeURIComponent(name)}=`;

  return document.cookie
    .split("; ")
    .find((entry) => entry.startsWith(encodedName))
    ?.slice(encodedName.length) ?? null;
}

async function getCookieValue(name) {
  if ("cookieStore" in window) {
    return (await cookieStore.get(name))?.value ?? null;
  }

  return getDocumentCookie(name);
}

Once you start recreating set, delete, change events, sameSite, partitioned, path matching, and service worker behavior, you are not writing a fallback anymore. You are writing a cookie library.

What to test

Test this in a real browser context. Cookies care about protocol, domain, path, and browser policy, so a unit test alone will not catch the painful cases. My post on changing browser location and timezone for testing covers a different feature, but the same rule applies: browser state needs browser testing.

Before shipping:

  • Run over HTTPS or localhost because the API requires a secure context.
  • Confirm your fallback path still works in browsers without cookieStore.
  • Verify that path matches on deletes.
  • Confirm maxAge and expires are not used together.
  • Keep session cookies server-owned and HttpOnly.
  • Test service worker subscriptions only if you actually use the service worker path.

The practical rule

Use Cookie Store API to make script-visible cookie work less fragile. It gives you promises, object options, and change events. That is a real improvement over parsing and assigning cookie strings.

But do not make cookies carry data that belongs in localStorage, IndexedDB, OPFS, or your server session. Cookies still ride along with requests. They still need careful scope. And if a cookie is security-sensitive, keep it out of JavaScript entirely.

Sources

Stay Sharp. Weekly Insights.
New posts, framework updates and weekly software conversations.

No spam. Unsubscribe anytime.
Walt is a software engineer, startup founder and previous mentor for a coding bootcamp. He has been creating software for the past 20+ years.
No comments posted yet
// Add a comment
// Color Theme

Custom accent
Pick any color
for the accent