A contract arrives through a File Input as a base64-encoded .docx. The operator needs to read it before approving, inside the app, and the file cannot leave the company's network. Rendering Office Open XML in the browser takes a parser, and the usual shortcuts send the document somewhere else first.
This note covers the component we built for that case, what it does, and how to add it to a Retool app. The code is open source on GitHub under the MIT license.
Why the usual routes do not fit
There are three common ways to preview a Word file in a web app. Each one breaks an internal-only requirement or adds a service to run.
Google Docs or Office Online embeds need a publicly reachable URL for the file.
Server-side conversion to PDF adds a conversion service, its latency, and its maintenance.
Popular React viewers such as react-doc-viewer embed Microsoft's Office Online iframe for Word files, so the document still leaves the browser.
For contracts, specifications, and internal reports, none of those is acceptable. We wanted a component that renders the .docx in the browser and sends nothing anywhere.
What the component does
The Custom Doc Viewer is a Retool custom component that renders .docx files entirely client-side with the docx-preview library. It parses the document's XML and renders it as HTML and CSS inside the component.
Base64 input. It accepts raw base64 or a data URL, from a File Input or a query.
Size limit.
maxFileSizeMBcaps the file size, 50 MB by default, so a large file cannot exhaust the browser's memory.Responsive scaling. Pages scale to the component's width and stay centered.
Loading, error, and empty states. Each is built in, with a clear message.
Events.
onDocumentLoadedandonLoadErrorlet the app react to each outcome.Accessibility. ARIA attributes, screen reader support, and keyboard navigation.
It supports .docx only. A legacy .doc file shows an error message, because docx-preview reads Office Open XML and the older binary format is different.
Install and deploy
Clone the repository and install its dependencies. You need Node.js 20 or later.
git clone https://github.com/StackdropCO/custom-doc-viewer.git
cd custom-doc-viewer
npm installLog in to your Retool organization with the custom component CLI. It asks for an API access token with read and write scopes for Custom Component Libraries.
npx retool-ccl loginStart development mode. Changes sync into your Retool organization as you save, under the library's dev version.
npm run devWhen it works, deploy. Each deploy publishes an immutable version of the library.
npm run deployThen open the Custom Component settings in your Retool app and switch the library from dev to the version you just published. Production apps should always point at a published version.
How it connects to Retool
The entry point declares every property and event the app sees. This is the file as it ships:
import React from 'react';
import { Retool } from '@tryretool/custom-component-support';
import DocViewer from './DocViewer';
export const CustomDocViewer: React.FC = () => {
const [base64Data] = Retool.useStateString({ name: 'base64Data' });
const [backgroundColor] = Retool.useStateString({
name: 'backgroundColor',
initialValue: 'white',
});
// Configurable properties
const [maxFileSizeMB] = Retool.useStateNumber({
name: 'maxFileSizeMB',
initialValue: 50,
});
const [showLoadingState] = Retool.useStateBoolean({
name: 'showLoadingState',
initialValue: true,
});
// Hidden state for internal tracking (read-only from Retool)
const [isLoading, setIsLoading] = Retool.useStateBoolean({
name: 'isLoading',
initialValue: false,
inspector: 'hidden',
});
const [errorMessage, setErrorMessage] = Retool.useStateString({
name: 'errorMessage',
initialValue: '',
inspector: 'hidden',
});
// Events for Retool integration
const onDocumentLoaded = Retool.useEventCallback({ name: 'onDocumentLoaded' });
const onLoadError = Retool.useEventCallback({ name: 'onLoadError' });
return (
<DocViewer
base64Data={base64Data}
backgroundColor={backgroundColor}
maxFileSizeMB={maxFileSizeMB}
showLoadingState={showLoadingState}
isLoading={isLoading}
setIsLoading={setIsLoading}
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
onDocumentLoaded={onDocumentLoaded}
onLoadError={onLoadError}
/>
);
};The four inputs appear in the inspector. isLoading and errorMessage are hidden from the inspector and readable from anywhere in the app. Rendering, scaling, and validation live in src/DocViewer.tsx, and the base64 conversion in src/utils/.
Use it in an app
Once deployed, Custom Doc Viewer appears under Custom Components in the component picker. Drop it on the canvas and set base64Data to your document source:
// From a File Input component
{{ fileInput1.value[0] }}
// From a query that returns base64 content
{{ getDocument.data.base64Content }}Optional properties:
maxFileSizeMB, for example10for a 10 MB limit or100for 100 MB.backgroundColor, to match the app.showLoadingState, to show or hide the loading indicator.
Read the component's state anywhere in the app:
// Disable the Approve button while the document renders
{{ customDocViewer1.isLoading }}
// Show the reason a document failed in an alert
{{ customDocViewer1.errorMessage }}Attach handlers to the two events in the inspector. Use onDocumentLoaded to run a follow-up query or write an audit entry that the document was opened. Use onLoadError to show a notification with the error message.
Things to know
A custom component runs in its own iframe and keeps its own state. Set its colors through the backgroundColor property. To include the viewer in a form's submission, reference its properties in the query the form runs.
Render only documents from sources you trust. Rendering safety depends on docx-preview's XML parser, so keep that dependency updated and pin your app to a reviewed version of the library.
What is next
On the roadmap, not committed:
Zoom controls.
Page navigation for long documents.
Text search with highlighting.
More formats, such as .rtf, .odt, and legacy .doc, through additional libraries.
One-click printing through the browser's print dialog.
Issues and pull requests are welcome on the repository. More of our components are listed in awesome-retool-components.
Our view
We recommend client-side rendering for any internal app that previews contracts or reports, because the document never leaves the operator's browser. That keeps the preview inside the same access rules as the rest of the app. Pin production apps to a published version, and give the component a named owner who keeps docx-preview current. What is a custom Retool component covers when building one is worth it.