SPB Git

spb/ultra-sharp-agent-skills Public

Ultra-Sharp Agent Skills — a research-first skill-authoring system + 72 production-ready skills for AI agents.

Python 100%

# Form Patterns — Copy-Paste Reference

# Contents

  • Complete field markup
  • Common autocomplete values
  • Validation timing (blur, then input)
  • Error summary after failed submit
  • Submit button pending state
  • Multi-step form skeleton
  • Gotchas

# Complete field markup

html
<div class="field">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" required
         autocomplete="email" placeholder="name@example.com"
         aria-describedby="email-hint email-error">
  <p id="email-hint" class="hint">We only use this for receipts.</p>
  <p id="email-error" class="error" hidden></p>
</div>

The error node exists from the start (hidden) so aria-describedby stays stable.

# Common autocomplete values

Field type autocomplete
Email email email
Phone tel tel
First / last name text given-name / family-name
Street / city / postal text address-line1 / address-level2 / postal-code
Country (select) country-name
Card number / expiry / CVC text cc-number / cc-exp / cc-csc
New / current password password new-password / current-password
One-time code text one-time-code

# Validation timing (blur, then input)

js
const messages = {
  valueMissing: (label) => `Enter your ${label.toLowerCase()}.`,
  typeMismatch: () => 'Enter an email address with an @, like name@example.com.',
  tooShort: (label, input) => `${label} must be at least ${input.minLength} characters.`,
};

function validate(input) {
  const label = input.labels[0].textContent;
  const errEl = document.getElementById(input.getAttribute('aria-describedby')
                                        .split(' ').find(id => id.endsWith('-error')));
  let msg = '';
  for (const key of Object.keys(messages)) {
    if (input.validity[key]) { msg = messages[key](label, input); break; }
  }
  input.setAttribute('aria-invalid', msg ? 'true' : 'false');
  errEl.textContent = msg;
  errEl.hidden = !msg;
  return !msg;
}

document.querySelectorAll('input, select, textarea').forEach((input) => {
  input.addEventListener('blur', () => validate(input), { once: false });
  input.addEventListener('input', () => {
    // re-validate live only after the field has already errored
    if (input.getAttribute('aria-invalid') === 'true') validate(input);
  });
});

Pair with <form novalidate> so these messages replace the browser bubbles.

# Error summary after failed submit

js
form.addEventListener('submit', (e) => {
  const fields = [...form.querySelectorAll('input, select, textarea')];
  const invalid = fields.filter((f) => !validate(f));
  if (invalid.length) {
    e.preventDefault();
    const summary = document.getElementById('form-alert');
    summary.innerHTML = `<p>Fix ${invalid.length} error(s):</p><ul>` +
      invalid.map((f) => `<li><a href="#${f.id}">${f.labels[0].textContent}</a></li>`).join('') +
      '</ul>';
    invalid[0].focus();
  }
});
html
<div id="form-alert" role="alert" tabindex="-1"></div>

# Submit button pending state

js
form.addEventListener('submit', async (e) => {
  e.preventDefault();
  const btn = form.querySelector('button[type="submit"]');
  btn.disabled = true;
  btn.dataset.label = btn.textContent;
  btn.textContent = 'Creating account…';
  try {
    await submit(new FormData(form));
  } catch (err) {
    showServerErrors(err);           // role="alert" summary; inputs keep their values
  } finally {
    btn.disabled = false;
    btn.textContent = btn.dataset.label;
  }
});

Disable only during the request — a permanently disabled submit hides why the form can't be sent.

# Multi-step form skeleton

html
<form id="wizard">
  <p class="progress" aria-live="polite">Step <span id="step-n">1</span> of 3</p>
  <fieldset data-step="1"><legend>Your details</legend>…</fieldset>
  <fieldset data-step="2" hidden><legend>Shipping</legend>…</fieldset>
  <fieldset data-step="3" hidden><legend>Review &amp; confirm</legend>…</fieldset>
  <button type="button" id="back" hidden>Back</button>
  <button type="button" id="next">Continue</button>
  <button type="submit" id="finish" hidden>Place order</button>
</form>

Rules encoded above: steps are fieldsets shown one at a time (data survives because nothing unmounts), per-step validation runs on "Continue", the last step is a read-only review, and the progress line is announced on change.

# Gotchas

  • <form novalidate> disables browser bubbles but NOT the validity API — exactly what you want for custom messages.
  • Disabled inputs are skipped on submit AND invisible to autofill/screen readers in some browsers — prefer readonly for locked-but-submitted values.
  • pattern is anchored (implicit ^…$) and silently fails on typos in the regex; always pair with a title/error message that states the format.
  • iOS zooms into inputs with font-size <16px — keep inputs ≥16px.
  • Numeric codes (postal, OTP): use inputmode="numeric" pattern="[0-9]*", NOT type="number" (which strips leading zeros and adds spinners).
  • Autofocus on page load disorients screen-reader users — reserve autofocus for single-purpose pages (search, login).