MeldUI

Document Viewer: Customization

Customize the toolbar, side panels, and viewer chrome via ToolbarConfig, slots, and CSS class overrides.

The default DocumentViewer chrome is configured to work out of the box, but every part of the toolbar and side-panel layout is replaceable.

The four control levers

DocumentViewer is controlled through four distinct, composable inputs:

LeverPropControlsBundle impact
FeaturesfeaturesWhich capabilities exist — registers the EmbedPDF plugin and shows its built-in toolbar button. All-or-nothing per flag.Disabled → plugin tree-shaken
Feature configfeatureConfigTuning for an enabled feature (zoom min/max, highlight palette, …).None
Toolbartoolbar (ToolbarConfig)Chrome composition — reorder/restrict groups, hide individual buttons, inject custom buttons.None
Slots + ref API#side-panels; template refInject extra side panels; drive the viewer programmatically (goToPage, createAnnotation, saveAsCopy, …).None

Built-in toolbar buttons are intentionally coupled to features: a flag that’s off means the plugin isn’t registered, so the button would be dead — it’s hidden rather than shown inert. When you want a button without the built-in behavior (e.g. a custom Print routed to your own handler), turn the feature off and add a customButtons entry. toolbar.groups/toolbar.hide can only further hide built-in buttons, never re-show one for a disabled feature.

For per-document capability/permission control, see Per-document permissions below — it rides on a per-document features object plus a :key remount.

Toolbar customization

Pass a toolbar prop typed as ToolbarConfig:

import type { ToolbarConfig } from '@meldui/vue'

const toolbar: ToolbarConfig = {
  // Restrict + reorder visible groups (omit `groups` entirely to show all, in
  // canonical order). Valid groups: pageNav, zoom, rotate, viewMode,
  // interactionMode, search, panels, annotate, stamp, sign, redact, actions.
  groups: ['pageNav', 'zoom', 'search', 'panels', 'actions'],
  // Hide individual built-in buttons by id (e.g. rotate-cw, rotate-ccw,
  // view-mode, interaction-mode, thumbnails, outline, comments, download,
  // print, fullscreen, zoom-in, zoom-out, zoom-preset, prev-page, next-page).
  hide: ['rotate-cw', 'rotate-ccw'],
  customButtons: [
    {
      id: 'share',
      label: 'Share', // tooltip + accessible label
      onClick: () => openShareDialog(),
      // isActive?: () => boolean   // optional pressed state
      // isDisabled?: () => boolean // optional disabled state
    },
  ],
}
<DocumentViewer source="/doc.pdf" wasm-url="/pdfium.wasm" :toolbar="toolbar" ... />

groups

Reorders or restricts the toolbar groups. The canonical order (used when groups is omitted) is:

;[
  'pageNav',
  'zoom',
  'rotate',
  'viewMode',
  'interactionMode',
  'search',
  'panels',
  'annotate',
  'stamp',
  'sign',
  'redact',
  'actions',
]

Pass a subset to hide other groups entirely. Pass a reordered array to change their visual order. A group only renders if its underlying feature is enabled — listing annotate does nothing unless features.annotations is on.

hide

Hides individual buttons by id, even when their group is visible. The built-in ids are:

prev-page, next-page, zoom-out, zoom-preset, zoom-in, rotate-ccw, rotate-cw, view-mode, interaction-mode, search, thumbnails, outline, comments, download, print, fullscreen.

customButtons

Append your own buttons. The CustomToolbarButton fields are:

  • id — unique within the toolbar (required)
  • label — tooltip + accessible (aria-label) text (required)
  • onClick — click handler (required)
  • isActive?() => boolean for a pressed/toggle state
  • isDisabled?() => boolean to disable the button

icon and group are not applied today. The CustomToolbarButton type declares both, but via <DocumentViewer> custom buttons currently render with a default icon in a fixed toolbar position (after the annotation tools, before the actions group). Don’t rely on icon/group for placement or glyph — use label for identification.

Side panel customization

The three default panels are:

  • OutlineOutlinePanel.vue (renders document bookmarks as a tree)
  • ThumbnailsThumbnailsPanel.vue (lazy-loaded thumbnail grid)
  • AnnotationsAnnotationsPanel.vue (list of comments + replies)

Each is enabled by its corresponding features flag (outline, thumbnails, commentThreads) and toggled from the toolbar.

To add your own panel alongside the built-ins, import the host (ViewerSidePanel) and place it in the #side-panels slot:

<script setup lang="ts">
import { ref } from 'vue'
import { DocumentViewer, ViewerSidePanel } from '@meldui/vue'
import MyCustomOutline from './MyCustomOutline.vue'

const sidePanelOpen = ref(false)
</script>

<template>
  <DocumentViewer source="/doc.pdf" wasm-url="/pdfium.wasm">
    <template #side-panels>
      <ViewerSidePanel v-if="sidePanelOpen" title="Custom Outline" @close="sidePanelOpen = false">
        <MyCustomOutline />
      </ViewerSidePanel>
    </template>
  </DocumentViewer>
