shadimage
000h-cojeev
uiquestionnairesource: ok · hit

Questionnaire

A little guided brief: open questions, a focused journey or a two-column worksheet with living choice marks and honest completion progress.

npx shadcn add "https://000h.cojeev.com/r/questionnaire.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationmotion

Registry dependencies

https://cojeev.com/ui/r/cojeev.jsonhttps://cojeev.com/ui/r/label.jsonhttps://cojeev.com/ui/r/progress.json

Files4 · source cached 10/8/2026, 7:43:16 PM

registry/cojeev/ui/questionnaire.tsx
"use client";
import { useMorph } from "@/lib/cojeev-motion/use-morph";
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 * as React from "react";
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Label, type LabelProps } from "@/components/ui/label";
import { Progress } from "@/components/ui/progress";
import { useFlowGroup } from "@/lib/cojeev-motion/use-flow";
import {
  controlRadiusStyle,
  type ControlRadius,
} from "@/lib/cojeev/control-appearance";
export const questionnaireVariants = cva("v-quest grid gap-[var(--s-7)]");
export type QuestionnaireProps = React.ComponentProps<"div"> & {
  presentation?: "stacked" | "journey" | "worksheet";
  radius?: ControlRadius;
};
export function Questionnaire({
  className,
  presentation,
  radius,
  style,
  ...props
}: QuestionnaireProps) {
  return (
    <div
      data-slot="questionnaire"
      data-part="root"
      data-presentation={presentation}
      style={{ ...style, ...controlRadiusStyle(radius) }}
      className={cn(questionnaireVariants(), className)}
      {...props}
    />
  );
}
export type QuestionnaireProgressProps = React.ComponentProps<"div"> & {
  value: number;
  total: number;
  label?: string;
};
export function QuestionnaireProgress({
  value,
  total,
  className,
  children,
  label = "Questions answered",
  ...props
}: QuestionnaireProgressProps) {
  const maximum = Math.max(0, Number.isFinite(total) ? total : 0);
  const current = Math.max(
    0,
    Math.min(maximum, Number.isFinite(value) ? value : 0),
  );
  return (
    <div
      data-slot="questionnaire-progress"
      className={cn(
        "v-quest__progress flex items-center gap-[var(--s-3)] text-[length:var(--fs-small)] text-[color:var(--v-text-2)]",
        className,
      )}
      {...props}
    >
      {children ?? (
        <>
          <b>
            {current} of {maximum}
          </b>
          <Progress
            aria-label={label}
            value={maximum > 0 ? (current / maximum) * 100 : 0}
          />
        </>
      )}
    </div>
  );
}
export type QuestionnaireQuestionProps = React.ComponentProps<"fieldset">;
export function QuestionnaireQuestion({
  className,
  ...props
}: QuestionnaireQuestionProps) {
  return (
    <fieldset
      data-slot="questionnaire-question"
      className={cn(
        "v-quest__q grid min-w-0 gap-[var(--s-3)] [border:0] p-0 bg-transparent [box-shadow:none]",
        className,
      )}
      {...props}
    />
  );
}
export type QuestionnaireLabelProps = LabelProps;
export function QuestionnaireLabel({
  className,
  ...props
}: QuestionnaireLabelProps) {
  return (
    <Label
      data-slot="questionnaire-label"
      className={cn("[font-size:var(--fs-lead)] [font-weight:var(--fw-semibold)]", className)}
      {...props}
    />
  );
}
type ChoiceContext = {
  name: string;
  selectorShape: SelectorShape;
  selectorTone: SelectorTone;
  selectorSize: SelectorSize;
  selectorIndicator: SelectorIndicator;
  showSelectorIndicator: boolean;
  value?: string;
  change: (value: string) => void;
};
const ChoiceContext = React.createContext<ChoiceContext | null>(null);
export type QuestionnaireOptionsProps = Omit<
  React.ComponentProps<"div">,
  "defaultValue"
