---
title: "Actions & scripts — Svelte"
description: "Decide what a PDF may do by itself: links, buttons, form calculations, JavaScript, opening websites and printing."
framework: "Svelte"
source: "https://www.embedpdf.com/docs/headless/svelte/documents/actions"
---

# Actions & scripts

A PDF can do things by itself: a button that resets a form, a field that adds up others, a link that
opens a website, a script that runs when the document opens. The actions plugin runs them, with
rules you choose. Register it, and install the browser's defaults for opening websites and printing
once, near the top of your viewer:

**`actions-setup.svelte`**

```svelte
<script lang="ts">
  // Inside <Viewer>, with actionsPlugin() in its plugins
  import { useActionsUiAdapter } from '@embedpdf/svelte/actions';

  useActionsUiAdapter();
</script>

<!-- your viewer -->
```

Without it, the plugin still runs what stays inside the viewer, such as going to a page or resetting
a form, but it won't open a website or a print dialog.

## What runs

| Action                             | By default                                                          |
| :--------------------------------- | :------------------------------------------------------------------ |
| Go to a page or a destination      | Runs                                                                |
| A viewer action, such as next page | Runs                                                                |
| Show or hide a form field          | Runs                                                                |
| Reset a form                       | Runs                                                                |
| Open a website                     | Goes to your UI adapter when the user clicked; reported otherwise   |
| Print                              | Goes to your UI adapter when the user clicked; never on its own     |
| Submit a form                      | Goes to your submit handler when the user clicked; never on its own |
| JavaScript                         | Off, until you turn it on                                           |

Actions that start a program, open another file on the user's computer, play media or import data
never run.

## Turn on JavaScript

Many forms calculate totals, check values and format dates with JavaScript. Turn it on:

```ts
actionsPlugin({ javascript: { enabled: true } });
```

Scripts run in a sealed sandbox: they can read and change the document's form fields and move
between pages, but they can't reach your app, the page's HTML or the network. Each script has a
time budget, so a script that loops forever is stopped instead of freezing the viewer.

A script can ask who the user is (`identity.name` in Acrobat's JavaScript). It gets the user the
document was opened for, and you can add the fields scripts often ask for:

```ts
actionsPlugin({
  javascript: { enabled: true, identity: { corporation: 'Acme', email: 'dana@acme.com' } },
});
```

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { actionsPlugin } from '@embedpdf/svelte/actions';
  import { localEngine } from '@embedpdf/engine';
  import ScriptConsole from './ScriptConsole.svelte';

  import '../javascript.css';

  const engine = localEngine();
  // JavaScript on, and two fields scripts often ask for about the user.
  const plugins = [
    actionsPlugin({
      javascript: { enabled: true, identity: { name: 'Dana Smith', corporation: 'Acme' } },
    }),
  ];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <ScriptConsole />
  </DocumentGate>
</Viewer>
```

**`javascript.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #0876fd;
  --accent-strong: #0565da;
  --on-accent: #ffffff;
  --focus: #7db6ff;
  --code: #f0f3f9;
  --danger: #c62a2a;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The example, in a card */
.panel {
  box-sizing: border-box;
  padding: 16px;
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.button.primary {
  color: var(--on-accent);
  background: var(--accent);
  border-color: var(--accent);
}

.button.primary:hover {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
}

/* The script */
.name {
  display: block;
  margin-bottom: 6px;
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
}

.code {
  box-sizing: border-box;
  width: 100%;
  resize: vertical;
  font-family: ui-monospace, monospace;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--code);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 10px 12px;
}

.code:focus-visible {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px rgb(125 182 255 / 0.35);
}

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 10px 0 12px;
}

/* What the script said, newest first */
.output {
  min-height: 40px;
  margin: 0;
  padding: 8px 12px;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
}

.output li {
  padding: 3px 0;
  font-size: 13px;
  overflow-wrap: anywhere;
}

.output .alert {
  color: var(--ink);
}

