---
title: "Your own components — Svelte"
description: "Put your own React, Vue, Svelte or Angular components in the viewer's toolbars, panels and header, with your styles, your app's state, and the viewer's commands."
integration: "Svelte"
source: "https://www.embedpdf.com/docs/viewer/svelte/customize/components"
---

# Your own components

Put your own component in a toolbar, in a panel, or above and below the viewer. It stays a component
of your app: your framework renders it, your CSS styles it, and it reads your app's state as it always
does. The viewer only decides where it goes.

Two steps: make room for it in the [layout](layout), then give the viewer the component. This one
puts a version picker from your app in the toolbar:

**`toolbar-item.svelte`**

```svelte
<script lang="ts">
  import { PDFViewer, ToolbarItem, type Layout } from '@embedpdf/viewer-svelte';
  import { versions } from './versions.svelte'; // your app

  const layout = (layout: Layout) =>
    layout.add({ custom: 'versions', command: 'acme:versions' }, { to: 'main', section: 'start' });
</script>

<PDFViewer src={versions.currentUrl} {layout} style="height: 100vh">
  <ToolbarItem id="versions">
    <select class="versions" bind:value={versions.current}>
      {#each versions.list as version (version.id)}
        <option value={version.id}>{version.label}</option>
      {/each}
    </select>
  </ToolbarItem>
</PDFViewer>
```

`{ custom: 'versions' }` makes room in the toolbar, and the id ties it to your component. The
toolbar measures your component like any button, so it fits it in, and when the toolbar runs out of
room, `command` takes its place in the "More" menu: here, a command of yours that opens the same
choice in a dialog. Leave `command` out, and your component stays in the toolbar while other items
make room.

## Know where it sits

The same component can sit in a top bar, a vertical rail or a floating bar. `useToolbarItem()` tells
it where it is, so it can lay itself out:

**`item-context.svelte`**

```svelte
<script lang="ts">
  import { useToolbarItem } from '@embedpdf/viewer-svelte';

  let { people }: { people: { id: string; name: string; avatar: string }[] } = $props();
  const item = useToolbarItem(); // item.orientation is 'horizontal' or 'vertical'
</script>

<div class="reviewers {item.orientation}">
  {#each people as person (person.id)}
    <img src={person.avatar} alt={person.name} title={person.name} />
  {/each}
</div>
```

| Field         | What it is                                                                           |
| :------------ | :----------------------------------------------------------------------------------- |
| `orientation` | `'horizontal'` or `'vertical'`                                                       |
| `edge`        | The bar's place: `'top'`, `'bottom'`, `'start'`, `'end'`, `'float'` or `'selection'` |
| `variant`     | Which of your `variants` the toolbar picked, when you gave some                      |
| `size`        | The viewer's [size](small-screens): `'compact'`, `'medium'` or `'expanded'`          |

The same values are on your element as `data-orientation`, `data-edge`, `data-variant` and
`data-size`, for CSS.

To have the toolbar pick a smaller form when it's short of room, list your forms, largest first:
`{ custom: 'versions', variants: ['full', 'short'], command: 'acme:versions' }`. Draw the one
`variant` names.

## Look like the viewer

`Button`, `Menu` and `MenuItem` are the viewer's own, for your components. A menu made with them
opens the way the viewer's menus do: away from its bar, flipped when there's no room, and up from
the bottom on a phone.

**`menu.svelte`**

```svelte
<script lang="ts">
  import { Menu, MenuItem } from '@embedpdf/viewer-svelte';
  import { copyLink, emailLink } from './share'; // your app
</script>

<Menu icon="link" label="Share">
  <MenuItem icon="copy" onselect={copyLink}>Copy link</MenuItem>
  <MenuItem icon="message" onselect={emailLink}>Email a link</MenuItem>
</Menu>
```

| Component  | What it is                                                                                                                                     |
| :--------- | :--------------------------------------------------------------------------------------------------------------------------------------------- |
| `Button`   | A toolbar button. `command="zoom:in"` makes it that command, with its icon, tooltip and state; or give it `icon`, `label` and a click handler. |
| `Menu`     | A button that opens a menu: `icon`, `label`, and `MenuItem`s inside                                                                            |
| `MenuItem` | A row in a menu: `icon`, its text, and what happens when it's picked. `command="…"` makes it a command.                                        |

