> ## Documentation Index
> Fetch the complete documentation index at: https://quickbutik.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Vanilla JS setup

> Use the framework-free @quickbutik/kit client with your own rendering, in the browser or on a server.

The root entry `@quickbutik/kit` (or the slimmer `/sdk`) has no runtime dependencies and works in any runtime with `fetch`. Use it when you render the DOM yourself, already have a state library, or run on a server.

<Tip>
  Before rendering everything by hand, have a look at the [web components](/kit/web-components/setup). They cover the catalog, the variant picker, the cart, checkout and the thank-you page with markup only, and they share the same cart store you'll use below. You can mix them freely with the client on one page.
</Tip>

## 1. Load the SDK

<Tabs>
  <Tab title="With a bundler (Vite)">
    ```bash theme={null}
    npm create vite@latest my-shop -- --template vanilla-ts
    cd my-shop
    npm install @quickbutik/kit
    ```

    ```bash .env.local theme={null}
    VITE_QUICKBUTIK_PUBLISHABLE_KEY=qb_pk_…
    ```

    ```ts theme={null}
    import { createShopkitClient } from "@quickbutik/kit"
    ```
  </Tab>

  <Tab title="Without a bundler">
    Load the script-tag bundle. Then `window.Quickbutik.client` is a client already built from the tag's `data-*` attributes, and `Quickbutik.createShopkitClient` builds your own. Everything in this guide works the same with `Quickbutik.client` in place of `kit`.

    ```html theme={null}
    <script defer
      src="https://cdn.jsdelivr.net/npm/@quickbutik/kit@1.8.0/dist/quickbutik-kit.global.js"
      data-publishable-key="qb_pk_…"
      data-currency="SEK"></script>
    <script type="module">
      const { data } = await Quickbutik.client.products.list({ limit: 12 })
    </script>
    ```

    Use `type="module"` on your own inline script. `defer` is ignored on inline scripts, so a plain one would run before the kit has defined `window.Quickbutik`.
  </Tab>
</Tabs>

## 2. Create one client

```ts src/kit.ts theme={null}
import { createShopkitClient, formatMoney } from "@quickbutik/kit"

export const kit = createShopkitClient({
  publishableKey: import.meta.env.VITE_QUICKBUTIK_PUBLISHABLE_KEY,
})

export const CURRENCY = "SEK"
export const LANGUAGE = "sv"

export const money = (minor: number, currency = CURRENCY) =>
  formatMoney(minor, currency, { locale: "sv-SE" })
```

Create **one client per page load in the browser** and **one per request on the server**, because the cookie jar belongs to a request. Clients are cheap. In a browser the cart id is remembered in `document.cookie` (`qb_cart_id` for 30 days, `qb_checkout_session` for 24 hours, and `qb_currency` once the shopper picks a currency), falling back to `localStorage` and then memory. If a cart doesn't survive a reload, check `kit.runtime.persistent`. See [Storage & SSR](/kit/concepts/storage-and-ssr).

<Note>
  **Money is always an integer in minor units** (öre, cents): `24900` is 249,00 kr. Every response states the currency its amounts are in (`product.currency`, `cart.currency`), so format with that and keep `CURRENCY` only as a fallback. A shopper can browse in another currency the shop offers: see [Currencies](/kit/concepts/currencies).
</Note>

## 3. Verify the key

Before building pages, make sure the key and environment work:

```ts theme={null}
const [shop, products, categories] = await Promise.all([
  kit.shop.get(),                       // needs checkout:read
  kit.products.list({ limit: 1 }),      // needs products:read
  kit.categories.list({ root: true }),
])
console.log(shop.name, products.data.length, categories.data.length)
```

A `ShopkitScopeError`, a `403` carrying `required_scopes`, or a `401` on every call points at the key or at an `apiUrl` override. See [Troubleshooting](/kit/troubleshooting).

## 4. A shared cart store

A header badge, a cart drawer and a cart page should read **one** observable cart. The kit's `CartStore` is framework-free, and the easiest way to get one in browser code is from `configure()` in the elements entry. It needs no React and no registered elements:

```ts src/cart-state.ts theme={null}
// Browser only: the /elements entry throws when imported in Node.
import { configure } from "@quickbutik/kit/elements"

export const shop = configure({
  publishableKey: import.meta.env.VITE_QUICKBUTIK_PUBLISHABLE_KEY,
  currency: "SEK",
  locale: "sv-SE",
})

export const kit = shop.client     // use THIS client everywhere so the store and your calls share one cart
export const cart = shop.cartStore
```

With the script tag, the same store is `Quickbutik.cart` and the client is `Quickbutik.client`.

<Note>
  `configure()` also sets up the page's cookie consent: it appends the kit's default banner to `<body>` and loads the shop's analytics once the shopper agrees, and the cart store reports `add_to_cart` / `remove_from_cart` into it. Pass `consent: { lang: "sv", privacyPolicyUrl: "/integritetspolicy" }` to shape the banner, or `consent: false` to turn it all off. See [Consent and analytics](/kit/concepts/consent-and-analytics).
</Note>

The shopper's currency switches through the same client: `kit.setCurrency("EUR")` reprices every later read, and the store re-reads the same cart in the new currency. See [Currencies](/kit/concepts/currencies).

| Member | Behaviour |
| - | - |
| `subscribe(fn)` / `getSnapshot()` | Snapshot is `{ cart, status, pending, error, itemCount }` |
| `load()` | Reads the remembered cart and **never creates one**, so crawlers and first visits cause no write |
| `refresh()` | Re-reads the cart |
| `add(item)` | Creates the cart on first use and recovers once from a stale cookie |
| `updateItem(itemId, quantity)` | Absolute quantity, and `0` removes the line. `itemId` is the **line id** (`item.id`), never the product id |
| `removeItem(itemId)` | Removes one line |
| `clear()` | Deletes the remembered cart and forgets it |
| `hydrate(cart)` | Adopts a cart you fetched elsewhere |

Mutations capture errors into `error` and resolve `null` instead of throwing. `pending` counts the mutations in flight.

```ts theme={null}
cart.subscribe(() => {
  const s = cart.getSnapshot()
  badge.textContent = s.pending ? "…" : String(s.itemCount)
})
void cart.load()
```

<Warning>
  Call `configure()` **once** at startup. A second call replaces the client and store for elements mounted afterwards, while anything already on the page keeps the first ones.
</Warning>

## 5. Handle errors

```ts theme={null}
import { ShopkitApiError, ShopkitNetworkError, ShopkitScopeError } from "@quickbutik/kit"

try {
  await kit.cart.add({ productId })
} catch (e) {
  if (e instanceof ShopkitScopeError) console.error(e.required, e.declared)     // thrown before the request
  else if (e instanceof ShopkitApiError) console.error(e.status, e.details, e.retryable)
  else if (e instanceof ShopkitNetworkError) console.error("offline or timeout", e.cause)
}
```

A missing resource is **not** an error. `products.get()`, `getBySlug()`, `categories.get()`, `cart.get()`, `cart.current()` and `checkout.getSession()` resolve to `null`. See [Errors](/kit/reference/errors).

## Next

<CardGroup cols={2}>
  <Card title="Build the storefront" icon="store" href="/kit/vanilla/storefront">
    Catalog, product page, cart, checkout and thank-you page.
  </Card>

  <Card title="Server-side" icon="server" href="/kit/vanilla/server-side">
    Node, Hono, Express, Astro and Workers.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.