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