Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Inputs

Radio Group

Inputs

Radio Group React component for Tailwind CSS v4

Phone InputRating
Live props
Field
label
description
error
required
invalid
disabled
readOnly
Appearance
variant
size
tone
color
indicator
radius
Layout
orientation
columns1
controlPosition
showControl
State
defaultValue
Behavior
loop
name
Code
import { RadioGroup } from "@/components/ui/radio-group"

<RadioGroup />
  1. Home
  2. Components
  3. Inputs
  4. Radio Group
Free

Radio Group React component for Tailwind CSS v4

v1.0.1·Updated in 1.9.0

A React radio group to pick one of a few visible options: rows with a dot that springs in, or cards with icons, prices and a sliding ring.

Click or tap anywhere on a row or card to choose it: the circle fills with the tone and the dot springs in, and on cards a ring slides from the old choice to the new one while the icon tile takes the tone. The circle dips under your finger while you press, and a card dips slightly as a whole. Tab lands on the chosen option (or the first one when nothing is chosen), the arrow keys move and choose at once (Up and Left go back, Down and Right go forward, mirrored in right-to-left layouts, wrapping at the ends unless loop is off), Space chooses the focused option, and Tab leaves the group. Clicking the group's label focuses the chosen option. On a phone every row grows to 44px so each one is a full finger target, grids drop columns once a cell would get too narrow (a phone always gets one), cards stack their icon above the text when they are narrow, and horizontal rows wrap instead of scrolling. A read-only group keeps its choice visible and focusable but ignores changes; a disabled option dims and is skipped by the arrow keys. With reduced motion the dot and the ring jump instead of springing.

Radio Group is a free React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add radio-group, together with Field, useControllableState, Motion presets, Tones and UIProvider and the npm packages @radix-ui/react-radio-group and motion.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add radio-group

Usage

Import RadioGroup from @/components/ui/radio-group.

import { Package, Store, Truck } from "lucide-react"
import { RadioGroup, type RadioOption } from "@/components/ui/radio-group"

const delivery: RadioOption[] = [
  { value: "courier", label: "Home delivery", description: "FedEx, 1-2 days", icon: <Truck />, badge: "$9.99" },
  { value: "locker", label: "Locker", description: "Pick up any time from 3,000 lockers", icon: <Package />, badge: "$5.99" },
  { value: "pickup", label: "In-store pickup", description: "Austin, 2400 S Congress Ave", icon: <Store />, badge: "Free" },
]

export function DeliveryMethod({ value, onChange }: { value: string; onChange: (value: string) => void }) {
  return (
    <RadioGroup
      name="delivery"
      label="Delivery method"
      variant="card"
      options={delivery}
      value={value}
      onValueChange={onChange}
      required
    />
  )
}

// custom content: compose the parts
import { RadioGroupRoot, RadioGroupItem } from "@/components/ui/radio-group"

export function PlanPicker() {
  return (
    <RadioGroupRoot label="Plan" variant="card" columns={2} defaultValue="pro">
      <RadioGroupItem value="free" label="Free" badge="$0" />
      <RadioGroupItem value="pro" label="Pro" badge="$9/mo">
        <ul className="mt-2 text-[13px] text-muted-foreground">
          <li>Unlimited listings</li>
        </ul>
      </RadioGroupItem>
    </RadioGroupRoot>
  )
}