</template>

ViewerSidePanel provides the host (header bar + close button + fixed width); your custom content fills its default slot. The built-in panels still render — #side-panels is additive, not a replacement.

Slot reference

<DocumentViewer> exposes a single slot:

SlotPurpose
side-panelsInject extra ViewerSidePanel panels next to the built-in outline / thumbnails / annotations panels

Replacing format renderers

To use a custom PDF / image / text / markdown renderer, import the renderer components directly:

import { PdfViewer, ImageViewer, TextViewer, MarkdownViewer } from '@meldui/vue'

You can also import the underlying composables and build a fully custom layout:

import { useTouch, buildCommands, useAnnotationThreads } from '@meldui/vue'

CSS class overrides

The viewer’s root element carries the document-viewer class and data-document-viewer attribute. Internal parts use prefixed classes (no document- prefix — see Theming):

ClassElement
document-viewerRoot container
viewer-toolbarSticky top toolbar
search-popoverSearch popover content
highlight-tooltipFloating tooltip on selected highlight
comment-formInline comment composer
pdf-viewerPDF renderer wrapper
pdf-pageEach page inside the PDF renderer
image-viewer / text-viewer / markdown-viewerNon-PDF renderer wrappers
annotations-panel / outline-panel / thumbnails-panelSide panel wrappers
annotation-rowA row inside the annotations panel
comment-markerOn-page sticky-note pin

Override these in your global CSS or via Tailwind utilities. The class prop on <DocumentViewer> is merged onto the root with cn() so per-instance overrides work cleanly.

<DocumentViewer class="rounded-xl border shadow-lg" source="/doc.pdf" wasm-url="/pdfium.wasm" />

Reloading the viewer with :key

Some inputs are read only once, at mount. The PDF path is powered by EmbedPDF, whose Vue <EmbedPDF> registers its plugin batch a single time inside onMounted and never watches the prop afterwards. DocumentViewer likewise does not watch(source). So changing one of these inputs at runtime is silently ignored — the recomputed plugin list never re-registers, and feature layers can end up half-wired.

The fix is to remount the component by binding a :key derived from the inputs that changed. When the key changes, Vue tears the viewer down and builds a fresh one with the new plugin batch, engine, and document.

<script setup lang="ts">
import { reactive, computed } from 'vue'
import { DocumentViewer, type ViewerFeatures } from '@meldui/vue'

const features = reactive<ViewerFeatures>({ zoom: true, search: true })
// Remount whenever a mount-only input changes:
const viewerKey = computed(() => JSON.stringify(features))
</script>

<template>
  <DocumentViewer :key="viewerKey" source="/doc.pdf" wasm-url="/pdfium.wasm" :features="features" />
</template>

Which inputs need a :key

InputBehaviorNeeds :key?
featuresMount-only — plugin batch registered once✅ yes
featureConfig (plugin-affecting parts)Mount-only — feeds the plugin registration✅ yes
source (same type) / mimeTypeMount-only — the document is loaded at init✅ yes
wasmUrl / workerRead once when the PDFium engine is created✅ yes
toolbarReactive — the toolbar reads its config live❌ no
downloadUrlReactive — read at download time❌ no
document type switch (pdf↔image↔text↔md)Already swaps the renderer internally❌ no
initialAnnotations / initialThreadsApplied by internal watchers❌ no

A remount resets in-viewer state — scroll position, current page, and zoom return to initialPage / initialScale, and any un-persisted in-session annotation edits are lost (persisted ones re-seed from initialAnnotations). Key only on inputs that genuinely require it, so toolbar tweaks, download-URL swaps, and annotation updates don’t trigger needless remounts.

Per-document permissions

There is no separate permissions prop. Because features is a mount-only input, per-document capabilities and permissions ride directly on the features object, recomputed on each remount.

This is the model to use when a left-hand tree/list swaps documents into one viewer: bind :key to the document id and compute features (and downloadUrl) from that document’s backend permissions.

<script setup lang="ts">
import { ref, computed } from 'vue'
import { DocumentViewer } from '@meldui/vue'

const activeDoc = ref(docs[0]) // { id, url, can_download, can_print, downloadUrl? }

// Capability + per-document permission collapse into one object. When the user
// picks another document the :key changes, the viewer remounts, and these flags
// (de)register the matching EmbedPDF plugins for the new document.
const features = computed(() => ({
  zoom: true,
  search: true,
  print: activeDoc.value.can_print,
  download: activeDoc.value.can_download,
  annotations: false,
}))
</script>

<template>
  <DocumentViewer
    :key="activeDoc.id"
    :source="activeDoc.url"
    :download-url="activeDoc.downloadUrl /* server-driven / watermarked copy */"
    :features="features"
    wasm-url="/pdfium.wasm"
  />
</template>

If you only need to hide a button for a user who still has the capability (rather than disable the whole feature), use toolbar.hide or a customButtons replacement instead of toggling features — that avoids a remount.

See also