# Svelte

> A Svelte 5 component with runes that collects on submit and sends the result with the form.

Source: https://whiskerjs.com/guides/svelte/ (last updated 2026-10-26)

> **In plain words:**
>
> A Svelte sign-in form that, on submit, gets a one-time code from your server, lets WhiskerJS read the browser and posts the result with the email and password. WhiskerJS is created the first time someone submits and only runs in the browser.

## Install: build from source

WhiskerJS is not on npm yet; the npm package is coming. Build it and install the tarball:

```sh
cd whiskerjs
pnpm install && pnpm build && pnpm pack    # writes whiskerjs-0.1.0.tgz
cd ../my-svelte-app
npm install ../whiskerjs/whiskerjs-0.1.0.tgz
```

## The component

`src/LoginForm.svelte`:

```svelte
<script lang="ts">
  import { load, type Agent } from 'whiskerjs';

  let email = $state('');
  let password = $state('');
  let status = $state('');
  let busy = $state(false);

  // One agent per page, created on first use. load() is cheap and keeps no state.
  let agent: Promise<Agent> | undefined;

  async function onsubmit(event: SubmitEvent) {
    event.preventDefault();
    busy = true;
    try {
      // A fresh nonce from your server, then collect. collect() never throws.
      const binding = await (await fetch('/api/whisker-nonce')).json();
      const device = await (await (agent ??= load())).collect({ timeoutMs: 3000, binding });
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ email, password, device }),
      });
      status = response.ok ? 'Signed in' : `Refused (${response.status})`;
    } catch {
      status = 'Network error, please try again';
    } finally {
      busy = false;
    }
  }
</script>

<form {onsubmit}>
  <label>Email <input bind:value={email} name="email" type="email" required /></label>
  <label>Password <input bind:value={password} name="password" type="password" required /></label>
  <button type="submit" disabled={busy}>Sign in</button>
  <output id="status">{status}</output>
</form>
```

Mounted in `src/main.ts`:

```ts
import { mount } from 'svelte';
import LoginForm from './LoginForm.svelte';

mount(LoginForm, { target: document.getElementById('app')! });
```

## How it works

- **`onsubmit`** is the Svelte 5 event attribute; `event.preventDefault()` stops the browser’s own form post so the handler can add the device result.
- **Runes.** `$state` holds the form fields, the status line and the `busy` flag. The agent is a plain variable, created by the first submit (`agent ??= load()`).
- **The binding.** The nonce endpoint returns `{ nonce, timestamp }`, passed to `collect({ binding })`. The result’s `binding` carries both and a digest over the whole result, which your server checks: [Node](https://whiskerjs.com/guides/verify-node/), [Go](https://whiskerjs.com/guides/verify-go/), [PHP](https://whiskerjs.com/guides/verify-php/), [Python](https://whiskerjs.com/guides/verify-python/).
- **Errors.** `collect()` never throws or rejects. The `try` block covers the `fetch` calls.

## Things to know

- **SvelteKit and server rendering.** Component script code runs on the server during rendering; event handlers do not. Keep WhiskerJS calls in handlers, or in `onMount` if you need to collect without a user action.
- **Secure context.** https, or `localhost` with the Vite dev server.
- **Send the result unchanged** if the server verifies the binding.

## Tested

Tested with WhiskerJS 0.1.0 (signalsVersion 5, idVersion 2), Svelte 5.57.2, Vite 8.3.5 with @sveltejs/vite-plugin-svelte 7.3.1, and svelte-check 4.7.7 on 2026-10-26: built with `svelte-check && vite build`, served by the [Node server](https://whiskerjs.com/guides/verify-node/#a-complete-server) and submitted in headless Chromium 140 (Playwright 1.55.0). The server accepted the binding and refused the same result sent a second time.

## Questions and answers

### Does this work in SvelteKit?

The component does: load() and collect() are only called from the submit handler, which runs in the browser. SvelteKit server endpoints can then host the nonce and verification routes. This guide was tested with Svelte and Vite only.

### Why is the agent not $state?

Nothing in the markup depends on it. A plain variable is enough to keep the one agent for the life of the component.

### Can I use a form action instead of fetch?

You can, as long as the device result reaches the server unchanged. A hidden input holding JSON.stringify(result) works; this guide shows fetch because that was tested.

## Related pages

- [Verify in Node](https://whiskerjs.com/guides/verify-node/): The server this component was tested against.
- [Vue](https://whiskerjs.com/guides/vue/): The same form in Vue.
- [All integration guides](https://whiskerjs.com/guides/): The page-to-server contract.
- [Anti-replay binding](https://whiskerjs.com/docs/integration/#anti-replay-binding): Nonce, timestamp and digest.
