Radio Group
Categorystyled
ArchitectureReact
Client
Component
Dependencies
Usage
Simple Demo
Custom Demo
Install Instructions
Copy and paste the code into that file
import {
type ComponentPropsWithoutRef,
type ElementType,
type MutableRefObject,
type ReactNode,
createContext,
createElement,
useCallback,
useContext,
useRef,
} from "react";
//----- Third-Party Utils -----//
import { twMerge } from "tailwind-merge";
import { cva, type VariantProps } from "class-variance-authority";
//----- RadioGroup Context -----//
interface RadioGroupContextInterface {
selected: string | undefined;
defaultSelected: string | undefined;
onSelected: undefined | ((v: string) => void);
onButtonClick: (
inputRef: MutableRefObject<HTMLInputElement | null>,
radioElRef: MutableRefObject<unknown | null>
) => void;
name: string;
disabled: undefined | boolean;
}
const RadioGroupContext = createContext<RadioGroupContextInterface>({
selected: undefined,
defaultSelected: undefined,
onSelected: () => {},
onButtonClick: () => {},
name: "",
disabled: false,
});
//----- RadioGroup -----//
interface RadioGroupProps
extends Omit<ComponentPropsWithoutRef<"fieldset">, "id"> {
id: string;
label: string;
defaultSelected?: string;
selected?: string;
onSelected?: (v: string) => void;
}
export const RadioGroup = ({
id,
label,
children,
defaultSelected,
selected,
onSelected,
disabled = false,
...props
}: RadioGroupProps) => {
const onButtonClick = useCallback<
RadioGroupContextInterface["onButtonClick"]
>(
(inputRef, radioElRef) => {
if (disabled) return;
const { current: input } = inputRef;
const { current: radioEl } = radioElRef;
if (input === null || radioEl === null) return;
input.click();
// Storing the selected item
const targetId = input.id;
// Manual Updates incase the there props doesn't include `selected`
if (typeof selected === "undefined") {
const buttons = document.querySelectorAll(`[data-fieldset-id=${id}]`);
buttons.forEach((button) => {
const isSelected =
button.getAttribute("data-button-for") === targetId;
button.setAttribute("data-selected", `${isSelected}`);
});
}
// Storing the selected item in outer state
onSelected && onSelected(targetId);
},
[selected, onSelected, id, disabled]
);
return (
<fieldset id={id} {...props}>
<span>
<legend>{label}</legend>
</span>
<RadioGroupContext.Provider
value={{
selected,
onSelected,
onButtonClick,
name: id,
defaultSelected,
disabled,
}}
>
{children}
</RadioGroupContext.Provider>
</fieldset>
);
};
//----- RadioItem -----//
interface BaseRadioItemProps<T extends ElementType> {
label: string;
id: string;
displayLabel?: boolean;
children?: ReactNode;
component?: T;
wrapperClassName?: string;
}
const radioButtonClassNames = cva(["not-prose", "relative"], {
variants: {
styled: {
false: ["[&_*]:pointer-events-none"],
true: [
"shadow-neutral-100",
"border-transparent",
"dark:bg-neutral-600",
"bg-neutral-400",
"dark:data-[selected=true]:bg-primary-400",
"data-[selected=true]:bg-primary-500",
"rounded-full",
"w-4",
"h-4",
"transition-all",
"[box-shadow:_0_0_0_2px_transparent]",
"hover:data-[selected=false]:[box-shadow:_0_0_0_2px_theme(colors.primary.400/75%)]",
],
},
},
defaultVariants: {
styled: true,
},
});
const radioItemClassNames = cva(
["flex", "gap-2", "justify-start", "items-center", "content-center"],
{
variants: {
stacking: {
vertical: ["flex-col"],
horizontal: [],
},
},
defaultVariants: {
stacking: "horizontal",
},
}
);
type RadioItemProps<T extends ElementType> = BaseRadioItemProps<T> &
Omit<ComponentPropsWithoutRef<T>, keyof BaseRadioItemProps<T>> &
VariantProps<typeof radioItemClassNames> &
VariantProps<typeof radioButtonClassNames>;
export const RadioItem = <T extends ElementType = "button">({
label,
displayLabel = false,
children,
id,
component,
wrapperClassName = "",
className,
stacking,
styled,
...props
}: RadioItemProps<T>) => {
const inputRef = useRef<HTMLInputElement | null>(null);
const radioElRef = useRef<T | null>(null);
const { onButtonClick, selected, name, defaultSelected, disabled } =
useContext(RadioGroupContext);
return (
<div
className={twMerge(radioItemClassNames({ stacking }), wrapperClassName)}
>
{createElement(
component ? component : "button",
{
...props,
className: twMerge(
radioButtonClassNames({ styled }),
className ? className : ""
),
ref: radioElRef,
onClick: onButtonClick.bind(null, inputRef, radioElRef),
"data-selected": id === selected || id === defaultSelected,
"data-button-for": id,
"data-fieldset-id": name,
"data-disabled": disabled,
},
children
)}
<input
ref={inputRef}
id={id}
name={name}
type="radio"
style={{ display: "none" }}
/>
<label
style={
!displayLabel
? {
fontSize: "0rem",
userSelect: "none",
opacity: 0,
color: "transparent",
pointerEvents: "none",
width: "0px",
maxWidth: "0px",
overflow: "hidden",
position: "absolute",
}
: undefined
}
onClick={(e) => {
e.preventDefault();
onButtonClick(inputRef, radioElRef);
}}
htmlFor={id}
className="text-sm opacity-75 hover:cursor-pointer hover:opacity-100 transition-opacity select-none"
>
{label}
</label>
</div>
);
};
//----- Added Exports -----//
RadioGroup.Item = RadioItem;