---
title: "A custom RadioGroup component for Retool"
description: "Selection controls with the layouts and states the native RadioGroup does not offer."
canonical: "https://stackdrop.co/blog/custom-radiogroup-retool-component"
published: "2026-05-19T09:00:00.000Z"
updated: "2026-05-19T09:00:00.000Z"
authors: ["Vaggelis Kapetanakis"]
topics: ["Retool"]
readers: ["builders"]
---

# A custom RadioGroup component for Retool

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](https://github.com/StackdropCO/custom-radio-group-retool-component) 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.

```bash
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.

```bash
npm run dev
```

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

```bash
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](https://github.com/StackdropCO/awesome-retool-components), and [what is a custom Retool component](https://stackdrop.co/blog/what-is-a-custom-retool-component) covers when building one is worth it.
