Skip to content

component-form skill (pilot)

component-form was a pilot skill that auto-triggered on natural language form requests. Instead of running /kit-add form, you described the form you wanted and the skill resolved the right field types, generated the dependencies, and produced a complete, accessible form component.

The skill activates when your message contains any of these patterns:

PhraseExample
”create a ___ form""create a signup form"
"build a ___ form""build a contact form"
"generate a form""generate a login form with email and password"
"make a form for""make a form for collecting user feedback"
"I need a form that""I need a form that captures name, email, and message”

The skill infers the field list from your description. Given “create a signup form with name, email, and password”:

Detected fieldResolved typeComponent
nametext inputInput
emailemail inputInput (type=“email”)
passwordpassword inputInput (type=“password”)

Supported field types:

Field descriptionResolved to
text, name, first name, last name, titleInput (text)
email, email addressInput (email)
password, confirm passwordInput (password)
message, description, notes, bioTextarea
dropdown, select, pickSelect
agree, accept, checkCheckbox
radio, choose oneRadio group (using Checkbox)
file, upload, attachmentInput (file) — v1 fallback; see docs/troubleshooting.md
<form style="display:flex;flex-direction:column;gap:1rem;max-width:24rem">
<label style="display:flex;flex-direction:column;gap:.25rem">
  <span>Full name</span>
  <input type="text" placeholder="Jane Smith" />
</label>
<label style="display:flex;flex-direction:column;gap:.25rem">
  <span>Email address</span>
  <input type="email" placeholder="jane@example.com" />
</label>
<label style="display:flex;flex-direction:column;gap:.25rem">
  <span>Message</span>
  <textarea rows="3" placeholder="Your message…"></textarea>
</label>
<label style="display:flex;flex-direction:column;gap:.25rem">
  <span>Role</span>
  <select><option>Designer</option><option>Developer</option></select>
</label>
<label style="display:flex;gap:.5rem;align-items:center">
  <input type="checkbox" /> <span>I agree to the terms</span>
</label>
<button type="submit">Submit</button>
</form>

When component-form auto-triggers, exiting plan mode is the first action. Dependency generation invokes /kit-add (which writes TSX/SCSS files) and the form output itself is written to src/forms/<FormName>.tsx, both of which plan mode would block. The skill only stays in plan mode when the user asked for a description of the form without generating anything.

After resolving the field list, the skill runs /kit-add for all required dependencies:

Resolved dependencies:
/kit-add field input checkbox button
Generating (4 components)...
src/components/fpkit/Field.tsx ✓
src/components/fpkit/Field.scss ✓
src/components/fpkit/Input.tsx ✓
src/components/fpkit/Input.scss ✓
src/components/fpkit/Checkbox.tsx ✓
src/components/fpkit/Checkbox.scss ✓
src/components/fpkit/Button.tsx ✓
src/components/fpkit/Button.scss ✓

The form component is written to src/forms/<FormName>.tsx. Example for “create a signup form”:

src/forms/SignupForm.tsx
import React, { useState } from "react";
import { Field } from "../components/fpkit/Field";
import { Input } from "../components/fpkit/Input";
import { Checkbox } from "../components/fpkit/Checkbox";
import { Button } from "../components/fpkit/Button";
interface SignupFormValues {
name: string;
email: string;
password: string;
agreeToTerms: boolean;
}
interface SignupFormErrors {
name?: string;
email?: string;
password?: string;
agreeToTerms?: string;
}
export function SignupForm() {
const [values, setValues] = useState<SignupFormValues>({
name: "",
email: "",
password: "",
agreeToTerms: false,
});
const [errors, setErrors] = useState<SignupFormErrors>({});
const [submitting, setSubmitting] = useState(false);
function validate(): SignupFormErrors {
const e: SignupFormErrors = {};
if (!values.name.trim()) e.name = "Name is required";
if (!values.email.includes("@")) e.email = "Valid email required";
if (values.password.length < 8)
e.password = "Password must be at least 8 characters";
if (!values.agreeToTerms) e.agreeToTerms = "You must agree to continue";
return e;
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const errs = validate();
setErrors(errs);
if (Object.keys(errs).length > 0) return;
setSubmitting(true);
try {
// TODO: replace with your submit logic
await new Promise((r) => setTimeout(r, 1000));
} finally {
setSubmitting(false);
}
}
return (
<form onSubmit={handleSubmit} aria-labelledby="signup-heading" noValidate>
<h2 id="signup-heading">Create your account</h2>
<Field
label="Full name"
htmlFor="signup-name"
error={errors.name}
required
>
<Input
id="signup-name"
type="text"
value={values.name}
onChange={(e) => setValues((v) => ({ ...v, name: e.target.value }))}
aria-required="true"
aria-invalid={!!errors.name}
aria-describedby={errors.name ? "signup-name-error" : undefined}
/>
</Field>
<Field
label="Email address"
htmlFor="signup-email"
error={errors.email}
required
>
<Input
id="signup-email"
type="email"
value={values.email}
onChange={(e) => setValues((v) => ({ ...v, email: e.target.value }))}
aria-required="true"
aria-invalid={!!errors.email}
/>
</Field>
<Field
label="Password"
htmlFor="signup-password"
error={errors.password}
required
>
<Input
id="signup-password"
type="password"
value={values.password}
onChange={(e) =>
setValues((v) => ({ ...v, password: e.target.value }))
}
aria-required="true"
aria-invalid={!!errors.password}
aria-describedby="signup-password-hint"
/>
<span id="signup-password-hint" className="text-muted text-sm">
Must be at least 8 characters
</span>
</Field>
<Checkbox
id="signup-agree"
label="I agree to the terms of service"
checked={values.agreeToTerms}
onChange={(e) =>
setValues((v) => ({ ...v, agreeToTerms: e.target.checked }))
}
error={errors.agreeToTerms}
required
/>
<Button type="submit" variant="primary" disabled={submitting}>
{submitting ? "Creating account…" : "Create account"}
</Button>
</form>
);
}

Every generated form includes:

  • aria-labelledby on <form> pointing to the form heading
  • noValidate on <form> (prevents native browser validation so custom error messages render)
  • aria-required="true" on required fields (not just the required attribute)
  • aria-invalid set to true when a field has an error
  • aria-describedby linking error messages to their fields
  • role="alert" on error containers for screen reader announcement
  • Accessible disabled button — aria-disabled via the Button component’s built-in pattern
  • Form heading with unique id for aria-labelledby linkage

If the description is ambiguous, the skill asks one question before generating:

You mentioned "select" — should this be:
1. A native <select> dropdown
2. A custom Select component with keyboard navigation
Reply with 1 or 2.

The skill asks at most one clarifying question per run.