shadimage
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 · medium
ui-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);
}

}

More from 000h-cojeev

Browse all components in this library →