Radio Group

A component that allows users to select a single option from a set of mutually exclusive options.

import { RadioGroup, RadioGroupItem } from "~/components/radio-group";

export default function RadioGroupBasicDemo() {
  return (
    <RadioGroup defaultValue="1" orientation="horizontal">
      <RadioGroupItem value="1">Credit Card</RadioGroupItem>
      <RadioGroupItem value="2">Paypal</RadioGroupItem>
      <RadioGroupItem value="3">Debit</RadioGroupItem>
    </RadioGroup>
  );
}

Installation

CLI

Run the following command to add the component to your project:

npx @ark-preset/cli@latest add radio-group

Manual

Create the recipe file at src/components/recipes/radio-group.ts:

import { tv, type VariantProps } from "tailwind-variants";

export const radioGroupVariants = tv({
  slots: {
    root: "flex gap-2",
    item: [
      "inline-flex items-center gap-2 cursor-pointer",
      "data-[disabled]:opacity-80 data-[disabled]:cursor-not-allowed",
    ],
    itemControl: [
      "peer size-5 shrink-0 rounded-full border border-input ring-offset-background transition-colors disabled:cursor-not-allowed disabled:opacity-50",
      "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
      "data-[state=checked]:border-primary data-[state=checked]:border-[6px] data-[state=checked]:text-primary-foreground",
    ],
    itemIndicator: "size-2 rounded-full hidden",
    itemText: "text-sm font-medium text-foreground data-[disabled]:opacity-50",
    itemHiddenInput: "absolute opacity-0 w-0 h-0 pointer-events-none",
  },
  variants: {
    orientation: {
      horizontal: {
        root: "flex-row items-center gap-6",
      },
      vertical: {
        root: "flex-col gap-2",
      },
    },
  },
  defaultVariants: {
    orientation: "vertical",
  },
});

export type RadioGroupVariants = VariantProps<typeof radioGroupVariants>;

Create the component directory and files.

src/components/radio-group/radio-group.base.tsx:

import { RadioGroup as ArkRadioGroup } from "@ark-ui/solid/radio-group";
import { splitProps, type Component } from "solid-js";
import { radioGroupVariants, labelVariants, type RadioGroupVariants } from "../recipes/radio-group";

const styles = radioGroupVariants();

const Root: Component<ArkRadioGroup.RootProps & RadioGroupVariants> = (props) => {
  const [local, others] = splitProps(props, ["class", "orientation"]);
  return (
    <ArkRadioGroup.Root
      class={styles.root({ class: local.class, orientation: local.orientation })}
      orientation={local.orientation}
      {...others}
    />
  );
};

const RootProvider: Component<ArkRadioGroup.RootProviderProps & RadioGroupVariants> = (props) => {
  const [local, others] = splitProps(props, ["class", "orientation"]);
  return (
    <ArkRadioGroup.RootProvider
      class={styles.root({ class: local.class, orientation: local.orientation })}
      {...others}
    />
  );
};

const Label: Component<ArkRadioGroup.LabelProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkRadioGroup.Label class={labelVariants({ class: local.class })} {...others} />;
};

const Item: Component<ArkRadioGroup.ItemProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkRadioGroup.Item class={styles.item({ class: local.class })} {...others} />;
};

const ItemControl: Component<ArkRadioGroup.ItemControlProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkRadioGroup.ItemControl class={styles.itemControl({ class: local.class })} {...others} />
  );
};

const ItemText: Component<ArkRadioGroup.ItemTextProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkRadioGroup.ItemText class={styles.itemText({ class: local.class })} {...others} />;
};

const ItemHiddenInput = ArkRadioGroup.ItemHiddenInput;

const Indicator: Component<ArkRadioGroup.IndicatorProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkRadioGroup.Indicator class={styles.itemIndicator({ class: local.class })} {...others} />
  );
};

export const RadioGroup = {
  Root,
  RootProvider,
  Label,
  Item,
  ItemControl,
  ItemText,
  Indicator,
  ItemHiddenInput,
};

src/components/radio-group/index.tsx:

import { splitProps, type Component } from "solid-js";
import { RadioGroup as RadioGroupBase } from "./radio-group.base";
import { RadioGroup as ArkRadioGroup } from "@ark-ui/solid/radio-group";
import type { RadioGroupVariants } from "../recipes/radio-group";

const RadioGroup: Component<ArkRadioGroup.RootProps & RadioGroupVariants> = (props) => {
  const [local, others] = splitProps(props, ["class", "orientation", "children"]);
  return (
    <RadioGroupBase.Root orientation={local.orientation} {...others}>
      <RadioGroupBase.Indicator />
      {local.children}
    </RadioGroupBase.Root>
  );
};

