The content team needs rich text with headings and lists, saved as structured data the website can render. The app's text area stores a plain string. Retool's built-in components cover every other requirement on the screen. This one needs a JavaScript library the app does not include.
That is the usual moment someone asks about a custom component. This piece covers what one is, how it works, what it costs to keep, and how to tell whether you need one.
What a custom component is
A custom Retool component is a UI element you write yourself, in React and TypeScript, that runs inside a Retool app beside the built-in components. It extends Retool's component library with a pattern the library does not include: a block editor, a document preview, a chart type, or anything built on a third-party JavaScript library.
Once deployed, it appears in the component picker like any other component. Builders drag it onto the canvas, set its properties in the inspector, and bind those properties to queries.
How it works
Each custom component runs in its own sandboxed iframe. That keeps its code and dependencies apart from the rest of the app, so a library you load cannot interfere with Retool's own.
The component talks to the app through a small set of hooks from Retool's @tryretool/custom-component-support package. Each hook declares a property, and that property appears in the inspector. Events work the same way, so the app can run a query when something happens inside the component.
This is the entry point of a document viewer we published, trimmed to one input, one output, and one event:
import React from 'react';
import { Retool } from '@tryretool/custom-component-support';
import DocViewer from './DocViewer';
export const CustomDocViewer: React.FC = () => {
// An input the builder sets in the inspector, e.g. {{ fileInput1.value[0] }}
const [base64Data] = Retool.useStateString({ name: 'base64Data' });
// An output the app reads as {{ customDocViewer1.errorMessage }}
const [errorMessage, setErrorMessage] = Retool.useStateString({
name: 'errorMessage',
initialValue: '',
inspector: 'hidden',
});
// An event the builder attaches a handler to in the inspector
const onDocumentLoaded = Retool.useEventCallback({ name: 'onDocumentLoaded' });
// The full component passes more props (size limit, loading state, styling)
return (
<DocViewer
base64Data={base64Data}
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
onDocumentLoaded={onDocumentLoaded}
/>
);
};Inputs come in as properties. Outputs go back as state the rest of the app can reference. Events trigger handlers the builder configures, with no extra code in the app.
A custom component keeps its own state. To submit its value with a form, reference the component's properties in the query the form runs. It also takes its styling from the props you expose, so colors and spacing are set as properties rather than in the inspector's style panels.
Libraries and versions
Components are grouped into a custom component library. You build it locally with Retool's retool-ccl CLI, preview changes live with npx retool-ccl dev, and publish with npx retool-ccl deploy.
Each deploy creates an immutable version. Apps pin to a version in their custom component settings, so a change in progress never reaches a production app until someone moves the pin. One library can serve every app in the organization, which keeps a shared component consistent across teams.
When you need one
Build a custom component when all three of these hold:
The requirement is real for the people using the app, beyond a visual preference.
No built-in component, or combination of them, covers it.
The pattern exists as a mature JavaScript library, or the logic is small enough to own.
The cases we see most often:
Rich text with structured output. A content operations team needed headings, lists, and quotes saved as JSON. We wrapped Editor.js in a custom component bound to a database query, so content saves and loads with no cleanup.
Previews of file formats. Word documents previewed inline, in the browser, with no public URL. That became our document viewer.
Data-aware controls. A filter that shows the distribution of the data behind it, built as a range slider with a histogram.
Reuse. The same specialized element needed in many apps, built once and versioned.
When a built-in component will do
More often than teams expect. Retool's built-in components cover tables, forms, charts, file inputs, and most layouts, and they inherit the app's theme and form behavior directly. A layout need can often be met with containers and the HTML component. For multiple interactive elements in HTML, see how to route HTML component events.
Try the built-in route first, with someone who knows the library well. A custom component is code your team owns from the day it ships.
What it costs to keep
The build is the small part. A custom component carries four ongoing costs:
Dependencies. The third-party library inside it needs updates, including security fixes.
Versions. Every app pinned to an old version needs moving when you publish a new one.
Ownership. Someone has to know the code when it misbehaves, a year from now.
Testing. A change to a shared component touches every app that uses it.
None of this is heavy for one well-scoped component with a named owner. It adds up quickly for a dozen components nobody owns.
Classic apps and the new app builder
Custom components, as described here, are the extension point for Retool's classic apps. Retool's new app builder works with code directly. Its apps are React, written in Retool's chat and code views or by a coding agent such as Claude Code, then deployed to Retool. There, the line between built-in and custom is drawn inside the app's own code.
Our view
We recommend a custom component only when no built-in one meets a requirement your users depend on, because each one is code to keep. When it is the right call, publish it in a versioned library with a named owner. Every app then gets the same component, and changes reach production deliberately.
Our open-source components are on GitHub, free to use and extend.