> & {
  name?: string;
  selectorShape?: SelectorShape;
  selectorTone?: SelectorTone;
  selectorSize?: SelectorSize;
  selectorIndicator?: SelectorIndicator;
  showSelectorIndicator?: boolean;
  value?: string;
  defaultValue?: string;
  onValueChange?: (value: string) => void;
};
export function QuestionnaireOptions({
  ref,
  className,
  name,
  selectorShape = "organic",
  selectorTone = "pink",
  selectorSize = "default",
  selectorIndicator = "auto",
  showSelectorIndicator = true,
  value,
  defaultValue,
  onValueChange,
  children,
  ...props
}: QuestionnaireOptionsProps) {
  const id = React.useId();
  const [local, setLocal] = React.useState(defaultValue);
  const group = React.useRef<HTMLDivElement>(null);
  const flowRef = useFlowGroup<HTMLDivElement>(ref);
  const attach = React.useCallback(
    (node: HTMLDivElement | null) => {
      group.current = node;
      const release = flowRef(node);
      return () => {
        group.current = null;
        if (typeof release === "function") release();
      };
    },
    [flowRef],
  );
  React.useEffect(() => {
    const node = group.current,
      form = node?.querySelector("input")?.form;
    if (!node || !form) return;
    let pending: ReturnType<typeof setTimeout> | undefined;
    const reset = (event: Event) => {
      clearTimeout(pending);
      pending = setTimeout(() => {
        if (event.defaultPrevented) return;
        const next = value === undefined ? defaultValue : value;
        if (value === undefined) setLocal(defaultValue);
        // The browser resets native checked properties even if React's props
        // did not change. Restore only this group's owned radios after dispatch.
        for (const input of node.querySelectorAll<HTMLInputElement>(
          '[data-slot="questionnaire-option-input"]',
        ))
          input.checked = input.value === next;
      }, 0);
    };
    form.addEventListener("reset", reset);
    return () => {
      form.removeEventListener("reset", reset);
      clearTimeout(pending);
    };
  }, [value, defaultValue]);
  return (
    <ChoiceContext.Provider
      value={{
        name: name ?? id,
        selectorShape,
        selectorTone,
        selectorSize,
        selectorIndicator,
        showSelectorIndicator,
        value: value ?? local,
        change: (next) => {
          if (value === undefined) setLocal(next);
          onValueChange?.(next);
        },
      }}
    >
      <div
        ref={attach}
        data-slot="questionnaire-options"
        className={cn("v-quest__opts grid gap-[var(--s-2)]", className)}
        role="radiogroup"
        {...props}
      >
        {children}
      </div>
    </ChoiceContext.Provider>
  );
}
export type QuestionnaireOptionProps = React.ComponentProps<"label"> & {
  value: string;
  disabled?: boolean;
  selectorShape?: SelectorShape;
  selectorTone?: SelectorTone;
  selectorSize?: SelectorSize;
  selectorIndicator?: SelectorIndicator;
  showSelectorIndicator?: boolean;
  inputProps?: Omit<React.ComponentProps<"input">, "value" | "type">;
};
export function QuestionnaireOption({
  ref: externalMorphRef,
  value,
  disabled,
  selectorShape,
  selectorTone,
  selectorSize,
  selectorIndicator,
  showSelectorIndicator,
  style,
  inputProps,
  className,
  children,
  ...props
}: QuestionnaireOptionProps) {
  const context = React.useContext(ChoiceContext);
  if (!context)
    throw new Error("QuestionnaireOption must be inside QuestionnaireOptions");
  const ownedMorphRef = useMorph<HTMLLabelElement>("cards", externalMorphRef);
  const shape = selectorShape ?? context.selectorShape;
  const tone = selectorTone ?? context.selectorTone;
  return (
    <label
      ref={ownedMorphRef}
      style={selectorStyle(tone, style, selectorSize ?? context.selectorSize)}
      data-selector-shape={shape}
      data-stable-hit=""
      data-slot="questionnaire-option"
      data-state={context.value === value ? "checked" : "unchecked"}
      className={cn(
        "v-quest__opt grid grid-cols-[36px_minmax(0,1fr)_28px] items-center gap-[var(--s-4)] min-h-[64px] pt-[var(--s-4)] pr-[var(--s-4)] pb-[var(--s-4)] pl-[var(--s-4)] rounded-[var(--r-card)] bg-[var(--v-canvas)] cursor-pointer",
        className,
      )}
      {...props}
    >
      <input
        data-slot="questionnaire-option-input"
        type="radio"
        name={context.name}
        value={value}
        checked={context.value === value}
        disabled={disabled}
        {...inputProps}
        onChange={(event) => {
          inputProps?.onChange?.(event);
          if (!event.defaultPrevented && event.target.checked)
            context.change(value);
        }}
      />
      <span data-slot="questionnaire-selector">
        <SelectorGlyph
          shape={shape}
          tone={tone}
          state={context.value === value}
          indicator={selectorIndicator ?? context.selectorIndicator}
          showIndicator={showSelectorIndicator ?? context.showSelectorIndicator}
          disabled={disabled || inputProps?.disabled}
        />
      </span>
      {children}
    </label>
  );
}
export type QuestionnaireOptionBodyProps = React.ComponentProps<"span">;
export function QuestionnaireOptionBody({
  className,
  ...props
}: QuestionnaireOptionBodyProps) {
  return (
    <span
      data-slot="questionnaire-option-body"
      className={cn(
        "v-quest__opt-body order-2 grid gap-[2px] min-w-0",
        className,
      )}
      {...props}
    />
  );
}

