Token导航 LogoToken导航TokenDH.com
研究检索只读github未标认证来源可访问clear审计通过

form-ux-patterns形成用户体验模式

Agent Skill

用于辅助界面设计、视觉规范、排版、配色、布局和交互体验优化。它适合让 Agent 根据产品场景整理页面结构、生成 UI 方案、检查视觉一致性或改进组件层级。使用时需要结合现有品牌、设计系统和用户任务,不应只堆装饰元素;涉及真实页面改动时,应通过截图或浏览器预览检查文本溢出、对齐和响应式表现。

总安装

1,763

周安装

72

GitHub Stars

8

下载量

564
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

3

许可证

MIT

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:form-ux-patterns(形成用户体验模式)
来源仓库:https://github.com/bbeierle12/skill-mcp-claude
仓库路径:skills/form-ux-patterns
安装命令:
npx skills add https://github.com/bbeierle12/skill-mcp-claude --skill form-ux-patterns
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。不同来源提供的安装方式可能略有差异;本站展示可直接复制的安装命令,安装前请核对来源页面。

skills.shnpx skills
npx skills add https://github.com/bbeierle12/skill-mcp-claude --skill form-ux-patterns

简介

用于辅助界面设计模式、视觉规范和交互体验优化。

  • 适合根据产品场景整理页面结构或改进组件层级。
  • 需结合现有品牌、设计系统和用户任务进行设计。
  • 不应只堆叠装饰元素,应聚焦功能与体验平衡。form-ux-patterns 属于研究检索类 Skill,可作为该场景下的辅助能力补充。
  • 涉及真实页面改动时需通过浏览器预览检查对齐与响应式。

SKILL.md

Form UX Patterns

Patterns for complex forms based on cognitive load research and aviation UX principles.

Quick Start

// Multi-step form with chunking
import { useMultiStepForm } from './multi-step-form';

function CheckoutWizard() {
  const { currentStep, steps, goNext, goBack, isLastStep } = useMultiStepForm({
    steps: [
      { id: 'contact', title: 'Contact', fields: ['email', 'phone'] },
      { id: 'shipping', title: 'Shipping', fields: ['name', 'street', 'city', 'state', 'zip'] },
      { id: 'payment', title: 'Payment', fields: ['cardName', 'cardNumber', 'expiry', 'cvv'] }
    ]
  });

  return (
    <form>
      <StepIndicator steps={steps} current={currentStep} />
      <StepContent step={steps[currentStep]} />
      <StepNavigation onBack={goBack} onNext={goNext} isLast={isLastStep} />
    </form>
  );
}

Core Principles

1. Cognitive Chunking (Aviation Principle)

"Humans can hold 5-7 items in working memory" — Miller's Law
// ❌ BAD: All fields on one page
<form>
  <input name="email" />
  <input name="phone" />
  <input name="name" />
  <input name="street" />
  <input name="street2" />
  <input name="city" />
  <input name="state" />
  <input name="zip" />
  <input name="cardName" />
  <input name="cardNumber" />
  <input name="expiry" />
  <input name="cvv" />
  {/* 12 fields = cognitive overload */}
</form>

// ✅ GOOD: Chunked into logical groups (5-7 max per group)
<form>
  <fieldset>
    <legend>Contact (2 fields)</legend>
    <input name="email" />
    <input name="phone" />
  </fieldset>

  <fieldset>
    <legend>Shipping (5 fields)</legend>
    <input name="name" />
    <input name="street" />
    <input name="city" />
    <input name="state" />
    <input name="zip" />
  </fieldset>

  <fieldset>
    <legend>Payment (4 fields)</legend>
    <input name="cardName" />
    <input name="cardNumber" />
    <input name="expiry" />
    <input name="cvv" />
  </fieldset>
</form>

2. Briefing vs. Checklist (Aviation Principle)

Instructions should be separate from labels, given before the task.
// ❌ BAD: Instructions mixed with labels
<label>
  Password (must be 8+ characters with uppercase, lowercase, and number)
</label>
<input type="password" />

// ✅ GOOD: Briefing before, label during
<div className="field-briefing">
  <p>Create a strong password with:</p>
  <ul>
    <li>At least 8 characters</li>
    <li>Uppercase and lowercase letters</li>
    <li>At least one number</li>
  </ul>
</div>

<label>Password</label>
<input type="password" />

3. Progressive Disclosure