const RadioGroupItem: Component<ArkRadioGroup.ItemProps> = (props) => {
  const [local, others] = splitProps(props, ["children"]);
  return (
    <RadioGroupBase.Item {...others}>
      <RadioGroupBase.ItemControl />
      <RadioGroupBase.ItemText>{local.children}</RadioGroupBase.ItemText>
      <RadioGroupBase.ItemHiddenInput />
    </RadioGroupBase.Item>
  );
};

export { RadioGroup, RadioGroupItem, RadioGroupBase };

export { radioGroupVariants, type RadioGroupVariants } from "../recipes/radio-group";

Note: Make sure your project has the Tailwind CSS theme variables set up (--background, --foreground, --ring, --border, etc.) or override the utility classes to match your design system.

Usage

Basic Usage

import { RadioGroup, RadioGroupItem } from "~/components/radio-group";

export default function RadioGroupBasicDemo() {
  return (
    <RadioGroup defaultValue="1" orientation="horizontal">
      <RadioGroupItem value="1">Credit Card</RadioGroupItem>
      <RadioGroupItem value="2">Paypal</RadioGroupItem>
      <RadioGroupItem value="3">Debit</RadioGroupItem>
    </RadioGroup>
  );
}

Controlled Value

Use value and onValueChange to control the selection state externally:

Selected: 1

import { Index, createSignal } from "solid-js";
import { RadioGroup, RadioGroupItem } from "~/components/radio-group";
const paymentMethods = [
  { value: "1", label: "Credit Card" },
  { value: "2", label: "Paypal" },
  { value: "3", label: "Debit" },
];
export default function RadioGroupControlledDemo() {
  const [value, setValue] = createSignal("1");
  return (
    <div class="space-y-4">
      <p class="text-sm text-muted-foreground">Selected: {value()}</p>
      <RadioGroup
        value={value()}
        onValueChange={(e: any) => setValue(e.value || "1")}
        orientation="horizontal"
      >
        <Index each={paymentMethods}>
          {(method) => <RadioGroupItem value={method().value}>{method().label}</RadioGroupItem>}
        </Index>
      </RadioGroup>
    </div>
  );
}

Disabled Item

Individual items can be disabled using the disabled prop:

import { Index } from "solid-js";
import { RadioGroup, RadioGroupItem } from "~/components/radio-group";
const paymentMethods = [
  { value: "1", label: "Credit Card" },
  { value: "2", label: "Paypal" },
  { value: "3", label: "Debit", disabled: true },
];
export default function RadioGroupDisabledDemo() {
  return (
    <div>
      <RadioGroup defaultValue="1" orientation="horizontal">
        <Index each={paymentMethods}>
          {(method) => (
            <RadioGroupItem value={method().value} disabled={method().disabled}>
              {method().label}
            </RadioGroupItem>
          )}
        </Index>
      </RadioGroup>
    </div>
  );
}

Advanced Usage

When the composite RadioGroup doesn’t provide enough control, import the raw primitive parts from the base file directly:

import { RadioGroup } from "~/components/radio-group/radio-group.base";

Or import RadioGroupBase (the raw parts namespace) from the composite entry point:

import { RadioGroupBase } from "~/components/radio-group";

Root Provider

Use RadioGroupBase.RootProvider when you need to access the radio group state outside of the component tree. This pattern uses the useRadioGroup hook from Ark UI to create a shared context that both the radio group and external elements can reference.

Value: "1"
Payment Method
import { Index, createMemo } from "solid-js";
import { RadioGroupBase, RadioGroupItem } from "~/components/radio-group";
import { useRadioGroup } from "@ark-ui/solid/radio-group";
const paymentMethods = [
  { value: "1", label: "Credit Card" },
  { value: "2", label: "Paypal" },
  { value: "3", label: "Debit" },
];
export default function RadioGroupRootProviderDemo() {
  const radioGroup = useRadioGroup({ defaultValue: "1" });
  const value = createMemo(() => radioGroup().value);
  return (
    <div class="space-y-4">
      <output class="block text-sm text-muted-foreground">Value: {JSON.stringify(value())}</output>
      <RadioGroupBase.RootProvider value={radioGroup} orientation="horizontal">
        <RadioGroupBase.Label>Payment Method</RadioGroupBase.Label>
        <Index each={paymentMethods}>
          {(method) => <RadioGroupItem value={method().value}>{method().label} </RadioGroupItem>}
        </Index>
      </RadioGroupBase.RootProvider>
    </div>
  );
}

The key difference:

  • RadioGroup (Root) — manages its own state internally. Use for simple, self-contained radio groups.
  • RadioGroupBase.RootProvider — accepts a pre-created radio group context via useRadioGroup. Use when you need to read or control the radio group state from outside the component tree.

API Reference

See the Ark UI Radio Group documentation.