Props

  • optionsdefault "[]"
    RadioOption[]

    The choices: { value, label, description?, icon?, badge?, disabled? }. badge is a trailing price or tag on the label's line. For layouts an array cannot express, compose RadioGroupRoot with RadioGroupItem children (see Parts).

  • value
    string

    The chosen value when controlled. Pair it with onValueChange.

  • onValueChange
    (value: string) => void

    Called with the new value when the person picks an option, by pointer, Space or the arrow keys. Not called while readOnly.

  • labeldefault ""
    ReactNode

    The group's label, above the options. It names the radiogroup through aria-labelledby, and clicking it focuses the chosen option. Leave it empty and pass aria-label for a bare group.

  • descriptiondefault ""
    ReactNode

    Helper text under the options, linked with aria-describedby. The error takes its place while it shows.

  • errordefault ""
    ReactNode

    Replaces the description with an alert icon and the text, slides in and marks the group invalid: empty circles and card borders turn red.

  • requireddefault false
    boolean

    The red mark after the label, aria-required on the group, and required on the hidden form inputs, so a native form will not submit without a choice.

  • invaliddefault false
    boolean

    The invalid look and aria-invalid without a message, for forms that show errors elsewhere.

  • disableddefault false
    boolean

    Dims every option and blocks it; a form will not submit its value. Disable one option through its own disabled.

  • readOnlydefault false
    boolean

    The choice stays visible and focusable, but clicks and arrow keys do not change it (aria-readonly). No hover tint, no press dip.

  • variantdefault "default"
    "default" | "card"

    default: compact rows with the circle beside the text. card: soft-filled tiles with an icon tile, a description and a trailing price, and one selection ring that slides between cards.

  • sizedefault "md"
    "sm" | "md" | "lg"

    Circle 16 / 18 / 20px with a 6 / 7 / 8px dot, label 13 / 14 / 15px, card padding 12 / 16 / 20px.

  • tonedefault "neutral"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    The checked fill, the dot's contrast color and the card ring. neutral is black in light mode and white in dark mode; brand orange is opt-in.

  • colordefault ""
    string

    Any CSS color. Replaces the tone; the dot turns black or white by lightness. Leave empty to use the tone.

  • indicatordefault "dot"
    "dot" | "check"

    What appears in the filled circle: a dot that springs in, or a check mark that draws itself (nice on cards).

  • radiusdefault "md"
    "none" | "sm" | "md" | "lg" | "full"

    Cards only: corners of 0 / 12 / 16 / 24px (full falls back to 24px). The ring follows it.

  • orientationdefault "vertical"
    "vertical" | "horizontal"

    horizontal puts rows side by side and wraps them onto the next line instead of scrolling; cards share the row until they would get narrower than 10rem. Every arrow key works in both, like native radios.

  • columnsdefault 1
    1 | 2 | 3 | 4

    Up to this many columns of rows or cards. The grid follows the space it has, not the viewport: it drops a column whenever cells would get narrower than 12rem (cards) or 11rem (rows), so a phone always gets one column and a narrow sidebar never squeezes the text.

  • controlPositiondefault "auto"
    "auto" | "start" | "end"

    Where the circle sits: auto is before the text for rows and at the top end of cards. end suits settings lists; start suits cards that read like a checklist.

  • showControldefault true
    boolean

    Cards only: false hides the circle and lets the ring and the tinted icon tile show the choice (plan pickers). The radio stays in the accessibility tree.

  • defaultValuedefault ""
    string

    The option chosen on first render while uncontrolled. "" chooses nothing, and the first enabled option is then the tab stop. A value that matches no option also shows nothing chosen. The demo starts on curier while this is empty; try easybox or ridicare.

  • loopdefault true
    boolean

    The arrow keys wrap from the last option to the first and back. The demo's key hint says which.

  • namedefault ""
    string

    Form field name. Inside a <form> each option renders a hidden native radio with this name, so FormData and native validation work; a form reset returns to defaultValue. The demo prints what the form would submit.

  • id
    string

    The group's id (the radiogroup element). Defaults to useId(); the label and messages derive their ids from it.

  • fieldProps
    Partial<FieldProps>

    Anything else for the Field wrapper: optional, labelAction, orientation, labelWidth, hideLabel.

  • classNames
    Partial<Record<"group" | "option" | "body" | "item" | "indicator" | "icon" | "content" | "label" | "description" | "badge" | "ring" | "field" | "fieldLabel" | "fieldDescription" | "error", string>>

    Extra classes for the parts. label and description style each option's text; fieldLabel and fieldDescription the group's own label and hint; group is the radiogroup element, option each row or card, body the inner row of a card (the part that stacks when the card is narrow), item the circle.

  • className
    string

    Classes for the outermost element: the Field wrapper when there is a label, description or error, otherwise the radiogroup. Override the tone vars here, e.g. [--tone:#7C3AED].

  • ref
    RefObject<HTMLDivElement | null> or (node) => void

    The radiogroup element. Every other prop (aria-label, dir, form, data-*) lands there too.

Parts

  • RadioGroupRoot

    component

    The group without the options array, for layouts an array cannot express: plan cards with feature lists, mixed rows. Takes every RadioGroup prop except options, plus RadioGroupItem children.

  • RadioGroupItem

    component

    One option inside RadioGroupRoot: a <label> row or card whose whole surface picks it. Children render under the description; other props and ref go on the label.

    • value*
      string

      The value the group reports when this option is picked.

    • label
      ReactNode

      The option's name.

    • description
      ReactNode

      A muted line under the label.

    • icon
      ReactNode

      A leading icon: muted beside a row, in a tinted tile on a card.

    • badge
      ReactNode

      Trailing text on the label's line, like a price or a tag.

    • disableddefault false
      boolean

      Dims this option and skips it with the arrow keys.

    • classNames
      Partial<Record<RadioItemSlot, string>>

      Extra classes for the option's inner parts.

    • children
      ReactNode

      Extra content under the description, such as plan features.

  • RadioOption

    type

    One entry of options: { value, label, description?, icon?, badge?, disabled? }.

Examples

Payment method

Rows with icons, a description and a trailing surcharge. The whole row is the target.

Horizontal

Short choices side by side; they wrap onto the next line on a phone instead of scrolling.

Plan cards

RadioGroupRoot with custom items in 3 columns, showControl={false} and the brand tone: the ring alone marks the plan.

Sizes, tones and a custom color

16, 18 and 20px circles; any tone or CSS color, with a dot or a check that draws itself.

Disabled and read-only

A disabled option dims and the arrow keys skip it; a read-only group keeps its choice and stays focusable.

Required, checked on submit

Submit with nothing chosen: the error slides in and focus moves to the group. Picking a card clears it.

Settings list

controlPosition="end" and indicator="check" inside a list group, like a phone settings screen.

Delivery slots in two columns

Small cards in a 2-column grid that falls to one column on phones; a full slot is disabled.

Dependencies

@radix-ui/react-radio-groupmotionfielduse-controllable-statemotiontonesui-config

Files

  • components/ui/radio-group.tsx

FAQ

Is the React Radio Group component free?

Yes. The source is free and needs no account: copy it from the Code tab or install it with the CLI.

How do I install Radio Group?

Run npx wingo-ui@latest add radio-group. It also adds the Wingo UI items it builds on (Field, useControllableState, Motion presets, Tones and UIProvider) and the npm packages @radix-ui/react-radio-group and motion. With shadcn, run npx shadcn@latest add https://wingo-ui.com/r/radio-group.json.

How does Radio Group behave on phones?

On a phone every row grows to 44px so each one is a full finger target, grids drop columns once a cell would get too narrow (a phone always gets one), cards stack their icon above the text when they are narrow, and horizontal rows wrap instead of scrolling.

See also

CheckboxSegmented ControlToggle GroupSwitchField

Used in

Checkout stepsDeal PipelineFilter panelBooking WidgetAutomation Builder PageListing Detail PagePost Listing WizardAccessibility PageContact PageHome Services PagePortfolio Page

More React input components

RatingSearch InputSelectSignature PadTag InputTextarea
All 40 React input components

Articles using this component

  • React Segmented Control: iOS Thumb, Drag and When to Use It

    Mobile UI in React · Oct 9, 2026 · 8 min read

PreviousPhone InputNext Rating

Checkout

Shipping within the US

Step 2 of 3
Delivery method
Items (2)
$89.00
Shipping
$9.99
Total
$98.99
↑↓Up arrow Down arrowchoose, wraps aroundform sends delivery=courier