A custom RadioGroup component for Retool
reading · 00%

A custom RadioGroup component for Retool

Selection controls with the layouts and states the native RadioGroup does not offer.

Published
Reading4 min
CategoryRetool
Words949

The shipping selector on the order screen needs four carriers, each with its own service levels. Express should only appear for orders over $100, some services are domestic only, and the recommended option needs a badge. Built from standard parts, that becomes several Radio Groups, separate Text components for descriptions, and visibility rules spread across all of them.

Retool's RadioGroup handles a flat list of options well. This screen needed grouping, conditional rules, and option metadata inside one control. We built a custom component for it, written in TypeScript, and published it on GitHub under the MIT license.

What it does

The component takes one options array and renders it as a single selection control.

  • Single or multiple selection. Radio behavior by default, checkbox behavior with multipleSelect.

  • Four layouts. Vertical, horizontal, grid (with gridColumns), or justified.

  • Button position. Left, right, top, or bottom of each option.

  • Button shapes. Bullet, square, rounded square, or diamond.

  • Rich options. Titles, descriptions, icons, badges with their own color, and optional HTML rendering.

  • Group headers. Section headers inside the list, so large sets stay readable.

  • Conditional display. Each option can carry a showIf expression evaluated against app state.

  • Tooltips on truncation. Long text gets a tooltip only when it is cut off.

  • Styling. Colors, font sizes, alignment, and line clamping are all properties.

  • Keyboard access. Tab, Enter, and Space work, with ARIA labels for screen readers.

Install and deploy

You need Node.js 20 or later and an API access token with read and write scopes for Custom Component Libraries.

Shell
git clone https://github.com/StackdropCO/custom-radio-group-retool-component.git
cd custom-radio-group-retool-component
npm install
npx retool-ccl login

Run development mode while you work. Changes sync into your Retool organization on save.

Shell
npm run dev

Deploy when it is ready. Each deploy creates an immutable version of the library.

Shell
npm run deploy

In the app's Custom Component settings, switch from dev to the version you just published. For self-hosted or multi-instance setups, npx retool-ccl sync distributes the library across instances.

Configure the options

Drag the RadioGroup onto the canvas from the Custom Components section. Set options to a static array or bind it to a query. This is the shipping selector from the opening, with two carriers shown:

JavaScript
{{
  [
    { "type": "header", "title": "USPS" },
    {
      "id": "usps_standard",
      "title": "USPS Priority Mail",
      "description": "2-3 business days",
      "showIf": "{{ orderDetails.data.country === 'US' }}"
    },
    {
      "id": "usps_express",
      "title": "USPS Express",
      "description": "1-2 business days",
      "badge": "Recommended",
      "badgeColor": "#10b981",
      "showIf": "{{ orderDetails.data.total > 100 && orderDetails.data.country === 'US' }}"
    },
    { "type": "header", "title": "FedEx" },
    {
      "id": "fedex_ground",
      "title": "FedEx Ground",
      "description": "3-5 business days"
    },
    {
      "id": "fedex_overnight",
      "title": "FedEx Overnight",
      "description": "Next business day",
      "badge": "Fastest",
      "badgeColor": "#3b82f6",
      "showIf": "{{ orderDetails.data.total > 100 }}"
    }
  ]
}}

Each option needs an id and a title. Everything else is optional: description, disabled, renderAsHtml, icon, iconPosition, badge, badgeColor, and showIf. A header is any entry with "type": "header" and a title.

The showIf value is an expression string, "true" by default. It can reference any component or query in the app, such as {{ switch1.value }} or {{ dropdown1.value === 'premium' }}.

Then set the rest in the inspector:

  • defaultValue for single selection, or defaultValues for multiple.

  • layout and buttonPosition.

  • Colors, font sizes, and lineClamp for long descriptions.

Build the options from a query

Hardcoded options go stale the day a carrier changes a service. In production, keep the services in a table and build the array in a query transformer. This query returns one row per service, ordered by carrier:

SQL
select id, carrier, title, description, is_recommended
from shipping_services
where active = true
order by carrier, sort_order;

The transformer turns those rows into options, adding a header each time the carrier changes:

JavaScript
// Transformer on the getShippingServices query
const rows = formatDataAsArray(data);
const options = [];
let carrier = null;

for (const row of rows) {
  if (row.carrier !== carrier) {
    carrier = row.carrier;
    options.push({ type: 'header', title: carrier });
  }
  options.push({
    id: row.id,
    title: row.title,
    description: row.description,
    badge: row.is_recommended ? 'Recommended' : undefined,
  });
}

return options;

Set options to {{ getShippingServices.data }}. The operations team can now add or retire a service in the table, with no change to the app.

Read the selection

The component exposes the selection as state, and fires selectionChange whenever it changes. Attach a handler to that event to recalculate a price or run a query.

JavaScript
// Single select
const selectedShipping = radioGroup1.selectedValue;
// Returns: "fedex_overnight"

// Multiple select (feature selection, notification preferences)
const selectedNotifications = radioGroup1.selectedValues;
// Returns: ["email", "sms", "push"]

In a binding, the same values read as {{ radioGroup1.selectedValue }} and {{ radioGroup1.selectedValues }}.

Before production

  • Pin the app to a published version of the library, never the dev version.

  • Check that every showIf expression references a component or query that exists in the app.

  • Test keyboard navigation with Tab, Enter, and Space.

  • Keep option lists under 100 items, the size the component is tuned for.

What is next

On the roadmap, not committed: built-in search for lists of 20 or more options, and validation rules for required selections and minimum or maximum counts.

Our view

We recommend this component when one choice depends on order data and needs grouping or badges, because one options array is easier to review. Visibility rules spread across several components are harder to check. For a plain list of options, keep Retool's built-in RadioGroup, because it already inherits the app's theme and form behavior.

Issues and pull requests are welcome. The wider collection is in awesome-retool-components, and 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?