000h-cojeev
uiradio-groupsource: ok · hit
Radio group
Lets users select one option from a labeled set of mutually exclusive choices.
npx shadcn add "https://000h.cojeev.com/r/radio-group.json"free
Interaction
dropdown
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumui-primitives@radix-ui/react-radio-group
animationmotion
Registry dependencies
https://cojeev.com/ui/r/cojeev.json
Files5 · source cached 10/13/2026, 5:02:21 PM
registry/cojeev/ui/radio-group.tsx
"use client";
import * as React from "react";
import { controlRadiusStyle, type ControlRadius } from "@/lib/cojeev/control-appearance";
import {
SelectorGlyph,
selectorStyle,
type SelectorShape,
type SelectorTone,
type SelectorSize,
type SelectorIndicator,
} from "@/lib/cojeev/selector";
export type {
SelectorShape,
SelectorTone,
SelectorSize,
SelectorIndicator,
} from "@/lib/cojeev/selector";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import * as Primitive from "@radix-ui/react-radio-group";
import { useFlowGroup } from "@/lib/cojeev-motion/use-flow";
export const radioGroupVariants = cva("v-radios grid gap-2", {
variants: { pictographic: { true: "v-iradios", false: "" } },
defaultVariants: { pictographic: false },
});
const RadioStyleContext = React.createContext<{
appearance?: "row" | "card" | "chip";
pictographic: boolean;
shape: SelectorShape;
tone: SelectorTone;
size?: SelectorSize;
indicator?: SelectorIndicator;
showIndicator?: boolean;
value?: string;
disabled?: boolean;
cancelKeyboardNavigation: () => void;
}>({ pictographic: false, shape: "organic", tone: "pink", cancelKeyboardNavigation: () => {} });
export type RadioGroupProps = React.ComponentProps<typeof Primitive.Root> &
VariantProps<typeof radioGroupVariants> & {
shape?: SelectorShape;
tone?: SelectorTone;
size?: SelectorSize;
indicator?: SelectorIndicator;
showIndicator?: boolean;
appearance?: "row" | "card" | "chip";
radius?: ControlRadius;
};
export function RadioGroup({
className,
pictographic = false,
ref,
children,
shape = "organic",
tone = "pink",
size,
indicator = "auto",
showIndicator = true,
appearance,
radius,
style,
value: controlled,
defaultValue,
onValueChange,
onKeyDownCapture,
onPointerDownCapture,
onClickCapture,
onFocus,
...props
}: RadioGroupProps) {
const [local, setLocal] = React.useState(defaultValue);
const value = controlled ?? local;
const keyboardNavigation = React.useRef<{ origin: HTMLElement; activated: boolean } | null>(null);
const navigationExpiry = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const cancelKeyboardNavigation = React.useCallback(() => {
clearTimeout(navigationExpiry.current);
keyboardNavigation.current = null;
}, []);
React.useEffect(() => cancelKeyboardNavigation, [cancelKeyboardNavigation]);
const ownedRadio = (target: EventTarget | null, group: HTMLElement) => {
if (!(target instanceof HTMLElement) || target.getAttribute("role") !== "radio") return null;
return target.closest('[data-slot="radio-group"]') === group ? target : null;
};
const flowRef = useFlowGroup<HTMLDivElement>(ref, {
itemSelector: '[data-slot="radio-group-item"]',
activeSelector: '[data-state="checked"]',
});
return (
<RadioStyleContext.Provider
value={{
pictographic: !!pictographic && !appearance,
appearance,
shape,
tone,
size,
indicator,
showIndicator,
value,
disabled: props.disabled,
cancelKeyboardNavigation,
}}
>
<Primitive.Root
ref={flowRef}
onKeyDownCapture={event => {
cancelKeyboardNavigation();
onKeyDownCapture?.(event);
if (event.defaultPrevented || props.disabled || event.altKey || event.ctrlKey || event.metaKey) return;
const origin = ownedRadio(event.target, event.currentTarget);
const key = event.key;
const navigationKey = ["Home", "End"].includes(key)
|| (props.orientation !== "horizontal" && ["ArrowUp", "ArrowDown"].includes(key))
|| (props.orientation !== "vertical" && ["ArrowLeft", "ArrowRight"].includes(key));
if (!origin || !navigationKey) return;
keyboardNavigation.current = { origin, activated: false };
// Radix defers roving focus to a task. Retain keyboard intent through
// that task even if a fast keyup has already cleared its document flag.
navigationExpiry.current = setTimeout(() => {
navigationExpiry.current = setTimeout(cancelKeyboardNavigation, 0);
}, 0);
}}
onPointerDownCapture={event => { cancelKeyboardNavigation(); onPointerDownCapture?.(event); }}
onClickCapture={event => {
const navigation = keyboardNavigation.current;
if (navigation && ownedRadio(event.target, event.currentTarget) !== navigation.origin) navigation.activated = true;
onClickCapture?.(event);
}}
onFocus={event => {
onFocus?.(event);
const navigation = keyboardNavigation.current;
const target = ownedRadio(event.target, event.currentTarget);
if (!navigation || !target || target === navigation.origin) return;
// A held arrow already clicks inside Radix's Item onFocus. The scoped
// click marker prevents a second callback, including rejected controlled changes.
if (!navigation.activated && !event.defaultPrevented && !props.disabled
&& target.getAttribute("aria-checked") !== "true" && !target.matches(":disabled,[data-disabled]")) {
navigation.activated = true;
target.click();
}
cancelKeyboardNavigation();
}}
data-slot="radio-group"
data-part="root"
data-appearance={appearance}
style={{ ...controlRadiusStyle(radius), ...style }}
value={value}
onValueChange={(next) => {
if (controlled === undefined) setLocal(next);
onValueChange?.(next);
}}
data-flow-group={pictographic && !appearance ? "" : undefined}
data-flow={pictographic && !appearance ? undefined : "off"}
className={cn(radioGroupVariants({ pictographic: !!pictographic && !appearance }), className)}
{...props}
>
{children}
</Primitive.Root>
</RadioStyleContext.Provider>
);
}
export type RadioGroupItemProps = React.ComponentProps<
typeof Primitive.Item
> & { appearance?: "row" | "card" | "chip"; radius?: ControlRadius; pictographic?: boolean; shape?: SelectorShape; tone?: SelectorTone; size?: SelectorSize; indicator?: SelectorIndicator; showIndicator?: boolean };
export function RadioGroupItem({
className,
pictographic,
shape,
tone,
size,
indicator,
showIndicator,
style,
appearance,
radius,
children,
ref,
onKeyDown,
...props
}: RadioGroupItemProps) {
const inherited = React.useContext(RadioStyleContext);
const icon = !(appearance ?? inherited.appearance) && (pictographic ?? inherited.pictographic);
const selectedShape = shape ?? inherited.shape;
const selectedTone = tone ?? inherited.tone;
return (
<Primitive.Item
ref={ref}
onKeyDown={event => {
onKeyDown?.(event);
if (event.defaultPrevented) inherited.cancelKeyboardNavigation();
}}
data-slot="radio-group-item"
data-part="item"
data-appearance={appearance ?? inherited.appearance}
data-selector-shape={selectedShape}
style={selectorStyle(selectedTone, { ...controlRadiusStyle(radius), ...style }, size ?? inherited.size ?? (icon ? 16 : "default"))}
data-pictographic={icon || undefined}
className={cn(
icon ? "v-iradio" : "v-radio inline-flex items-center gap-[var(--s-3)]",
className,
)}
{...props}
>
<span data-slot="radio-group-indicator" data-part="indicator">
<SelectorGlyph
shape={selectedShape}
tone={selectedTone}
state={inherited.value === props.value}
indicator={indicator ?? inherited.indicator}
showIndicator={showIndicator ?? inherited.showIndicator}
disabled={props.disabled || inherited.disabled}
/>
</span>
{children}
</Primitive.Item>
);
}
export type RadioGroupBodyProps = React.ComponentProps<"span">;
export function RadioGroupBody({ className, ...props }: RadioGroupBodyProps) {
return (
<span
data-slot="radio-group-body"
className={cn("v-radio__body", className)}
{...props}
/>
);
}
registry/cojeev/lib/selector.tsx
"use client";
import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import { shapeContour, signatureShapePaths } from "@/lib/cojeev/signature-shapes";
import { useChoreography } from "@/lib/cojeev-motion/choreography";
import { useMorph } from "@/lib/cojeev-motion/use-morph";
export type SelectorShape =
"organic" | "rounded" | "circle" | "pebble" | "leaf" | "flower";
export type SelectorTone = "pink" | "blue" | "olive" | "yellow";
/** Numeric sizes are clamped to 16–64px; the containing control keeps its hit area. */
export type SelectorSize = "sm" | "default" | "lg" | number;
export type SelectorIndicator = "auto" | "dot" | "check" | "diamond" | "flower";
export function selectorSize(size: SelectorSize = "default") {
return typeof size === "number"
? Math.min(64, Math.max(16, Number.isFinite(size) ? size : 28))
: ({ sm: 20, default: 28, lg: 36 }[size] ?? 28);
}
export const selectorShapes: SelectorShape[] = [
"organic",
"pebble",
"rounded",
"circle",
"leaf",
"flower",
];
export const selectorTones: SelectorTone[] = [
"pink",
"blue",
"olive",
"yellow",
];
const contour = (radius: (angle: number) => number) =>
shapeContour((angle) => [
50 + radius(angle) * Math.cos(angle),
50 + radius(angle) * Math.sin(angle),
]);
const paths: Record<SelectorShape, string> = {
organic: contour((t) => 41 + 1.1 * Math.cos(3 * t + 0.6)),
rounded: contour(
(t) =>
40 /
Math.pow(Math.abs(Math.cos(t)) ** 5 + Math.abs(Math.sin(t)) ** 5, 1 / 5),
),
circle: contour(() => 41),
pebble: signatureShapePaths["pebble-soft"],
leaf: shapeContour((t) => {
const x = 38 * Math.cos(t),
y = 30 * Math.sin(t) * (1 + 0.22 * Math.cos(t));
return [50 + (x - y) * 0.707, 50 + (x + y) * 0.707];
}),
flower: contour((t) => 37 + 5 * Math.cos(5 * t - Math.PI / 2)),
};
export function selectorStyle(
tone: SelectorTone,
style?: React.CSSProperties,
size: SelectorSize = "default",
): React.CSSProperties {
return {
"--selector-accent": `var(--v-${tone})`,
"--selector-size": `${selectorSize(size)}px`,
...style,
} as React.CSSProperties;
}
/** A single silhouette/mark renderer for native and Radix selection semantics. */
export function SelectorGlyph({
shape = "organic",
tone = "pink",
state,
kind = "radio",
disabled = false,
indicator = "auto",
showIndicator = true,
}: {
shape?: SelectorShape;
tone?: SelectorTone;
state: boolean | "indeterminate";
kind?: "checkbox" | "radio";
disabled?: boolean;
indicator?: SelectorIndicator;
showIndicator?: boolean;
}) {
const { quiet, transition } = useChoreography();
const surface = React.useRef<HTMLSpanElement>(null);
const morphRef = useMorph<HTMLSpanElement>("controls", surface);
React.useEffect(() => {
const node = surface.current;
const owner = node?.closest('[role="checkbox"], [role="radio"], label');
if (!node || !owner) return;
const controller = new AbortController();
for (const type of ["keydown", "keyup"] as const)
owner.addEventListener(
type,
(event) => {
const key = (event as KeyboardEvent).key;
if (key === " " || key === "Enter")
node.dispatchEvent(new KeyboardEvent(type, { key }));
},
{ signal: controller.signal },
);
for (const type of ["focusin", "focusout"] as const)
owner.addEventListener(
type,
() => node.dispatchEvent(new FocusEvent(type)),
{ signal: controller.signal },
);
return () => controller.abort();
}, []);
const active = state === true || state === "indeterminate";
const mark = indicator === "auto" ? (kind === "checkbox" ? "check" : "dot") : indicator;
const strokeWidth = active && !showIndicator ? 2.5 : active ? 1.25 : 1.5;
const fill = disabled
? "var(--v-disabled-face)"
: active
? `var(--v-${tone})`
: "var(--v-canvas)";
const ink = disabled ? "var(--v-disabled-ink)" : "var(--v-on-accent)";
const stroke = disabled
? "var(--v-disabled-edge)"
: active
? ink
: "var(--v-text-2)";
const mappedShape = {
rounded: "squircle",
circle: "circle",
pebble: "pebble",
leaf: "leaf",
flower: "flower-5",
};
return (
<span
data-slot="selector-glyph"
data-selector-shape={shape}
data-selector-tone={tone}
data-selector-indicator={showIndicator ? mark : "none"}
data-state={state === "indeterminate" ? "indeterminate" : active ? "checked" : "unchecked"}
data-motion-quiet={quiet || undefined}
aria-hidden="true"
style={{
display: "inline-grid",
position: "relative",
placeItems: "center",
width: "var(--selector-size,28px)",
height: "var(--selector-size,28px)",
flexShrink: 0,
}}
>
<span
ref={morphRef}
data-slot="selector-surface-host"
data-morph="both"
data-tier="tile"
data-shape={shape === "organic" ? undefined : mappedShape[shape]}
data-depth={shape === "organic" ? 0.018 : 0}
data-lobes={shape === "organic" ? 3 : 0}
data-asym=".1"
data-sw={strokeWidth}
data-motion={quiet || disabled ? "off" : undefined}
aria-disabled={disabled || undefined}
style={
{
position: "absolute",
width: "calc(var(--selector-size,28px) - 4px)",
height: "calc(var(--selector-size,28px) - 4px)",
borderRadius: "48% 52% 50% 50%",
"--mfill": fill,
"--mstroke": stroke,
} as React.CSSProperties
}
/>
<motion.svg
data-slot="selector-drawing"
aria-hidden="true"
focusable="false"
viewBox="0 0 100 100"
width="100%"
height="100%"
style={{ display: "block", overflow: "visible", flexShrink: 0 }}
>
<motion.path
data-slot="selector-surface"
d={paths[shape]}
initial={false}
animate={{ d: paths[shape], scale: 1 }}
transition={transition}
fill={fill}
stroke={
disabled
? "var(--v-disabled-edge)"
: active
? ink
: "var(--v-text-2)"
}
strokeWidth={strokeWidth}
vectorEffect="non-scaling-stroke"
/>
<AnimatePresence initial={false}>
{active && showIndicator && (
<motion.g
key={`${state}-${mark}`}
data-slot="selector-mark"
initial={quiet ? false : { opacity: 0, scale: 0.55 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: quiet ? 1 : 0.55 }}
transition={transition}
style={{ transformOrigin: "50px 50px" }}
>
{state === "indeterminate" ? (
<path
d="M32 50H68"
stroke={ink}
strokeWidth="9"
strokeLinecap="round"
/>
) : mark === "check" ? (
<path
d="M28 50 43 65 73 35"
fill="none"
stroke={ink}
strokeWidth="8"
strokeLinecap="round"
strokeLinejoin="round"
/>
) : mark === "diamond" ? (
<path d="M50 29 71 50 50 71 29 50Z" fill={ink} />
) : mark === "flower" ? (
<path d={paths.flower} transform="translate(28 28) scale(.44)" fill={ink} />
) : (
<circle cx="50" cy="50" r="13" fill={ink} />
)}
</motion.g>
)}
</AnimatePresence>
</motion.svg>
</span>
);
}
registry/cojeev/styles/radio-group.css
@layer cojeev-states {
[data-slot="radio-group"] { display:grid; gap:var(--s-2); }
.v-radio[data-slot="radio-group-item"] { display:inline-flex; align-items:center; gap:var(--s-3); min-height:44px; min-width:0; border:0; box-shadow:none; background:transparent; color:var(--v-text); font-size:var(--fs-body); text-align:start; cursor:pointer; }
[data-slot="radio-group-indicator"] { display:inline-grid; place-items:center; width:var(--selector-size,28px); height:var(--selector-size,28px); flex:none; }
[data-slot="radio-group-item"]:focus-visible { outline:none; }
[data-slot="radio-group-item"]:focus-visible [data-slot="radio-group-indicator"] { outline:2px solid var(--ring); outline-offset:3px; border-radius:var(--r-sm); }
[data-slot="radio-group-item"][data-disabled] { color:var(--v-disabled-ink); cursor:not-allowed; }
.v-radio[data-slot="radio-group-item"].-block { display:grid; grid-template-columns:var(--selector-size,28px) minmax(0,1fr) auto; gap:var(--s-4); padding:var(--s-4) var(--s-4); border-radius:var(--r-card-sm); background:var(--card); box-shadow:inset 0 0 0 1px var(--v-border); }
.v-radio[data-slot="radio-group-item"].-block:not([data-disabled]):hover { background:var(--v-beige-2); }
.v-radio[data-slot="radio-group-item"].-block[data-state="checked"] { background:color-mix(in oklab,var(--selector-accent) 16%,var(--v-canvas)); box-shadow:inset 0 0 0 1.5px var(--selector-accent); }
[data-slot="radio-group-body"] { display:grid; gap:3px; min-width:0; overflow-wrap:anywhere; }
[data-slot="radio-group-body"] b { font-size:var(--fs-body); font-weight:var(--fw-semibold); line-height:1.35; }
[data-slot="radio-group-body"] small { color:var(--v-text-2); font-size:var(--fs-small); line-height:1.45; }
[data-slot="radio-group-item"][data-disabled] [data-slot="radio-group-body"] small { color:var(--v-disabled-ink); }
.v-iradios[data-slot="radio-group"] { grid-template-columns:repeat(auto-fit,minmax(88px,1fr)); max-width:400px; }
.v-iradio[data-slot="radio-group-item"] { position:relative; display:grid; justify-items:center; gap:var(--s-2); padding:var(--s-5) var(--s-3) var(--s-4); border-radius:var(--r-card-sm); background:var(--card); color:var(--v-text); cursor:pointer; }
.v-iradio[data-slot="radio-group-item"] [data-slot="radio-group-indicator"] { position:absolute; inset:5px 5px auto auto; width:var(--selector-size,16px); height:var(--selector-size,16px); }
.v-iradio[data-slot="radio-group-item"] [data-slot="selector-glyph"] { width:var(--selector-size,16px); height:var(--selector-size,16px); }
.v-iradio[data-slot="radio-group-item"] > .v-icon { width:26px; height:26px; }
.v-iradio[data-slot="radio-group-item"] small { font-size:var(--fs-meta); font-weight:var(--fw-medium); }
.v-iradio[data-slot="radio-group-item"][data-state="checked"] { background:var(--selector-accent); color:var(--v-on-accent); }
.v-iradio[data-slot="radio-group-item"]:focus-visible { outline:2px solid var(--ring); outline-offset:3px; }
@media(pointer:coarse) { .v-radio[data-slot="radio-group-item"] { min-height:44px; } }
/* The shared Button body engine owns the moving selector contour. The original
SVG is the immediate SSR/Off/reduced fallback; the mark stays above either. */
[data-slot="selector-glyph"]:has([data-slot="selector-surface-host"] > svg.v-morph) [data-slot="selector-surface"] { opacity:0; }
[data-slot="selector-drawing"] { position:relative; pointer-events:none; }
[data-slot="radio-group-item"][data-appearance] { display:flex; align-items:center; min-height:44px; min-width:0; padding:var(--s-3) var(--s-3); gap:var(--s-3); color:var(--v-text); text-align:start; border:0; border-radius:var(--v-control-radius,12px); background:transparent; cursor:pointer; }
[data-slot="radio-group-item"][data-appearance="row"] { width:100%; border-radius:0; border-bottom:1px solid var(--v-border); }
[data-slot="radio-group-item"][data-appearance="card"] { display:grid; grid-template-columns:var(--selector-size,28px) minmax(0,1fr); align-items:start; width:100%; padding:var(--s-5); border-radius:min(var(--v-control-radius,16px),24px); background:var(--card); box-shadow:inset 0 0 0 1px var(--v-edge); }
[data-slot="radio-group-item"][data-appearance="chip"] { width:fit-content; border-radius:var(--v-control-radius,999px); box-shadow:inset 0 0 0 1px var(--v-edge); gap:var(--s-2); }
[data-slot="radio-group-item"][data-appearance]:not([data-disabled])[data-state="checked"] { background:color-mix(in oklab,var(--selector-accent) 18%,var(--v-canvas)); }
[data-slot="radio-group-item"][data-appearance]:focus-visible { outline:2px solid var(--ring); outline-offset:3px; }
[data-slot="radio-group-item"][data-appearance]:focus-visible [data-slot="radio-group-indicator"] { outline:none; }
[data-slot="radio-group-item"][data-appearance][data-disabled] { color:var(--v-disabled-ink); cursor:not-allowed; }
[data-slot="radio-group-body"] { display:grid; gap:3px; min-width:0; overflow-wrap:anywhere; }
[data-slot="radio-group-body"] small { color:var(--v-text-2); font-size:var(--fs-small); line-height:1.45; }
}
registry/cojeev/styles/control-appearance.css
@layer cojeev-states {
/* One inherited corner contract, with a readable cap for multiline/native fields. */
:is(.v-input,.v-igroup,.v-select,.v-native,.v-number-input,.v-btn) {
border-radius: var(--v-control-radius, var(--r-pill));
}
:is(.v-textarea,.v-composer,.v-native,.v-multi-select__trigger) {
border-radius: min(var(--v-control-radius, 20px), 20px);
}
.v-btn[data-control-shape="card"] { border-radius: var(--r-md); }
.v-btn[data-control-shape="pill"] { border-radius: var(--r-pill); }
/* A Field can own the treatment for its immediate shell; explicit child paint wins. */
:root .v-native[data-appearance] { padding-inline-end:var(--s-10); }
:root .v-native[data-appearance][size]:not([size="1"]) { padding-inline:var(--s-2); }
[data-slot="field"][data-appearance="contour"] { --v-field-face: var(--v-canvas); }
:root [data-appearance="contour"]:is(.v-input,.v-igroup,.v-textarea,.v-native,.v-select,.v-number-input,.v-multi-select__trigger),
:root [data-slot="field"][data-appearance="contour"] :is(.v-input,.v-igroup,.v-textarea):not([data-appearance]) {
background-color: var(--v-canvas);
color: var(--v-text);
box-shadow: inset 0 0 0 1px var(--v-edge);
}
:root [data-appearance="editorial"]:is(.v-input,.v-igroup,.v-textarea,.v-native,.v-select,.v-number-input,.v-multi-select__trigger),
:root [data-slot="field"][data-appearance="editorial"] :is(.v-input,.v-igroup,.v-textarea):not([data-appearance]) {
border-radius: 0;
background-color: transparent;
color: var(--v-text);
box-shadow: inset 0 -1px 0 var(--v-edge);
padding-inline: 2px;
}
:root [data-slot="field"][data-appearance="inset"] {
padding: var(--s-4) var(--s-4);
border-radius: min(var(--v-control-radius, 20px), 20px);
background: var(--v-beige);
color: var(--v-text);
box-shadow: inset 0 1px 2px color-mix(in oklab, var(--v-text) 9%, transparent);
gap: var(--s-2);
}
:root [data-appearance="inset"]:is(.v-input,.v-igroup,.v-textarea,.v-native,.v-select,.v-number-input,.v-multi-select__trigger) {
background-color: var(--v-beige);
color: var(--v-text);
box-shadow: inset 0 1px 2px color-mix(in oklab, var(--v-text) 12%, transparent);
}
:root [data-slot="field"][data-appearance="inset"] :is(.v-input,.v-igroup,.v-textarea):not([data-appearance]) {
background: transparent;
box-shadow: none;
padding-inline: 0;
}
:root [data-slot="field"][data-appearance="inset"] [data-slot="field-label"] { color: var(--v-text-2); }
:root [data-slot="field"][data-appearance="inset"]:focus-within { outline: 2px solid var(--v-brand); outline-offset: 2px; }
:root [data-appearance]:is(.v-input,.v-igroup,.v-textarea,.v-native,.v-select,.v-number-input,.v-multi-select__trigger):focus-within,
:root [data-slot="field"][data-appearance] :is(.v-input,.v-igroup,.v-textarea):focus-within {
outline: 2px solid var(--v-brand);
outline-offset: 3px;
}
:root [data-slot="field"][data-appearance="inset"] :is(.v-input,.v-igroup,.v-textarea):focus-within { outline: none; }
:root [data-appearance]:is(.v-input,.v-igroup,.v-textarea,.v-native,.v-select,.v-number-input,.v-multi-select__trigger):is(:disabled,[data-disabled="true"],:has(input:disabled)),
:root [data-slot="field"][data-appearance] :is(.v-input,.v-igroup,.v-textarea):is(:disabled,:has(input:disabled)) {
background: var(--v-disabled-face);
color: var(--v-disabled-ink);
box-shadow: inset 0 0 0 1px var(--v-disabled-edge);
cursor: not-allowed;
}
:root [data-slot="field"][data-appearance][data-state="error"],
:root [data-appearance]:is(.v-input,.v-igroup,.v-textarea,.v-native,.v-select,.v-number-input,.v-multi-select__trigger):is([aria-invalid="true"],[data-invalid="true"],:has([aria-invalid="true"])) {
outline: 2px solid var(--v-danger-fill);
outline-offset: 3px;
}
:root [data-slot="field"][data-appearance][data-state="error"] [role="alert"] { color: var(--v-danger-ink); }
.v-form-example { display: grid; gap: var(--s-5); width: 100%; min-width: 0; max-width: 480px; margin-inline: auto; }
.v-form-example__intro { display: grid; gap: var(--s-1); }
.v-form-example__intro strong { font-family: var(--font-display, inherit); font-size: var(--fs-title); font-weight: var(--fw-medium); }
.v-form-example__intro p { margin: 0; color: var(--v-text-2); font-size: var(--fs-small); line-height: 1.5; }
.v-form-example__status { min-height: 1.5em; margin: 0; font-size: var(--fs-meta); color: var(--v-text-2); overflow-wrap: anywhere; }
.v-form-example__footer { display: flex; gap: var(--s-3); align-items: center; justify-content: space-between; }
/* Composer owns one surface, while the actual textarea keeps native editing. */
:root .v-composer[data-appearance] { border-radius:min(var(--v-control-radius,20px),20px); background:var(--v-canvas); color:var(--v-text); box-shadow:inset 0 0 0 1px var(--v-edge); }
:root .v-composer[data-appearance="inset"] { background:var(--v-beige); box-shadow:inset 0 1px 2px color-mix(in oklab,var(--v-text) 12%,transparent); }
:root .v-composer[data-appearance="editorial"] { border-radius:0; background:transparent; box-shadow:inset 0 -1px 0 var(--v-edge); }
:root .v-composer[data-appearance] .v-textarea:is(:focus,:hover,:not(:focus)) { background:transparent; box-shadow:none; outline:0; border-radius:0; resize:vertical; }
:root .v-composer[data-appearance] .v-textarea-scroll > textarea { padding-inline-end:var(--s-8); }
:root [data-slot="field"][data-appearance] [data-slot="input-group"] .v-textarea { background:transparent; box-shadow:none; outline:0; border:0; border-radius:0; }
:root .v-composer[data-appearance]:focus-within { outline:2px solid var(--v-brand); outline-offset:3px; }
:root [data-slot="field"][data-appearance="inset"] .v-composer[data-appearance] { background:transparent; box-shadow:none; outline:0; }
.v-form-example[data-form-example="field"] { max-width:600px; }
.v-form-example [data-slot="field-label"] { font-size:var(--fs-control); line-height:1.5; }
.v-form-example [data-slot="field-description"] { font-size:var(--fs-small); margin:0; }
.v-form-example__footer > .v-btn, .v-form-example [data-slot="textarea-composer-bar"] > .v-btn { min-height:44px; }
.v-form-example [data-slot="textarea-composer-bar"] { justify-content:space-between; flex-wrap:wrap; gap:var(--s-3); padding:var(--s-3) var(--s-3); border-top:1px solid var(--v-border); }
.v-form-example__allowance { height:4px; border-radius:var(--r-xs); overflow:hidden; background:var(--v-beige); margin-top:-8px; }
.v-form-example__allowance > span { display:block; height:100%; max-width:100%; border-radius:inherit; background:var(--v-blue-deep); }
.v-form-example__label-row { display:flex; align-items:baseline; flex-wrap:wrap; gap:var(--s-2); justify-content:space-between; }
.v-form-example__label-row > span { font-size:var(--fs-meta); color:var(--v-text-2); }
.v-form-example__hint { margin:0; color:var(--v-text-2); font-size:var(--fs-small); line-height:1.5; }
/* Editorial draws only the focus edge: no moving caret, label or hit area. */
:root [data-appearance="editorial"]:is(.v-input,.v-igroup,.v-textarea,.v-composer),
:root [data-slot="field"][data-appearance="editorial"] :is(.v-input,.v-igroup,.v-textarea):not([data-appearance]) {
background-image:linear-gradient(var(--v-brand),var(--v-brand)); background-repeat:no-repeat; background-position:left bottom; background-size:0% 2px;
transition:background-size var(--t-micro) var(--enter);
}
:root [data-appearance="editorial"]:is(.v-input,.v-igroup,.v-textarea,.v-composer):focus-within,
:root [data-slot="field"][data-appearance="editorial"] :is(.v-input,.v-igroup,.v-textarea):focus-within { background-size:100% 2px; outline:0; }
@media(prefers-reduced-motion:reduce) { :root [data-slot="field"] :is(.v-input,.v-igroup,.v-textarea,.v-composer) { transition:none; } }
}
registry/cojeev/styles/choice-foundations.css
@layer cojeev-states {
/* Shared example composition, shipped with every choice-family entry. */
.v-choice-example {
display: grid;
gap: var(--s-5);
width: 100%;
max-width: 560px;
min-width: 0;
color: var(--v-text);
}
.v-choice-example header {
display: grid;
gap: var(--s-1);
}
.v-choice-example header strong {
font-size: var(--fs-lead);
font-weight: var(--fw-semibold);
}
.v-choice-example header p,
.v-choice-result {
margin: 0;
font-size: var(--fs-small);
line-height: 1.5;
color: var(--v-text-2);
}
.v-choice-options {
display: grid;
gap: var(--s-3);
min-width: 0;
}
.v-choice-options[data-approach="chip"] {
display: flex;
flex-wrap: wrap;
gap: var(--s-2);
}
.v-choice-options[data-approach="card"] {
grid-template-columns: repeat(auto-fit, minmax(min(100%, 160px), 1fr));
align-items: stretch;
}
.v-choice-options[data-approach="card"]
> :is([data-slot="checkbox"], [data-slot="radio-group-item"]) {
grid-template-columns: 1fr;
align-content: start;
gap: var(--s-4);
}
.v-choice-options[data-approach="card"] [data-slot$="-body"] {
gap: var(--s-2);
}
/* The summary below already closes the list; a second adjacent rule reads as a seam. */
.v-choice-options[data-approach="row"]
> :is([data-slot="checkbox"], [data-slot="radio-group-item"]):last-child {
border-bottom: none;
}
.v-choice-result {
padding-top: var(--s-3);
border-top: 1px solid var(--v-border);
}
}