Icons are the viewer's icons and [yours](commands#icons), by name. For anything else, the viewer's
colors are CSS variables your styles can use: `var(--epdf-surface)`, `var(--epdf-text)`,
`var(--epdf-border)`, `var(--epdf-accent)`. See [Theming](theming#in-your-own-components).

## Use the viewer's commands and state

`useCommand(id)` gives you a command as a button needs it: its `label`, `icon` and `shortcut`,
whether it's `enabled`, `active` and `visible`, and `run()`. It's always there: an id the viewer
doesn't have is neither visible nor enabled. And every hook from the [headless
docs](https://www.embedpdf.com/docs/headless/svelte) works inside the viewer, so your component reads the viewer's state the same
way a headless app does:

**`zoom.svelte`**

```svelte
<script lang="ts">
  import { Button, useCommand } from '@embedpdf/viewer-svelte';
  import { useStageState } from '@embedpdf/svelte/stage';

  const stage = useStageState();
  const fitWidth = useCommand('zoom:fit-width');
</script>

<div class="zoom-pill">
  <Button command="zoom:out" />
  <span>{Math.round(stage.zoomLevel * 100)}%</span>
  <Button command="zoom:in" />
  <button class="chip" aria-pressed={fitWidth.current.active} onclick={fitWidth.current.run}>
    {fitWidth.current.label}
  </button>
</div>
```

`useStageState()` is the [Stage](https://www.embedpdf.com/docs/headless/svelte/viewing/stage#state) plugin's state, from the
headless adapter for your framework. Install it next to the viewer (`@embedpdf/react`,
`@embedpdf/vue`, `@embedpdf/svelte` or `@embedpdf/angular`) and use any of its hooks: the search
results, the selected annotation, the form values.

## A panel of your own

Add the panel to the layout with a side, a title and an icon, then give the viewer what goes inside:

**`panel.svelte`**

```svelte
<script lang="ts">
  import { PDFViewer, Panel, type Layout } from '@embedpdf/viewer-svelte';
  import Notes from './Notes.svelte'; // your app

  const layout = (layout: Layout) =>
    layout
      .addPanel('notes', { side: 'end', title: 'Notes', icon: 'book' })
      .add('panel:notes', { to: 'main', section: 'end' });
</script>

<PDFViewer src="/report.pdf" {layout} style="height: 100vh">
  <Panel id="notes">
    <Notes />
  </Panel>
</PDFViewer>
```

Every panel gets a `panel:<id>` command that opens and closes it, so `panel:notes` is the toolbar
button. The viewer draws the panel's frame: the header with its title and close button, and the way
it opens, beside the pages on a wide screen and up from the bottom on a phone. You draw what's
inside. See [Panels](panels).

## Above, below, and when nothing is open

Regions are places around the viewer for your own components:

**`region.svelte`**

```svelte
<script lang="ts">
  import { PDFViewer, Region } from '@embedpdf/viewer-svelte';
  import AppHeader from './AppHeader.svelte'; // your app
  import RecentFiles from './RecentFiles.svelte';
</script>

<PDFViewer style="height: 100vh">
  <Region name="header"><AppHeader /></Region>
  <Region name="empty"><RecentFiles /></Region>
</PDFViewer>
```

| Region   | Where it is                                 |
| :------- | :------------------------------------------ |
| `header` | Above everything, across the whole viewer   |
| `footer` | Below everything                            |
| `tabs`   | In place of the document tabs, for your own |
| `empty`  | Over the pages while no document is open    |

A region you don't fill shows nothing, or the viewer's own (the tabs).

## In the menu over selected text

To add an action to the bar that appears over selected text, such as "Explain this", make it a
command and add it there:

```js
{
  commands: [
    {
      id: 'acme:explain',
      label: 'Explain',
      icon: 'message',
      run: async ({ get }) => openAssistant(await get('selection').readText()),
    },
  ],
  layout: (layout) => layout.add('acme:explain', { to: 'selection' }),
}
```

The bar over selected annotations is `annotation`. See [Menus](menus#over-a-selection).

## Good to know

### Your styles apply, the viewer's don't

Your component lives in your page, not inside the viewer's shadow DOM. Your CSS styles it, and
the viewer's CSS never touches it. It does inherit the viewer's font and text color and its CSS
variables, so plain elements fit in without any styling.

### One id, one component

Each `id` is one item in one place. To show the same component in two bars, give each place its
own id.

### Keyboard

Your item is part of the toolbar's keyboard order. Arrow keys move to it like any button; the first
element in it that takes focus gets it. Inside your item, keys work as you wrote them: the viewer's
shortcuts never fire while focus is in a text field or a `<select>`.

### Plain HTML

Without a framework, give the element a `slot` attribute: `item:<id>`, `panel:<id>`, or the
region's name. The viewer's components are elements too: `<epdf-button>`, `<epdf-menu>` and
`<epdf-menu-item>`, which fire a `select` event when picked.
