primitives
Glitch Radio Group
A cyberpunk-styled radio button group where each option is rendered as an asymmetric clip-path polygon button with a neon shadow offset layer. On hover and when checked, a scan-line glitch overlay appears — animating a shimmy effect that cycles through seven clip-path slices with rapid horizontal translate jitter, evoking a CRT screen artifact. The entire interaction is CSS-only via adjacent-sibling selectors (input:checked + .btn, input:hover + .btn); no JS or pointer tracking needed. Each option uses a visually-hidden native <input type="radio"> stretched over the wrapper for full a11y, keyboard operability, and form semantics, with a visible focus-visible ring on keyboard focus. Token-mapped: --primary drives the fill, a color-mixed variant of --primary drives the shadow offset, and --primary-foreground provides the label color.
Ported from andrew-demchenk0 (MIT)
Install
1. Register the namespace (once per project):
// components.json — register the @bottega namespace once
{
"registries": {
"@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
}
}2. Add the component:
npx shadcn add @bottega/glitch-radio-groupUsage
<GlitchRadioGroup value="" onChange={undefined} options={[]} name="" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value* | string | — | Controlled selected value. |
| onChange* | (value: string) => void | — | Called when the user selects an option. |
| options* | GlitchRadioOption[] | — | The set of options to render. |
| name* | string | — | Shared `name` attribute for the radio group (important for form semantics). |
| className | string | Additional class names merged onto the group root. |
Source
import type React from "react";
import styles from "./glitch-radio-group.module.css";
export type GlitchRadioOption = {
/** The value submitted with the form. */
value: string;
/** Human-readable label rendered inside the button face. */
label: string;
/** Short tag rendered in the top-right corner badge (e.g. "r1"). Defaults to option index. */
tag?: string;
};
export type GlitchRadioGroupProps = {
/** Controlled selected value. */
value: string;
/** Called when the user selects an option. */
onChange: (value: string) => void;
/** The set of options to render. */
options: GlitchRadioOption[];
/** Shared `name` attribute for the radio group (important for form semantics). */
name: string;
/** Additional class names merged onto the group root. */
className?: string;
};
/**
* Glitch Radio Group — server component (no pointer JS needed; CSS-only hover/checked glitch).
*
* Each option is a visually-hidden native <input type="radio"> positioned absolutely over a
* custom clip-path button face. The checked and hover states are driven by adjacent-sibling
* CSS selectors (input:checked + .btn, input:hover + .btn). The glitch overlay animates
* a shimmy+clip-path scan-line effect on hover and on the active selection.
*/
export function GlitchRadioGroup({
value,
onChange,
options,
name,
className = "",
}: GlitchRadioGroupProps) {
return (
<div
className={[styles.group, className].filter(Boolean).join(" ")}
role="radiogroup"
>
{options.map((opt, i) => {
const id = `${name}-${opt.value}`;
const tag = opt.tag ?? `r${i + 1}`;
return (
<div key={opt.value} className={styles.radioWrapper}>
{/* Visually-hidden native radio — carries all a11y + form state */}
<input
type="radio"
className={styles.input}
id={id}
name={name}
value={opt.value}
checked={value === opt.value}
onChange={() => onChange(opt.value)}
aria-label={opt.label}
/>
{/* Custom drawn button face — purely decorative, wired via htmlFor */}
<label htmlFor={id} className={styles.btn} aria-hidden="true">
{opt.label}
{/* Glitch shimmy layer — aria-hidden, decorative scan-line animation */}
<span className={styles.btnGlitch} aria-hidden="true">
{opt.label}
</span>
{/* Short numeric/textual badge in the corner */}
<span className={styles.number}>{tag}</span>
</label>
</div>
);
})}
</div>
);
}
Dependencies
- @bottega/tokens