Show only what's needed, when it's needed.
// Reveal fields based on selection
function ShippingForm() {
  const [method, setMethod] = useState<'standard' | 'express' | 'pickup'>('standard');

  return (
    <form>
      <RadioGroup
        label="Delivery method"
        value={method}
        onChange={setMethod}
        options={[
          { value: 'standard', label: 'Standard (5-7 days)' },
          { value: 'express', label: 'Express (2-3 days)' },
          { value: 'pickup', label: 'Store pickup' }
        ]}
      />

      {/* Only show address for shipping methods */}
      {method !== 'pickup' && (
        <AddressFields />
      )}

      {/* Only show store selector for pickup */}
      {method === 'pickup' && (
        <StoreSelector />
      )}
    </form>
  );
}

Multi-Step Forms

Step Configuration

// types/multi-step.ts
export interface FormStep {
  /** Unique step identifier */
  id: string;

  /** Display title */
  title: string;

  /** Optional description (briefing) */
  description?: string;

  /** Fields in this step (for validation) */
  fields: string[];

  /** Zod schema for this step */
  schema?: z.ZodType;

  /** Whether step can be skipped */
  optional?: boolean;

  /** Condition for showing this step */
  condition?: (formData: Record<string, any>) => boolean;
}

export interface FormChunk {
  /** Chunk identifier */
  id: string;

  /** Chunk title */
  title: string;

  /** Briefing text (shown before fields) */
  briefing?: string;

  /** Fields in this chunk (max 5-7) */
  fields: string[];
}

Multi-Step Hook

// hooks/use-multi-step-form.ts
import { useState, useCallback, useMemo } from 'react';
import { UseFormReturn } from 'react-hook-form';

export interface UseMultiStepFormOptions {
  steps: FormStep[];
  form: UseFormReturn<any>;
  onComplete?: (data: any) => void;
}

export interface UseMultiStepFormReturn {
  /** Current step index */
  currentStep: number;

  /** Current step config */
  step: FormStep;

  /** All steps (filtered by conditions) */
  steps: FormStep[];

  /** Total step count */
  totalSteps: number;

  /** Whether on first step */
  isFirstStep: boolean;

  /** Whether on last step */
  isLastStep: boolean;

  /** Progress percentage (0-100) */
  progress: number;

  /** Go to next step (validates current) */
  goNext: () => Promise<boolean>;

  /** Go to previous step */
  goBack: () => void;

  /** Go to specific step */
  goTo: (index: number) => void;

  /** Can navigate to step (all previous valid) */
  canGoTo: (index: number) => boolean;
}

export function useMultiStepForm({
  steps: allSteps,
  form,
  onComplete
}: UseMultiStepFormOptions): UseMultiStepFormReturn {
  const [currentStep, setCurrentStep] = useState(0);

  // Filter steps by conditions
  const steps = useMemo(() => {
    const data = form.getValues();
    return allSteps.filter(step =>
      !step.condition || step.condition(data)
    );
  }, [allSteps, form]);

  const step = steps[currentStep];
  const totalSteps = steps.length;
  const isFirstStep = currentStep === 0;
  const isLastStep = currentStep === totalSteps - 1;
  const progress = ((currentStep + 1) / totalSteps) * 100;

  const goNext = useCallback(async () => {
    // Validate current step fields
    const isValid = await form.trigger(step.fields as any);

    if (!isValid) {
      // Focus first error
      const firstError = document.querySelector('[aria-invalid="true"]');
      (firstError as HTMLElement)?.focus();
      return false;
    }

    if (isLastStep) {
      // Submit form
      const data = form.getValues();
      onComplete?.(data);
    } else {
      setCurrentStep(prev => prev + 1);
      // Focus step heading
      requestAnimationFrame(() => {
        document.getElementById('step-heading')?.focus();
      });
    }

    return true;
  }, [step, isLastStep, form, onComplete]);

  const goBack = useCallback(() => {
    if (!isFirstStep) {
      setCurrentStep(prev => prev - 1);
      requestAnimationFrame(() => {
        document.getElementById('step-heading')?.focus();
      });
    }
  }, [isFirstStep]);

  const goTo = useCallback((index: number) => {
    if (index >= 0 && index < totalSteps) {
      setCurrentStep(index);
    }
  }, [totalSteps]);

  const canGoTo = useCallback((index: number) => {
    // Can always go back
    if (index < currentStep) return true;

    // Can only go forward if all previous steps are valid
    // (would need form state tracking for this)
    return index <= currentStep;
  }, [currentStep]);

  return {
    currentStep,
    step,
    steps,
    totalSteps,
    isFirstStep,
    isLastStep,
    progress,
    goNext,
    goBack,
    goTo,
    canGoTo
  };
}