export type QuestionnaireWeekdaysProps = React.ComponentProps<"div">;
export function QuestionnaireWeekdays({
  ref,
  className,
  ...props
}: QuestionnaireWeekdaysProps) {
  const flowRef = useFlowGroup<HTMLDivElement>(ref);
  return (
    <div
      ref={flowRef}
      data-slot="questionnaire-weekdays"
      className={cn("v-weekdays flex gap-[var(--s-2)]", className)}
      {...props}
    />
  );
}
export type QuestionnaireWeekdayProps = React.ComponentProps<"label">;
export function QuestionnaireWeekday({
  className,
  ...props
}: QuestionnaireWeekdayProps) {
  return (
    <label
      data-slot="questionnaire-weekday"
      className={cn(
        "relative grid place-items-center size-[40px] [border-radius:50%] text-[length:var(--fs-caps)] font-semibold bg-[var(--card)] text-[color:var(--v-text-2)] cursor-pointer",
        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/questionnaire.css
@layer cojeev-states {
[data-slot="questionnaire-progress"] b {
  color: var(--v-text);
}
[data-slot="questionnaire-progress"] .v-track {
  flex: 1;
  min-width: 80px;
}
[data-slot="questionnaire-option"] {
  transition:
    background var(--t-element),
    box-shadow var(--t-element);
}
[data-slot="questionnaire-option"]:not(:has(input:checked)) {
  box-shadow: inset 0 0 0 1px var(--v-border);
}
[data-slot="questionnaire-option"]:not(:has(input:checked)):hover {
  background: var(--v-beige-2);
  box-shadow: inset 0 0 0 1px var(--v-border);
}
[data-slot="questionnaire-option"]:active {
  transform: none;
}
[data-slot="questionnaire-option"] input {
  order: 3;
  appearance: none;
  width: 22px;
  height: 22px;
  margin: 0;
  border-radius: 50%;
  border: 1.5px solid var(--v-edge);
  background: var(--v-canvas);
  display: grid;
  place-items: center;
  transition:
    background var(--t-micro),
    border-color var(--t-micro);
}
[data-slot="questionnaire-option"] .v-disk[data-slot] {
  order: 1;
  width: 36px;
  height: 36px;
}
[data-slot="questionnaire-option"] input:checked {
  background:
    radial-gradient(circle, var(--ink-fixed) 0 4px, transparent 4.6px),
    var(--v-pink);
  border-color: var(--v-pink);
  border-radius: 46% 54% 50% 50%/50% 46% 54% 50%;
  animation: v-questionnaire-tick var(--t-max) var(--enter) both;
}
[data-slot="questionnaire-options"]:not(.v-glide)
  [data-slot="questionnaire-option"]:has(input:checked) {
  background: var(--v-pink-soft);
  color: var(--v-text);
  box-shadow: none;
}
[data-slot="questionnaire-option"]:has(input:checked) .v-quest__opt-body small {
  color: var(--v-text);
}
[data-slot="questionnaire-option"]:has(input:focus-visible) {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}
[data-slot="questionnaire-option-body"] b {
  font-weight: var(--fw-semibold);
  font-size: var(--fs-body);
  line-height: 1.3;
}
[data-slot="questionnaire-option-body"] small {
  font-size: var(--fs-small);
  color: var(--v-text-2);
  line-height: 1.4;
}
[data-slot="questionnaire-option"]:has(input:disabled) {
  color: var(--v-disabled-ink);
  cursor: not-allowed;
}
:root[data-mode="dark"] [data-slot="questionnaire-option"] {
  background: var(--v-beige);
}
:root[data-mode="dark"] [data-slot="questionnaire-option"] input {
  border-color: var(--v-text-2);
  background: var(--v-beige-2);
}
:root[data-mode="dark"] [data-slot="questionnaire-option"] input:checked {
  background:
    radial-gradient(circle, var(--ink-fixed) 0 4px, transparent 4.6px),
    var(--v-pink);
  border-color: var(--v-pink);
}
@keyframes v-questionnaire-tick {
  0% {
    transform: scale(0.8);
    border-radius: 54% 46% 50% 50%/46% 50% 50% 54%;
  }
  55% {
    transform: scale(1.14, 0.94);
    border-radius: 50% 50% 46% 54%/54% 50% 46% 50%;
  }
  80% {
    transform: scale(0.98, 1.04);
  }
  100% {
    transform: scale(1);
    border-radius: 46% 54% 50% 50%/50% 46% 54% 50%;
  }
}

[data-slot="questionnaire-weekday"] {
  transition:
    background var(--t-micro),
    color var(--t-micro);
}
[data-slot="questionnaire-weekday"] input {
  position: absolute;
  opacity: 0;
  inset: 0;
  margin: 0;
}
[data-slot="questionnaire-weekday"]:hover {
  background: color-mix(in oklab, var(--card) 90%, var(--v-ink));
  color: var(--v-text);
}
[data-slot="questionnaire-weekday"]:has(input:checked) {
  background: var(--v-pink);
  color: var(--v-on-accent);
}
[data-slot="questionnaire-weekday"]:has(input:focus-visible) {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}
@media (max-width: 640px) {
  [data-slot="questionnaire-weekday"] {
    flex: 1 1 0;
    min-width: 0;
    aspect-ratio: 1;
  }
}
:root[data-mode="dark"] [data-slot="questionnaire-weekday"] {
  background: var(--v-beige);
}

/* The native input remains the focus/form owner; the same organic selector used
   by RadioGroup paints its checked state without replacing native behaviour. */
[data-slot="questionnaire-option"] {
  position: relative;
  border-radius: var(--r-card-sm);
  grid-template-columns: 36px minmax(0, 1fr) var(--selector-size, 28px);
}
[data-slot="questionnaire-option"] [data-slot="questionnaire-option-input"] {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  border: 0;
  white-space: nowrap;
}
[data-slot="questionnaire-selector"] {
  order: 3;
  display: inline-grid;
  width: var(--selector-size, 28px);
  height: var(--selector-size, 28px);
}
[data-slot="questionnaire-option-body"] {
  overflow-wrap: anywhere;
}
[data-slot="questionnaire-option"]:not(:has(.v-disk)) {
  grid-template-columns: minmax(0, 1fr) var(--selector-size, 28px);
}
[data-slot="questionnaire-option"]:has(input:disabled) {
  background: var(--v-disabled-face);
}

/* 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="questionnaire-question"] > legend {
  margin-bottom: var(--s-3);
}

/* New questionnaire compositions are opt-in; AgentChat keeps the legacy layout. */
.v-questionnaire-example {
  width: 100%;
  min-width: 0;
  max-width: 720px;
  margin-inline: auto;
  padding-inline: var(--s-2);
  box-sizing: border-box;
}
.v-quest[data-presentation] {
  gap: var(--s-6);
  color: var(--v-text);
}
.v-questionnaire-example__intro {
  display: grid;
  grid-template-columns: 48px minmax(0, 1fr);
  gap: var(--s-4);
  align-items: center;
}
.v-questionnaire-example__intro p {
  margin: 0 0 var(--s-1);
  color: var(--v-text-2);
  font-size: var(--fs-meta);
}
.v-questionnaire-example__intro h3 {
  font: var(--fw-medium) clamp(22px, 3vw, 28px) / 1.2 var(--font-display);
  margin: 0;
}
.v-questionnaire-example__tab {
  display: grid;
  place-items: center;
  width: 48px;
  height: 56px;
  background: var(--v-yellow);
  color: var(--v-on-accent);
  border-radius: var(--r-sm) var(--r-sm) var(--r-panel) var(--r-sm);
}
.v-quest[data-presentation] .v-quest__progress {
  display: grid;
  grid-template-columns: 58px minmax(0, 1fr);
  gap: var(--s-4);
  font-size: var(--fs-meta);
}
.v-quest[data-presentation] .v-quest__progress > b {
  font-weight: var(--fw-medium);
  font-variant-numeric: tabular-nums;
}
.v-questionnaire-example__questions {
  display: grid;
  gap: var(--s-6);
  min-width: 0;
}
.v-quest[data-presentation] [data-slot="questionnaire-question"] {
  --questionnaire-flow-face: color-mix(
    in oklab,
    var(--v-pink) 18%,
    var(--v-canvas)
  );
  --questionnaire-flow-base: transparent;
  position: relative;
  min-width: 0;
  margin: 0;
  padding: var(--s-2) var(--s-5) var(--s-5);
  border: 1px solid var(--v-border);
  border-radius: min(var(--v-control-radius, 24px), 24px);
  background: var(--v-canvas);
}
.v-quest[data-presentation] [data-slot="questionnaire-question"][hidden] {
  display: none;
}
.v-quest[data-presentation] [data-slot="questionnaire-question"] > legend {
  max-width: 100%;
  padding: 0 var(--s-2);
  margin: 0 0 var(--s-1) -8px;
  font: var(--fw-medium) var(--fs-title)/1.4 var(--font-display);
  color: var(--v-text);
}
.v-quest[data-presentation]
  [data-slot="questionnaire-question"]
  > legend:focus {
  outline: none;
}
.v-quest[data-presentation]
  [data-slot="questionnaire-question"]
  > legend:focus-visible {
  outline: 2px solid var(--v-brand);
  outline-offset: 4px;
  border-radius: var(--r-xs);
}
.v-questionnaire-example__number {
  display: inline-grid;
  place-items: center;
  vertical-align: middle;
  width: 36px;
  height: 42px;
  border-radius: var(--r-sm) var(--r-sm) var(--r-card-sm) var(--r-sm);
  background: var(--v-pink);
  color: var(--v-on-accent);
  font-size: var(--fs-reading);
  font-variant-numeric: tabular-nums;
}
.v-questionnaire-example__heading {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  align-items: center;
  gap: var(--s-3);
}
.v-quest[data-presentation]
  [data-slot="questionnaire-question"]:nth-child(2)
  .v-questionnaire-example__number {
  background: var(--v-blue);
}
.v-quest[data-presentation] [data-slot="questionnaire-question"]:nth-child(2) {
  --questionnaire-flow-face: color-mix(
    in oklab,
    var(--v-blue) 18%,
    var(--v-canvas)
  );
}
.v-questionnaire-example__hint {
  margin: 0 0 var(--s-1);
  color: var(--v-text-2);
  font-size: var(--fs-small);
  line-height: 1.5;
}
.v-quest[data-presentation] .v-quest__opts {
  gap: var(--s-2);
}
.v-quest[data-presentation] [data-slot="questionnaire-option"] {
  min-height: 76px;
  padding: var(--s-4) var(--s-4);
  gap: var(--s-3);
  grid-template-columns: minmax(0, 1fr) var(--selector-size, 28px);
  border-radius: min(var(--v-control-radius, 16px), 16px);
  box-shadow: inset 0 0 0 1px var(--v-border);
  background: transparent;
  transition: background var(--t-element);
}
.v-quest[data-presentation]
  [data-slot="questionnaire-option"]:has(input:checked) {
  background: color-mix(in oklab, var(--v-pink) 18%, var(--v-canvas));
  color: var(--v-text);
  box-shadow: inset 0 0 0 1px
    color-mix(in oklab, var(--v-pink) 60%, var(--v-edge));
}
.v-quest[data-presentation]
  [data-slot="questionnaire-question"]:nth-child(2)
  [data-slot="questionnaire-option"]:has(input:checked) {
  background: color-mix(in oklab, var(--v-blue) 18%, var(--v-canvas));
  box-shadow: inset 0 0 0 1px
    color-mix(in oklab, var(--v-blue) 60%, var(--v-edge));
}
.v-quest[data-presentation]
  [data-slot="questionnaire-option"]:has(input:disabled) {
  background: var(--v-disabled-face);
  color: var(--v-disabled-ink);
}
.v-quest[data-presentation]
  [data-slot="questionnaire-option"]:has(input:disabled)
  small {
  color: inherit;
}
.v-questionnaire-example__footer {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-3);
}
.v-questionnaire-example__footer .v-btn {
  min-height: 44px;
}
.v-questionnaire-example__footer > .v-btn:last-child {
  margin-inline-start: auto;
}
.v-questionnaire-example__result {
  margin: 0;
  padding-top: var(--s-4);
  border-top: 1px dashed var(--v-border);
  font-size: var(--fs-small);
  line-height: 1.6;
  color: var(--v-text-2);
}
.v-quest[data-presentation="journey"] .v-questionnaire-example__questions {
  border-inline-start: 1px solid var(--v-border);
  padding-inline-start: var(--s-5);
}
.v-quest[data-presentation="journey"] [data-slot="questionnaire-question"] {
  padding-top: var(--s-4);
}
.v-quest[data-presentation="journey"] .v-questionnaire-example__number {
  width: 44px;
  height: 48px;
  border-radius: 50% 40% 48% 35%;
}
.v-quest[data-presentation="worksheet"] .v-questionnaire-example__questions {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: start;
  gap: var(--s-5);
}
.v-quest[data-presentation="worksheet"] [data-slot="questionnaire-question"] {
  padding-inline: var(--s-4);
}
.v-quest[data-presentation="worksheet"]
  [data-slot="questionnaire-question"]
  > legend {
  font-size: var(--fs-lead);
}
.v-quest[data-presentation="worksheet"] [data-slot="questionnaire-option"] {
  min-height: 120px;
  padding: var(--s-4) var(--s-3);
}
@media (max-width: 640px) {
  .v-quest[data-presentation="worksheet"] .v-questionnaire-example__questions {
    grid-template-columns: 1fr;
  }
  .v-quest[data-presentation="worksheet"] [data-slot="questionnaire-option"] {
    min-height: 88px;
  }
  .v-quest[data-presentation] [data-slot="questionnaire-question"] {
    padding-inline: var(--s-3);
  }
  .v-questionnaire-example__number {
    width: 28px;
    height: 36px;
  }
  .v-quest[data-presentation] [data-slot="questionnaire-question"] > legend {
    font-size: var(--fs-lead);
  }
  .v-quest[data-presentation="journey"] .v-questionnaire-example__questions {
    padding-inline-start: var(--s-3);
  }
}
@media (prefers-reduced-motion: reduce) {
  .v-quest[data-presentation] [data-slot="questionnaire-option"] {
    transition: none;
  }
}

}
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; } }

}

More from 000h-cojeev

Browse all components in this library →