Modular UI Customization Showcase Demo Code Sample

Requirements
View Demo

Easily customize the viewer UI through modular components defined in JSON. Each component can be specified independently, enabling dynamic and granular control over the interface layout and behavior.

This demo allows you to:

  • Select from sample JSON files to switch the UI components:
    • Default UI
    • Alternate UI
    • Vertical Headers
    • No Ribbons
    • Ribbons with Icons
  • Review modular components and their structure in JSON data files

Implementation steps
To add Modular UI Customization capability with WebViewer:

Step 1: Get started with WebViewer in your preferred web stack
Step 2: Add the ES6 JavaScript sample code provided in this guide

Once you generate your license key, it will automatically be included in your sample code below.

License Key

1
2// ES6 Compliant Syntax
3// GitHub Copilot - October 7, 2025
4// File: modular-ui-customization/index.js
5
6import WebViewer from '@pdftron/webviewer';
7import { saveAs } from 'file-saver';
8
9const element = document.getElementById('viewer');
10const onLoad = async (instance) => {
11 instance.UI.enableFeatureFlag(instance.UI.FeatureFlags.CUSTOMIZABLE_UI);
12
13 // Load default configuration.
14 const defaultConfig = await loadConfiguration(0);
15 if (defaultConfig) {
16 configurationOptions[0].cachedConfig = defaultConfig;
17 instance.UI.importModularComponents(defaultConfig);
18 }
19};
20
21let theInstance = null;
22// Initialize WebViewer and load default document.
23WebViewer(
24 {
25 path: '/lib',
26 licenseKey: 'YOUR_LICENSE_KEY',
27 initialDoc: 'https://apryse.s3.amazonaws.com/public/files/samples/WebviewerDemoDoc.pdf',
28 enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File.
29 },
30 element
31).then((instance) => {
32 theInstance = instance;
33 onLoad(instance);
34});
35
36// Configuration metadata with file mapping
37const configurationOptions = [
38 {
39 title: 'Default UI', configFile: 'default.json', cachedConfig: null,
40 description: 'The default UI features our out-of-the-box ribbon interface created using Modular Components.',
41 },
42 {
43 title: 'Alternate UI', configFile: 'custom-left.json', cachedConfig: null,
44 description: 'This alternate UI replaces the left tab panel with individual toggle buttons to open standalone panels.\n' +
45 'A vertical header is added to the left side of the UI to house the panel toggles and the select/pan tools.',
46 },
47 {
48 title: 'Vertical Headers', configFile: 'vertical.json', cachedConfig: null,
49 description: 'This configuration features vertical headers on the left and right sides of the UI.\n' +
50 'The left header contains the icon ribbons tools and the right header contains panel toggles as well as page navigation controls.',
51 },
52 {
53 title: 'No Ribbons', configFile: 'no-ribbons.json', cachedConfig: null,
54 description: 'This configuration uses tools without ribbons, instead, the tools are placed in the top header.',
55 },
56 {
57 title: 'Ribbons with Icons', configFile: 'ribbons-with-icons.json', cachedConfig: null,
58 description: 'This configuration uses tools with ribbons and icons, as well as a right vertical header with panel toggles as well as page navigation controls.',
59 },
60 {
61 title: 'Custom Configuration', configFile: '', cachedConfig: null, description: null,
62 },
63];
64
65const customConfigIndex = configurationOptions.findIndex(config => config.title === 'Custom Configuration');
66
67// Function to load configuration file.
68const loadConfiguration = async (configIndex) => {
69 const configFile = configurationOptions[configIndex].configFile;
70 if (configFile === '') return null;
71
72 try {
73 const response = await fetch(`/showcase-demos/modular-ui-customization/modular-ui/${configFile}`);
74 if (!response.ok) {
75 throw new Error(`Failed to load configuration: ${response.statusText}`);
76 }
77 return await response.json();
78 } catch (error) {
79 console.error('Error loading configuration:', error);
80 return null;
81 }
82};
83
84// UI section
85
86// Create a container for all controls (labels, radio buttons and button).
87const controlsContainer = document.createElement('div');
88
89// Create a radio group for configurations.
90const configGroup = document.createElement('div');
91
92configurationOptions.forEach((config, index) => {
93 const label = document.createElement('label');
94 label.textContent = config.title;
95 label.htmlFor = config.title;
96 label.className = 'radio-label-class';
97
98 const radio = document.createElement('input');
99 radio.type = 'radio';
100 radio.name = 'config';
101 radio.id = config.title;
102 radio.value = config.configFile;
103 radio.checked = (index === 0); // Check the first option by default.
104 radio.disabled = (index === customConfigIndex);
105 configGroup.appendChild(radio);
106 configGroup.appendChild(label);
107 radio.onchange = async () => {
108 descriptionElement.textContent = configurationOptions[index].description || '';
109 // If we already loaded this configuration, use the cached version.
110 if (configurationOptions[index].cachedConfig) {
111 await theInstance.UI.importModularComponents(configurationOptions[index].cachedConfig);
112 return;
113 }
114 // Otherwise, load it from the file.
115 const config = await loadConfiguration(index);
116 if (config) {
117 configurationOptions[index].cachedConfig = config;
118 await theInstance.UI.importModularComponents(config);
119 }
120 };
121});
122
123controlsContainer.appendChild(configGroup);
124// Create a description area.
125const descriptionElement = document.createElement('div');
126descriptionElement.style.marginTop = '10px';
127descriptionElement.style.whiteSpace = 'pre-wrap';
128descriptionElement.textContent = configurationOptions[0].description;
129controlsContainer.appendChild(descriptionElement);
130
131// Create a hidden file input for uploading configuration files.
132const fileUpload = document.createElement('input');
133fileUpload.style.display = 'none';
134fileUpload.type = 'file';
135fileUpload.accept = '.json';
136fileUpload.onchange = (event) => {
137 const file = event.target.files[0];
138 if (file) {
139 const reader = new FileReader();
140 reader.onload = async (e) => {
141 try {
142 const jsonContent = JSON.parse(e.target.result);
143 await theInstance.UI.importModularComponents(jsonContent);
144 descriptionElement.textContent = 'Custom configuration loaded from file: ' + file.name;
145 configurationOptions[customConfigIndex].cachedConfig = jsonContent;
146 configurationOptions[customConfigIndex].description = 'Cached custom configuration from file: ' + file.name;
147 const customRadio = document.getElementById('Custom Configuration');
148 customRadio.disabled = false;
149 customRadio.checked = true;
150 } catch (error) {
151 console.error('Invalid JSON file:', error);
152 descriptionElement.textContent = 'Error Loading Configuration, See Console For Details';
153 }
154 };
155 reader.readAsText(file);
156 }
157};
158
159const downloadJSON = (data) => {
160 // Convert JSON data to string.
161 const jsonString = JSON.stringify(data, null, 2); // Pretty print with 2 spaces indentation
162
163 // Create a Blob from the JSON string.
164 const blob = new Blob([jsonString], { type: 'application/json' });
165
166 // Use FileSaver's saveAs function to trigger the download.
167 saveAs(blob, 'modular_components.json');
168};
169
170// Create a button to load the selected configuration.
171const buttonLoad = document.createElement('button');
172buttonLoad.textContent = 'Load UI Configuration';
173buttonLoad.onclick = async () => {
174 fileUpload.click();
175}
176controlsContainer.appendChild(buttonLoad);
177// Create a button to download the current configuration.
178const buttonDownload = document.createElement('button');
179buttonDownload.textContent = 'Download UI Configuration';
180buttonDownload.onclick = async () => {
181 try {
182 const data = theInstance.UI.exportModularComponents();
183 downloadJSON(data);
184 } catch (error) {
185 console.error('Error exporting modular components:', error);
186 }
187}
188controlsContainer.appendChild(buttonDownload);
189// Apply classes for styling using CSS.
190buttonLoad.className = 'btn-style';
191buttonDownload.className = 'btn-style';
192controlsContainer.className = 'control-container';
193// Append elements to the controls container.
194element.parentElement.insertBefore(controlsContainer, element);
195

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales