A custom document viewer component for Retool
reading · 00%

A custom document viewer component for Retool

Previews for the file formats Retool's built-in components do not render.

Published
Reading4 min
CategoryRetool
Words987

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. maxFileSizeMB caps 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. onDocumentLoaded and onLoadError let 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.

Shell
git clone https://github.com/StackdropCO/custom-doc-viewer.git
cd custom-doc-viewer
npm install

Log 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.

Shell
npx retool-ccl login

Start development mode. Changes sync into your Retool organization as you save, under the library's dev version.

Shell
npm run dev

When it works, deploy. Each deploy publishes an immutable version of the library.

Shell
npm run deploy

Then 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:

TSXsrc/index.tsx
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:

JavaScript
// From a File Input component
{{ fileInput1.value[0] }}

// From a query that returns base64 content
{{ getDocument.data.base64Content }}

Optional properties:

  • maxFileSizeMB, for example 10 for a 10 MB limit or 100 for 100 MB.

  • backgroundColor, to match the app.

  • showLoadingState, to show or hide the loading indicator.

Read the component's state anywhere in the app:

JavaScript
// 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.

Filed underRetool
Worth passing along? Share this note.
LinkedInX
Newsletter

Get the next one in your inbox

We write when something is worth reading. Subscribing means you are happy to hear from us when that happens. Leave any time. Privacy policy.

Vaggelis Kapetanakis

Keep readingRetool
Related notes

Still here? Good sign

More notes from real builds, written up for the next person who needs one.

All articles
ContactLet's chat
Let's chat

Tell us what
you're building

Describe the process that is slowing your team down: the inbox, the spreadsheet, the portal nobody owns. We'll come back with a concrete approach, a rough timeline, and, where it helps, a working prototype within weeks.

What happens next
  1. We read every messageAn architect reads what you sent before anyone replies.
  2. You hear back in ≤ 2 daysA short reply with first questions and a suggested next step.
  3. We scope and prototypeA scoping call, then a working surface, not a slide deck.
Where are you in the process?