.output .error {
  font-family: ui-monospace, monospace;
  font-size: 12px;
  color: var(--danger);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`ScriptConsole.svelte`**

```svelte
<!-- Edit the script and run it. Its alerts and its errors show below it. -->
<script lang="ts">
  import { onMount } from 'svelte';
  import {
    useActions,
    useActionsEvent,
    useActionsUiAdapter,
    type ActionContext,
    type PdfActionTree,
  } from '@embedpdf/svelte/actions';

  // A JavaScript action, as a PDF carries it behind a button or a link.
  const script = (code: string): PdfActionTree => ({
    root: { type: 'javascript', subtype: 'JavaScript', script: code, next: [] },
    incomplete: false,
    warningFlags: 0,
    warnings: [],
  });
  const click: ActionContext = {
    origin: 'user',
    source: { kind: 'api' },
    event: { scope: 'activate' },
  };

  const GREETING = "app.alert('Hello, ' + identity.name + ' from ' + identity.corporation + '!');";

  const actions = useActions();
  let code = $state(GREETING);
  let output = $state<Array<{ kind: 'alert' | 'error'; text: string }>>([]);
  const show = (kind: 'alert' | 'error', text: string) => {
    output = [{ kind, text }, ...output].slice(0, 5);
  };

  // A script's alert goes to your UI adapter: here, a line below the script.
  useActionsUiAdapter({ alert: (message) => show('alert', message) });
  useActionsEvent(
    (capability) => capability.onScriptFailed,
    ({ error }) => show('error', error.message),
  );

  // The greeting runs once on load.
  onMount(() => {
    void actions.execute(script(GREETING), click);
  });
</script>

<section class="panel">
  <label class="name" for="script">
    Script {actions.isScriptingEnabled() ? '' : '(JavaScript is off)'}
  </label>
  <textarea id="script" class="code" spellcheck={false} rows={3} bind:value={code}></textarea>
  <div class="actions">
    <button type="button" class="button primary" onclick={() => actions.execute(script(code), click)}>
      Run
    </button>
    <button type="button" class="button" onclick={() => (code = 'app.alrt("typo");')}>
      A script with a mistake
    </button>
  </div>
  <ol class="output" aria-live="polite">
    {#each output as line, index (output.length - index)}
      <li class={line.kind}>{line.text}</li>
    {/each}
  </ol>
</section>
```

## How websites open, printing and alerts

`useActionsUiAdapter()` installs the browser's defaults: a website opens in a new tab after a safety
check, printing opens the print dialog, and a script's alert shows the browser's alert. Replace any
of them with your own UI:

**`actions-ui.svelte`**

```svelte
<script lang="ts">
  import { useActionsUiAdapter } from '@embedpdf/svelte/actions';
  import { toast } from './toast';

  useActionsUiAdapter({
    openUri: (uri) => {
      if (confirm(`Open ${uri}?`)) window.open(uri, '_blank', 'noopener');
    },
    alert: (message) => toast(message),
  });
</script>

<!-- your viewer -->
```

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { actionsPlugin } from '@embedpdf/svelte/actions';
  import { localEngine } from '@embedpdf/engine';
  import AskBeforeOpening from './AskBeforeOpening.svelte';

  import '../website-links.css';

  const engine = localEngine();
  const plugins = [actionsPlugin()];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <AskBeforeOpening />
  </DocumentGate>
</Viewer>
```

**`website-links.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #0876fd;
  --accent-strong: #0565da;
  --on-accent: #ffffff;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The example, in a card */
.panel {
  box-sizing: border-box;
  padding: 16px;
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.lead {
  margin: 0 0 12px;
  font-size: 13.5px;
  color: var(--ink);
}

.lead code {
  font-size: 12.5px;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.button.primary {
  color: var(--on-accent);
  background: var(--accent);
  border-color: var(--accent);
}

.button.primary:hover {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
}

/* Your question, before a website opens */
.prompt {
  margin-top: 14px;
  padding: 14px 16px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: 12px;
  box-shadow: 0 10px 30px -18px rgb(7 32 76 / 0.45);
}

.prompt-text {
  margin: 0 0 12px;
  font-size: 13.5px;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.prompt-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 8px;
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`AskBeforeOpening.svelte`**

```svelte
<!-- Your UI decides how a website opens: here, it asks first. -->
<script lang="ts">
  import { onMount } from 'svelte';
  import {
    useActions,
    useActionsUiAdapter,
    type ActionContext,
    type PdfActionTree,
  } from '@embedpdf/svelte/actions';

  // A link to a website, as a PDF carries it: what getActionTree() reads from a link.
  const websiteLink: PdfActionTree = {
    root: { type: 'uri', subtype: 'URI', uri: 'https://www.embedpdf.com', isMap: false, next: [] },
    incomplete: false,
    warningFlags: 0,
    warnings: [],
  };
  const click: ActionContext = {
    origin: 'user',
    source: { kind: 'api' },
    event: { scope: 'activate' },
  };

  const actions = useActions();
  let asking = $state<string | null>(null);

  useActionsUiAdapter({ openUri: (uri) => (asking = uri) });

  // The link is clicked once on load, so the question is there to see.
  onMount(() => {
    void actions.execute(websiteLink, click);
  });

  function openSite() {
    if (asking) window.open(asking, '_blank', 'noopener');
    asking = null;
  }
</script>

<section class="panel">
  <p class="lead">
    The document links to <code>https://www.embedpdf.com</code>.
  </p>
  <button type="button" class="button" onclick={() => actions.execute(websiteLink, click)}>
    Click the link
  </button>
  {#if asking}
    <div class="prompt" role="alertdialog" aria-label="Open a website">
      <p class="prompt-text">
        This document wants to open <strong>{asking}</strong>.
      </p>
      <div class="prompt-actions">
        <button type="button" class="button" onclick={() => (asking = null)}>Stay here</button>
        <button type="button" class="button primary" onclick={openSite}>
          Open in a new tab
        </button>
      </div>
    </div>
  {/if}
</section>
```

| Handler    | Called when                                   |
| :--------- | :-------------------------------------------- |
| `openUri`  | An action opens a website or an email address |
| `print`    | An action or a script asks to print           |
| `alert`    | A script shows a message                      |
| `gotoPage` | A script moves to a page                      |

## Change the rules

Each kind of action has a rule for each way it can start: a click by the user (`user`), the pointer
moving over something (`hover`), or the document opening or closing (`lifecycle`):

```ts
actionsPlugin({
  policy: {
    uri: { user: 'adapter', hover: 'block', lifecycle: 'block' }, // websites only on click
    javascript: { lifecycle: 'block' }, // no scripts when the document opens
  },
});
```

| Rule        | Means                                                                            |
| :---------- | :------------------------------------------------------------------------------- |
| `'allow'`   | Run it                                                                           |
| `'adapter'` | Hand it to your UI adapter (a form submit to your submit handler), which decides |
| `'report'`  | Don't run it; report it in `onDiagnosticReported`                                |
| `'block'`   | Don't run it                                                                     |

The rules can change while the app runs, for example when the user trusts a document:

```ts
actions.updateSettings({ policy: { javascript: { lifecycle: 'allow' } } });
```

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { actionsPlugin } from '@embedpdf/svelte/actions';
  import { localEngine } from '@embedpdf/engine';
  import WebsiteRules from './WebsiteRules.svelte';

  import '../rules.css';

  const engine = localEngine();
  // Websites open only on a click, as the page's example registers it.
  const plugins = [
    actionsPlugin({
      policy: { uri: { user: 'adapter', hover: 'block', lifecycle: 'block' } },
    }),
  ];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <WebsiteRules />
  </DocumentGate>
</Viewer>
```

**`rules.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #0876fd;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The example, in a card */
.panel {
  box-sizing: border-box;
  padding: 16px;
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* One row per way to start: its rule and a button, wrapping on a phone */
.starts {
  margin: 0;
  padding: 0;
  list-style: none;
}

.start {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
}

.start:first-child {
  padding-top: 0;
}

.start-label {
  flex: 1 1 180px;
  font-size: 13.5px;
  color: var(--ink);
}

.start-label code {
  margin-left: 4px;
  font-size: 12px;
  color: var(--muted);
}

.field {
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 10px;
}

.field:focus-visible {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px rgb(125 182 255 / 0.35);
}

/* What happened, newest first */
.footer {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 10px;
  margin-top: 12px;
}

.log {
  flex: 1 1 240px;
  min-height: 64px;
  margin: 0;
  padding: 8px 12px;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
}

.log li {
  padding: 3px 0;
  font-size: 12.5px;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.log li + li {
  color: var(--muted);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`WebsiteRules.svelte`**

```svelte
<!-- The rules for websites, changed while the app runs, and what happens when the link runs. -->
<script lang="ts">
  import { onMount } from 'svelte';
  import {
    useActions,
    useActionsEvent,
    useActionsSettings,
    useActionsUiAdapter,
    type ActionContext,
    type ActionOrigin,
    type ActionPolicyDecision,
    type PdfActionTree,
  } from '@embedpdf/svelte/actions';

  // A link to a website, as a PDF carries it: what getActionTree() reads from a link.
  const websiteLink: PdfActionTree = {
    root: { type: 'uri', subtype: 'URI', uri: 'https://www.embedpdf.com', isMap: false, next: [] },
    incomplete: false,
    warningFlags: 0,
    warnings: [],
  };

  // The three ways an action can start, each as the context it runs with.
  const STARTS: ReadonlyArray<{ label: string; context: ActionContext }> = [
    {
      label: 'A click',
      context: { origin: 'user', source: { kind: 'api' }, event: { scope: 'activate' } },
    },
    {
      label: 'The pointer over it',
      context: {
        origin: 'hover',
        source: { kind: 'api' },
        event: { scope: 'annotation', name: 'cursorEnter' },
      },
    },
    {
      label: 'The document opening',
      context: {
        origin: 'lifecycle',
        source: { kind: 'api' },
        event: { scope: 'document', name: 'open' },
      },
    },
  ];
  const RULES: readonly ActionPolicyDecision[] = ['allow', 'adapter', 'report', 'block'];

  const actions = useActions();
  const rules = useActionsSettings((settings) => settings.policy.uri);
  let log = $state<string[]>([]);
  const note = (line: string) => {
    log = [line, ...log].slice(0, 6);
  };

  // Instead of opening a tab, the adapter notes the website it was given.
  useActionsUiAdapter({ openUri: (uri) => note(`Your adapter got ${uri}`) });
  useActionsEvent(
    (capability) => capability.onDiagnosticReported,
    ({ code, action }) => note(`The ${action} action was not run: ${code}`),
  );

  const run = (context: ActionContext) => actions.execute(websiteLink, context);
  const setRule = (origin: ActionOrigin, rule: ActionPolicyDecision) =>
    actions.updateSettings({ policy: { uri: { [origin]: rule } } });

  // One click on load, so the log shows what a click does.
  onMount(() => {
    void run(STARTS[0].context);
  });
</script>

<section class="panel">
  <ul class="starts">
    {#each STARTS as { label, context } (context.origin)}
      <li class="start">
        <span class="start-label">
          {label} <code>{context.origin}</code>
        </span>
        <select
          class="field"
          aria-label="The rule for {context.origin}"
          value={rules.current[context.origin]}
          onchange={(event) =>
            setRule(context.origin, event.currentTarget.value as ActionPolicyDecision)}
        >
          {#each RULES as rule (rule)}
            <option value={rule}>{rule}</option>
          {/each}
        </select>
        <button type="button" class="button" onclick={() => run(context)}>Run the link</button>
      </li>
    {/each}
  </ul>
  <div class="footer">
    <ol class="log" aria-live="polite">
      {#each log as line, index (log.length - index)}
        <li>{line}</li>
      {/each}
    </ol>
    <button type="button" class="button" onclick={() => actions.resetSettings()}>
      Back to the registered rules
    </button>
  </div>
</section>
```

## Run an action from code

```ts
await actions.executeNamed('NextPage');
```

`executeNamed` runs a viewer action by its name: `'NextPage'`, `'PrevPage'`, `'FirstPage'`,
`'LastPage'` or `'Print'`. It follows the same rules as a click.

**`App.svelte`**

```svelte
<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { actionsPlugin } from '@embedpdf/svelte/actions';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { localEngine } from '@embedpdf/engine';
  import NamedActions from './NamedActions.svelte';

  import '../run-from-code.css';

  const engine = localEngine();
  const plugins = [stagePlugin(), renderPlugin(), actionsPlugin()];

  const ebook = async (): Promise<OpenInput> => {
    const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
    return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
  };
</script>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <NamedActions />
    <Stage class="stage">
      <RenderLayer />
    </Stage>
  </DocumentGate>
</Viewer>
```

**`run-from-code.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #0876fd;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The toolbar: it wraps on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.ran {
  font-weight: 500;
  color: var(--muted);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`NamedActions.svelte`**

```svelte
<!-- The viewer actions a PDF's buttons name, run from your own buttons. -->
<script lang="ts">
  import { onMount } from 'svelte';
  import { useActions, useActionsUiAdapter, type PdfNamedAction } from '@embedpdf/svelte/actions';
  import { useStageState } from '@embedpdf/svelte/stage';

  const VERBS: ReadonlyArray<[PdfNamedAction, string]> = [
    ['FirstPage', '⇤ First'],
    ['PrevPage', '‹ Previous'],
    ['NextPage', 'Next ›'],
    ['LastPage', 'Last ⇥'],
    ['Print', 'Print'],
  ];

  const actions = useActions();
  const stageState = useStageState();
  let ran = $state<string | null>(null);

  // Print goes to your UI adapter. This one says so, instead of opening the browser's print dialog.
  useActionsUiAdapter({ print: () => (ran = 'Print: your print handler ran') });

  // On load, as if a "Last page" button in the PDF was clicked. Running it twice changes nothing.
  onMount(() => {
    void actions.executeNamed('LastPage').then(({ status }) => (ran = `LastPage: ${status}`));
  });

  async function runNamed(name: PdfNamedAction) {
    const { status } = await actions.executeNamed(name);
    if (name !== 'Print') ran = `${name}: ${status}`;
  }
</script>

<div class="toolbar">
  {#each VERBS as [name, label] (name)}
    <button
      type="button"
      class="button"
      disabled={!actions.canExecuteNamed(name)}
      onclick={() => runNamed(name)}
    >
      {label}
    </button>
  {/each}
  <span class="spacer"></span>
  <output class="readout">
    Page {stageState.currentPageIndex + 1} of {stageState.pageCount}
    {#if ran}
      <span class="ran"> · {ran}</span>
    {/if}
  </output>
</div>
```

## Methods

Everything `useActions()` gives you.

| Method                                                        | What it does                                                                                                                                                                                                                                                |
| :------------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `executeNamed(name, context?)`                                | Runs a viewer action by its name, such as `'NextPage'`, as if the user clicked it. [Run an action from code](#run-an-action-from-code)                                                                                                                      |
| `execute(tree, context)`                                      | Runs actions you already have, such as a tree from `getActionTree()`, under the rules. Resolves `{ status, nodes, diagnostics }`: an action that fails is reported there, not thrown.                                                                       |
| `dispatch(trigger)`                                           | Reports that something happened, such as a click on a link or a page opening, and runs the actions behind it. The viewer does this for its own clicks and pages. Never rejects: problems come back in the result.                                           |
| `getActionTree(source)`                                       | Reads the actions behind a link or button, a form field, a page or the document, without running them. Resolves `null` when there are none.                                                                                                                 |
| `isScriptingEnabled()`                                        | Whether this document's JavaScript runs: it's turned on and the script engine started. [Turn on JavaScript](#turn-on-javascript)                                                                                                                            |
| `setUiAdapter(adapter)`                                       | Installs your handlers for websites, printing and alerts in place of the ones before, and returns a function that removes them. `useActionsUiAdapter()` does this for you. [How websites open, printing and alerts](#how-websites-open-printing-and-alerts) |
| `setSubmitHandler(handler)`                                   | Sets the function a form's Submit button hands its values to, with the address the PDF names. The viewer never sends them anywhere itself. Returns a function that removes it.                                                                              |
| `registerExecutor(type, executor)`                            | Runs one kind of action, such as `'goto'`, with your own code in place of the built-in one. Returns a function that removes it.                                                                                                                             |
| `runDocumentVerb(verb, operation)`                            | Runs your own save (`'save'`) or print (`'print'`) between the document's before and after actions, and resolves with what `operation` returns. `download()` and `print()` do this by themselves.                                                           |
| `prepareClose()`                                              | Runs the document's closing actions. Call it before `documents.close()`, which skips them.                                                                                                                                                                  |
| `getSettings()`, `updateSettings(changes)`, `resetSettings()` | Read the settings, change the rules while the app runs, and go back to what you registered. [Settings](#settings)                                                                                                                                           |
| `canExecute(tree, context)`, `canExecuteNamed(name)`          | Whether the rules would run these actions, or this viewer action. [Change the rules](#change-the-rules)                                                                                                                                                     |
| `canDispatch(trigger)`                                        | Whether that kind of trigger is turned on in the `triggers` setting. [Settings](#settings)                                                                                                                                                                  |

`context` is `{ origin, source, event }`: why the actions run (`'user'`, `'hover'` or `'lifecycle'`, as
in [Change the rules](#change-the-rules)), what started them, and which event it was. `executeNamed()`
fills it in as a click from your code. Every method that returns a promise also takes a `signal` in
its options, to cancel it.

## Settings

Set them when you register the plugin, and change them while the app runs with
`actions.updateSettings({ … })`; `actions.resetSettings()` goes back to what you registered, and
`useActionsSettings()` gives you the current settings.

| Setting               | What it does                                                                                                                                                                                      | Default             |
| :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------ |
| `javascript.enabled`  | Run the document's JavaScript. A change applies to documents opened after it, since a document's script engine starts when it opens                                                               | `false`             |
| `javascript.identity` | Who scripts think the user is, on top of the document's user                                                                                                                                      | the document's user |
| `policy`              | The rules above, per kind of action and per origin                                                                                                                                                | as in the table     |
| `triggers`            | Run actions of the `document`, its `page`s, or `annotation`s                                                                                                                                      | all `true`          |
| `openSequence`        | When the document's opening actions run: `'auto'`, `'headless'` (as the document opens, for an app without a Stage), `'off'`. A change applies to documents whose opening actions haven't run yet | `'auto'`            |

## Events

**`action-diagnostics.svelte`**

```svelte
<script lang="ts">
  import { useActionsEvent } from '@embedpdf/svelte/actions';

  useActionsEvent(
    (actions) => actions.onDiagnosticReported,
    ({ code, action }) => console.warn(`Action ${action} was not run: ${code}`),
  );
</script>
```

| Event                        | Fires when                                                                                                            | Carries                    |
| :--------------------------- | :-------------------------------------------------------------------------------------------------------------------- | :------------------------- |
| `onExecuted`                 | Actions ran, whatever started them                                                                                    | `tree`, `result`, `source` |
| `onDiagnosticReported`       | An action was blocked, reported or couldn't run                                                                       | `code`, `action`, `source` |
| `onScriptFailed`             | A script threw an error                                                                                               | `error`, `source`          |
| `onScriptDiagnosticReported` | A script was stopped from doing something, such as reaching the network, or used something the viewer doesn't support | `code`, `message`          |
| `onOpenSequenceCompleted`    | The document's opening actions ran                                                                                    | `result`                   |
| `onSettingsChanged`          | A setting changed                                                                                                     | `settings`, `changed`      |

## Permissions

Actions don't need a permission of their own, but some things they do do: printing needs
`doc.print`, and changing a form field needs `doc.forms.fill`. An action the user may not perform
is reported, never run.

## Good to know

### When opening actions run

With `openSequence: 'auto'`, the actions a document runs when it opens wait for the first sign of a
person: the UI adapter being installed, or a first click. So a document that plays a script on open
doesn't do it in a background tab nobody has looked at.

### Scripts and forms

Form calculations, validation and formatting are scripts too: with JavaScript on, the
[form plugin](../forms) runs them as people type. See [Filling forms](../forms).