Step Indicator Component

// components/StepIndicator.tsx
interface StepIndicatorProps {
  steps: FormStep[];
  currentStep: number;
  onStepClick?: (index: number) => void;
  canNavigate?: (index: number) => boolean;
}

export function StepIndicator({
  steps,
  currentStep,
  onStepClick,
  canNavigate
}: StepIndicatorProps) {
  return (
    <nav aria-label="Form progress">
      <ol className="step-indicator">
        {steps.map((step, index) => {
          const status = index < currentStep
            ? 'complete'
            : index === currentStep
              ? 'current'
              : 'upcoming';

          const clickable = canNavigate?.(index) ?? false;

          return (
            <li
              key={step.id}
              className={`step-indicator__item step-indicator__item--${status}`}
            >
              {clickable ? (
                <button
                  type="button"
                  onClick={() => onStepClick?.(index)}
                  aria-current={status === 'current' ? 'step' : undefined}
                >
                  <span className="step-indicator__number">{index + 1}</span>
                  <span className="step-indicator__title">{step.title}</span>
                </button>
              ) : (
                <span aria-current={status === 'current' ? 'step' : undefined}>
                  <span className="step-indicator__number">{index + 1}</span>
                  <span className="step-indicator__title">{step.title}</span>
                </span>
              )}
            </li>
          );
        })}
      </ol>

      {/* Progress bar */}
      <div
        className="step-indicator__progress"
        role="progressbar"
        aria-valuenow={currentStep + 1}
        aria-valuemin={1}
        aria-valuemax={steps.length}
        aria-label={`Step ${currentStep + 1} of ${steps.length}`}
      >
        <div
          className="step-indicator__progress-fill"
          style={{ width: `${((currentStep + 1) / steps.length) * 100}%` }}
        />
      </div>
    </nav>
  );
}

Step Navigation Component

// components/StepNavigation.tsx
interface StepNavigationProps {
  onBack: () => void;
  onNext: () => void;
  isFirstStep: boolean;
  isLastStep: boolean;
  isSubmitting?: boolean;
  backLabel?: string;
  nextLabel?: string;
  submitLabel?: string;
}

export function StepNavigation({
  onBack,
  onNext,
  isFirstStep,
  isLastStep,
  isSubmitting = false,
  backLabel = 'Back',
  nextLabel = 'Continue',
  submitLabel = 'Submit'
}: StepNavigationProps) {
  return (
    <div className="step-navigation">
      {!isFirstStep && (
        <button
          type="button"
          onClick={onBack}
          className="step-navigation__back"
          disabled={isSubmitting}
        >
          {backLabel}
        </button>
      )}

      <button
        type="button"
        onClick={onNext}
        className="step-navigation__next"
        disabled={isSubmitting}
      >
        {isSubmitting ? (
          <>
            <Spinner aria-hidden="true" />
            <span className="sr-only">Processing...</span>
            Processing...
          </>
        ) : (
          isLastStep ? submitLabel : nextLabel
        )}
      </button>
    </div>
  );
}

Conditional Fields

Pattern: Show/Hide Based on Selection

// components/ConditionalField.tsx
import { useFormContext, useWatch } from 'react-hook-form';
import { ReactNode } from 'react';

interface ConditionalFieldProps {
  /** Field to watch */
  watch: string;

  /** Condition for showing children */
  when: (value: any) => boolean;

  /** Children to render when condition is true */
  children: ReactNode;

  /** Whether to keep values when hidden */
  keepValues?: boolean;
}

export function ConditionalField({
  watch: watchField,
  when,
  children,
  keepValues = false
}: ConditionalFieldProps) {
  const { control, unregister } = useFormContext();
  const value = useWatch({ control, name: watchField });

  const shouldShow = when(value);

  // Optionally unregister fields when hidden
  useEffect(() => {
    if (!shouldShow && !keepValues) {
      // Get field names from children and unregister
      // (implementation depends on your field structure)
    }
  }, [shouldShow, keepValues]);

  if (!shouldShow) return null;

  return <>{children}</>;
}

