玻璃勾选

choice · glass-apple · 灵感来源:APG Checkbox / Radio Group + iOS 设置勾选

通知方式

已选:email · 「短信」是禁用项

部署环境

当前:staging · 原生方向键切换

已停用

复用卡

Checkbox & Radio Group (APG) · 玻璃勾选 · 勾选 / Glass

规格 · 术语提示词(给 AI 用)

做一个 Checkbox & Radio Group (APG)(玻璃勾选):
- 原生 checkbox / radio + fieldset / legend;单选组同 name,方向键与空格交给浏览器
- input sr-only,focus-visible 环用 peer 画在可视框上,读屏与键盘都不丢
- values 数组同时兼容多选与单选;组级 fieldset[disabled] 与单项禁用互不干扰
- 未选半透明 + 发丝边 + inset;选中 accent 实底 + 反色勾 / 点 + 轻浮起
用这些 CSS 变量:--color-accent --color-bg --color-text --color-text-muted --glass-bg --glass-border --glass-inset --shadow-sm --text-xs
灵感参考:APG Checkbox / Radio Group + iOS 设置勾选

参数(CSS 变量)

--color-accent --color-bg --color-text --color-text-muted --glass-bg --glass-border --glass-inset --shadow-sm --text-xs

必缺状态

焦点 / 禁用

整页场景

(尚未接进整页场景)

安装(一条链接)

调研笔记

玻璃勾选

拆解

  • 一个类型两种形状:kind="checkbox" 方形勾选,kind="radio" 圆形单选
  • 用原生 <input type=checkbox|radio> + <fieldset> / <legend>;label 包住整行,点文字也能选
  • 单选组共用同一个 name,方向键切换由浏览器原生完成;复选框空格切换
  • 输入框 sr-only,焦点环用 peer-focus-visible 画在可视框上——否则焦点落在看不见的 input 上
  • values 是数组:复选框多值,单选框 0 / 1 值;组级或单项都能禁用

材质

  • 未选:半透明玻璃底 + 发丝边 + inset 高光
  • 选中:accent 实底 + 反色勾 / 点 + --shadow-sm 轻微浮起
  • 单选的点用 bg-current,跟随选中态的前景色

边界

  • 不做 indeterminate(半选);需要时另加 indeterminate 状态
  • 单选组的方向键完全交给原生 input,不自建 roving tabindex
  • 组级禁用用 fieldset[disabled],单项禁用只锁那一行

变量:--glass-bg / --glass-border / --glass-inset / --color-accent / --shadow-sm。

实现

choice/glass-apple
import { useId } from 'react';
import { Check } from 'lucide-react';
import { Icon } from '~/components/Icon';

export interface ChoiceOption {
  value: string;
  label: string;
  disabled?: boolean;
}

interface Props {
  kind: 'checkbox' | 'radio';
  options: ChoiceOption[];
  values: string[];
  onChange?: (values: string[]) => void;
  label?: string;
  disabled?: boolean;
  className?: string;
}

/**
 * 玻璃勾选:复选框与单选框共用一套行结构,只有可视框形状不同。
 * 用原生 input 保留键盘与读屏行为;焦点环画在可视框上(input 本身 sr-only)。
 */
export function GlassAppleChoice({
  kind,
  options,
  values,
  onChange,
  label,
  disabled = false,
  className = '',
}: Props) {
  const groupId = useId();

  const toggle = (value: string) => {
    if (kind === 'radio') {
      onChange?.([value]);
      return;
    }
    const next = new Set(values);
    if (next.has(value)) next.delete(value);
    else next.add(value);
    onChange?.([...next]);
  };

  return (
    <fieldset
      disabled={disabled || undefined}
      aria-disabled={disabled || undefined}
      className={`border-0 p-0 m-0 min-w-0 ${disabled ? 'opacity-60' : ''} ${className}`}
    >
      {label && (
        <legend className="text-xs uppercase tracking-[0.18em] text-[var(--color-text-muted)] mb-2.5">
          {label}
        </legend>
      )}
      <div className="space-y-2.5">
        {options.map((option) => {
          const isOn = values.includes(option.value);
          const isDisabled = disabled || option.disabled;
          return (
            <label
              key={option.value}
              className={`flex items-center gap-3 text-sm ${
                isDisabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'
              }`}
            >
              <input
                type={kind}
                name={groupId}
                value={option.value}
                checked={isOn}
                disabled={isDisabled}
                onChange={() => toggle(option.value)}
                className="peer sr-only"
              />
              <span
                aria-hidden="true"
                className={`
                  grid place-items-center shrink-0 size-5
                  ${kind === 'checkbox' ? 'rounded-md' : 'rounded-full'}
                  ${isOn
                    ? 'bg-[var(--color-accent)] border border-[var(--color-accent)] text-[var(--color-bg)] shadow-[var(--shadow-sm)]'
                    : 'bg-[var(--glass-bg)] border border-[var(--glass-border)] shadow-[var(--glass-inset)]'}
                  peer-focus-visible:outline peer-focus-visible:outline-2
                  peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[var(--color-accent)]
                `}
              >
                {isOn && kind === 'checkbox' && (
                  <Icon icon={Check} size={12} strokeWidth={3} />
                )}
                {isOn && kind === 'radio' && (
                  <span className="size-2 rounded-full bg-current" />
                )}
              </span>
              <span>{option.label}</span>
            </label>
          );
        })}
      </div>
    </fieldset>
  );
}

元信息

标识
choice/glass-apple
状态
done
最后提交
2026-11-01
风格轴
glass-apple
依赖 token
--color-accent --color-bg --color-text --color-text-muted --glass-bg --glass-border --glass-inset --shadow-sm --text-xs
在工作台打开 源码与 notes 都在 src/components/choice/glass-apple/