Customizing the Nutrient Web SDK UI
The Web Viewer supports an API for customizing various parts of the SDK’s user interface (UI). This API enables in-place UI customization of various components in the SDK.
Use cases include:
- Fully replace the default component UI (for example, the comment thread) with your own
- Insert a custom UI at a predefined slot in an existing component (for example, a custom header in the comment thread)
- Replace an existing slot in a component with your own custom UI (for example, replace the default editor in the comment thread with your own UI)
The support for new customization API is currently limited to a few components. We will be expanding this in the future.
Slots
The UI customization API is largely enabled through the concept of slots. These are passed to the SDK as part of load configuration using the ui
property.
NutrientViewer.load({ // ... your config ui: { // config for UI customization }});
You can think of slots as predefined placeholders in the Web SDK UI where you can place your custom UI. The slots themselves might have a default UI, which can be replaced with a custom one, or they might be empty, enabling you to insert your own UI.
To start off, we’ve introduced a few slots that can be customized. For a list of currently supported slots, you may refer to the supported slots guide. We’ll gradually expand the slots to cover more components and functionalities.
Consider the following example of the comment thread component:

The comment thread component is a slot that can be fully replaced with a custom UI. Besides being a slot itself, it also comprises (nested) slots within.
Concepts
Consider an example where you want to fully replace the entire comment thread UI with your own custom implementation. Specify the commentThread
slot in the ui
configuration.
NutrientViewer.load({ // ... your config ui: { commentThread: ... }});
To provide a custom UI, a slot accepts a function that returns an object with a render
method. The render
method should return a DOM Node. Example:
NutrientViewer.load({ // ... your config ui: { commentThread: (instance, id) => { return { render: (params) => { const div = document.createElement("div"); // customize the div as needed // ...
// return a DOM Node return div; } }; } }});
Here’s the signature of the function which returns the render
method:
type UIFactory = (instance, id) => { render?: (params) => HTMLElement | null;};
The SDK internally calls the UIFactory
function once as it prepares to render the associated component. You can think of it as an initialization phase for the slot. It receives a couple of parameters - instance
and id
.
instance
is the instance of Nutrient Web SDK and you may make full use of the SDK APIs available on it.id
is a unique identifier for the specific component being rendered into the slot. A component, such as comment thread, can have multiple instances, and theid
helps to differentiate between them.id
is a string value, for example01K2HFFCTB5MZKY5H7P7XGYBGG
which refers to a comment thread id.
After the UIFactory
function is called, the SDK stores a reference to the returned object containing the render
method.
The SDK will internally call render
anytime params
change and it expects a change in the particular UI. render
will be called with the updated params
. Following are important points to note:
- The returned DOM Node from
render
will be placed into the specified slot. - If the specified slot is empty, the returned DOM Node will be appended to the slot DOM container.
- If the slot already has a DOM Node, it will be replaced with the returned DOM Node from
render
. - If the slot already has a default UI, it will be replaced with the custom UI, for example, comment thread UI has an
editor
slot which renders the default editor UI. You may replace the UI contained in this slot with your own custom UI. - If the slot has no default UI, the custom UI will be inserted into the slot, for example, comment thread has a
header
slot which is empty by default. You can insert your own custom UI into this slot.
render
may be called multiple times, so you should treat it as a pure function and avoid side effects inside it, for example, avoid changing any pre-existing variables or objects outside its scope. You should use render
to return a DOM Node that represents the current state of the UI, for example, if render
is called multiple times with the same params
you should write the logic such that it formulates the same expected markup each time.
The params
received by render
contains properties related to the specific slot (such as id
) that may be useful for constructing the UI. We’ll be adding more useful properties to params
incrementally.
Fully customize a slot
Below is an example of how you can fully customize the comment thread UI with your own:
NutrientViewer.load({ // ... your config ui: { commentThread: (instance, id) => ({ render: (params) => { // return a DOM Node const div = document.createElement("div"); div.style.backgroundColor = "lightblue"; div.style.padding = "10px"; div.style.border = "1px solid #ccc"; div.style.borderRadius = "5px"; div.innerText = `This is a custom UI for the comment thread: ${id}`;
return div; } }) }});
In the above example, you’ll notice the function passed to the commentThread
slot follows the same UIFactory
signature as described earlier. This will replace the default comment thread UI with the returned custom UI:

Nested slots
Slots form a hierarchical structure representing the tree of components available for UI customization. This enables partial customization use cases where you want to replace only a part of the component UI, while keeping the rest intact. This is especially useful where you just need to insert a custom UI at a specific location in the component.
In order to enable nested slots for a particular slot you can pass an object, instead of a function. To customize a nested slot, you can then pass a function to the nested slot key having the same UIFactory
signature.
Below is an example of how you can customize the nested slots for header
and footer
in comment thread UI with your own:
NutrientViewer.load({ // ... your config ui: { // instead of UIFactory function, we pass an object to commentThread slot specifying nested slots commentThread: { // nested slot for header having the same UIFactory signature header: () => { return { render: () => { const div = document.createElement("div"); div.style.backgroundColor = "lightgreen"; div.style.padding = "5px"; div.innerText = "This is a custom header for the comment thread.";
return div; }, }; }, // nested slot for footer having the same UIFactory signature footer: () => { return { render: () => { const div = document.createElement("div"); div.style.backgroundColor = "lightcoral"; div.style.padding = "5px"; div.innerText = "This is a custom footer for the comment thread.";
return div; }, }; }, } }});
The above example inserts the specified custom UI into the header and footer slots respectively, while keeping the rest of the comment thread UI intact.

Lifecycle methods
Besides the render
method, you can also define lifecycle methods in the object returned by the UIFactory
function. These methods enable you to perform actions at specific points in the component’s lifecycle. They’re also useful for effects such as adding event listeners or performing cleanup.
They also receive an id
parameter which can be used to identify the specific instance of the component.
Following are the currently supported lifecycle methods:
onMount
— Called when the component is mounted. You can use this to perform any setup actions, such as adding event listeners, and analytics events.onUnmount
— Called when the component is unmounted. You can use this to perform any cleanup actions, such as removing event listeners.
Below is an example of how you can use lifecycle methods in the commentThread
slot:
NutrientViewer.load({ // ... your config ui: { commentThread: (instance, id) => { const div = document.createElement("div");
return { render: (params) => { // return a DOM Node div.innerText = `This is a custom UI for the comment thread`; return div; }, onMount: (id) => { console.log(`Comment thread mounted with id: ${id}`); // You can add event listeners or perform other setup actions here }, onUnmount: (id) => { console.log(`Comment thread unmounted with id: ${id}`); // You can remove event listeners or perform other cleanup actions here } }; } }});
Similarly, you can use lifecycle methods in nested slots as well. Below is an example of how you can use lifecycle methods in the header
slot of the commentThread
:
NutrientViewer.load({ // ... your config ui: { commentThread: { header: (instance, id) => { const div = document.createElement("div"); div.innerText = "This is a custom header for the comment thread.";
return { render: () => div, onMount: (id) => { console.log(`Header mounted with id: ${id}`); // You can add event listeners or perform other setup actions here }, onUnmount: (id) => { console.log(`Header unmounted with id: ${id}`); // You can remove event listeners or perform other cleanup actions here } }; }, } }});
Conclusion
The UI customization API provides ways to fully or partially customize various parts of the Nutrient Web SDK UI with slots. Currently the supported slots are limited, but we will be expanding them in the near future.
For a list of currently supported slots, refer to the supported slots guide.