// Usage
<FormField name="hasCompany" label="Are you a business?" type="checkbox" />

<ConditionalField watch="hasCompany" when={(v) => v === true}>
  <FormField name="companyName" label="Company name" />
  <FormField name="taxId" label="Tax ID" />
</ConditionalField>

Pattern: Dynamic Field Array

// components/RepeatableField.tsx
import { useFieldArray, useFormContext } from 'react-hook-form';

interface RepeatableFieldProps {
  name: string;
  label: string;
  maxItems?: number;
  minItems?: number;
  renderItem: (index: number) => ReactNode;
}

export function RepeatableField({
  name,
  label,
  maxItems = 10,
  minItems = 1,
  renderItem
}: RepeatableFieldProps) {
  const { control } = useFormContext();
  const { fields, append, remove } = useFieldArray({ control, name });

  const canAdd = fields.length < maxItems;
  const canRemove = fields.length > minItems;

  return (
    <fieldset className="repeatable-field">
      <legend>{label}</legend>

      {fields.map((field, index) => (
        <div key={field.id} className="repeatable-field__item">
          {renderItem(index)}

          {canRemove && (
            <button
              type="button"
              onClick={() => remove(index)}
              aria-label={`Remove item ${index + 1}`}
            >
              Remove
            </button>
          )}
        </div>
      ))}

      {canAdd && (
        <button
          type="button"
          onClick={() => append({})}
          className="repeatable-field__add"
        >
          Add {label.toLowerCase()}
        </button>
      )}
    </fieldset>
  );
}

// Usage
<RepeatableField
  name="teammates"
  label="Team Members"
  maxItems={5}
  renderItem={(index) => (
    <>
      <FormField name={`teammates.${index}.name`} label="Name" />
      <FormField name={`teammates.${index}.email`} label="Email" />
    </>
  )}
/>

Form Layout Patterns

Single Column (Recommended Default)

// Best for most forms - clear visual flow
<form className="form-layout--single">
  <FormField name="email" label="Email" />
  <FormField name="password" label="Password" />
  <button type="submit">Sign in</button>
</form>

// CSS
.form-layout--single {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  max-width: 400px;
}

Two Column (Use Sparingly)

// Only for related short fields
<form className="form-layout--two-col">
  <FormField name="firstName" label="First name" />
  <FormField name="lastName" label="Last name" />

  <FormField name="city" label="City" className="col-span-1" />
  <FormField name="state" label="State" className="col-span-1" />
</form>

// CSS
.form-layout--two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
}

@media (max-width: 640px) {
  .form-layout--two-col {
    grid-template-columns: 1fr;
  }
}

Card Sections

// For long forms with distinct sections
<form className="form-layout--cards">
  <section className="form-card">
    <h3>Contact Information</h3>
    <FormField name="email" label="Email" />
    <FormField name="phone" label="Phone" />
  </section>

  <section className="form-card">
    <h3>Shipping Address</h3>
    <AddressFields />
  </section>

  <section className="form-card">
    <h3>Payment</h3>
    <PaymentFields />
  </section>
</form>

File Structure

form-ux-patterns/
├── SKILL.md
├── references/
│   ├── cognitive-load.md       # Research on chunking
│   └── wizard-patterns.md      # Multi-step best practices
└── scripts/
    ├── multi-step-form.tsx     # Multi-step hook + components
    ├── conditional-field.tsx   # Show/hide patterns
    ├── repeatable-field.tsx    # Dynamic arrays
    ├── step-indicator.tsx      # Progress indicator
    └── step-indicator.css      # Styles

Reference

  • references/cognitive-load.md — Research on Miller's Law and chunking
  • references/wizard-patterns.md — Multi-step wizard best practices

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

04

需要参考平台分布和安装热度时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

补充不同宿主或平台的使用分布数据

能力 5

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Claude Code

26.12%
按下载量换算147

OpenCode

20.79%
按下载量换算117

Antigravity

19.41%
按下载量换算109

windsurf

12.7%
按下载量换算72

Gemini CLI

7.47%
按下载量换算42

Codex

3.31%
按下载量换算19

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

只读

该 Skill 主要提供规则、说明或参考内容,本身偏只读;真正读写文件、联网或执行命令仍取决于宿主 Agent 的任务。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。

来源信息

继续浏览同类 Skills