Lab

Forms Lab

Eight form patterns — floating labels, validation states, custom inputs, range sliders, multi-step, search, and file uploads.

1. Basic Form — clean labels, rounded inputs, primary button
<form>
  <label>Name</label>
  <input type="text" placeholder="Ada Lovelace">

  <label>Email</label>
  <input type="email" placeholder="ada@example.com">

  <label>Role</label>
  <select>
    <option>Designer</option>
    <option>Engineer</option>
    <option>Student</option>
  </select>

  <label>Message</label>
  <textarea rows="4" placeholder="Say hi..."></textarea>

  <button class="primary" type="submit">Submit</button>
</form>
form {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

input, textarea, select {
  padding: 12px 14px;
  border: 1px solid #d4d4d8;
  border-radius: 8px;
}

input:focus {
  outline: none;
  border-color: #111;
}
2. Floating Label — label shrinks up on focus (peer selector trick)
<div class="field">
  <!-- placeholder=" " is critical for :placeholder-shown -->
  <input type="text" id="fname" placeholder=" ">
  <label for="fname">Full Name</label>
</div>
<div class="field">
  <input type="email" id="femail" placeholder=" ">
  <label for="femail">Email Address</label>
</div>
<div class="field">
  <input type="tel" id="fphone" placeholder=" ">
  <label for="fphone">Phone Number</label>
</div>
<button class="primary" type="submit">Continue</button>
.field { position: relative; }

.field input {
  padding: 22px 14px 10px; /* room for label inside */
}

.field label {
  position: absolute;
  top: 14px;
  left: 14px;
  color: #888;
  pointer-events: none;
  transition: all .2s;
}

/* Shrink label when input is focused OR has content */
input:focus + label,
input:not(:placeholder-shown) + label {
  top: 5px;
  font-size: 11px;
  color: #ec4899;
}
3. Custom Radio & Checkbox — fully styled, keyboard-accessible

Pick one — Subscription plan

Select interests

<h4>Pick one — Subscription plan</h4>
<label class="opt">
  <!-- Real input is hidden; .box.round is the visual -->
  <input type="radio" name="plan">
  <span class="box round"></span> Starter — free
</label>
<label class="opt">
  <input type="radio" name="plan" checked>
  <span class="box round"></span> Pro — $9/mo
</label>
<label class="opt">
  <input type="radio" name="plan">
  <span class="box round"></span> Team — $29/mo
</label>

<h4>Select interests</h4>
<label class="opt">
  <!-- Real input is hidden; .box is the visual -->
  <input type="checkbox" checked>
  <span class="box"></span> Design
</label>
<label class="opt">
  <input type="checkbox">
  <span class="box"></span> Development
</label>
<label class="opt">
  <input type="checkbox" checked>
  <span class="box"></span> Writing
</label>
<label class="opt">
  <input type="checkbox">
  <span class="box"></span> Photography
</label>
/* Hide the real input */
input[type="checkbox"], input[type="radio"] {
  display: none;
}

.box {
  width: 20px;
  height: 20px;
  border: 2px solid #d4d4d8;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Inner dot drawn with ::after, scaled to 0 by default */
.box::after {
  content: "";
  width: 10px;
  height: 10px;
  background: #fff;
  border-radius: 3px;
  transform: scale(0);
  transition: transform .15s;
}

/* Adjacent sibling combinator: when input is checked, style .box */
input:checked + .box {
  background: #ec4899;
  border-color: #ec4899;
}
input:checked + .box::after { transform: scale(1); }
4. Validation States — success, error, and helper messages
✓ Looks good — username is available
⚠ Please enter a valid email address
<div class="field success">
  <label>Username</label>
  <input type="text" value="ada_lovelace">
  <i class="ph-fill ph-check-circle state-icon"></i>
  <div class="msg">✓ Looks good — username is available</div>
</div>

<div class="field error">
  <label>Email</label>
  <input type="email" value="not-an-email">
  <i class="ph-fill ph-warning-circle state-icon"></i>
  <div class="msg">⚠ Please enter a valid email address</div>
</div>

<div class="field">
  <label>Website (optional)</label>
  <input type="url" placeholder="https://yoursite.com">
</div>

<button class="primary" type="submit">Create account</button>
.field.success input { border-color: #10b981; }
.field.success .msg   { color: #10b981; }
.field.success .state-icon { color: #10b981; }

.field.error input {
  border-color: #ef4444;
  background: #fef2f2;
}
.field.error .msg { color: #ef4444; }

/* Icon sits inside the input on the right */
.state-icon {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
}
5. Range Slider + Toggle Switch — styled with custom thumb + sliding pill
$2,500
High
<div>
  <div class="slider-row"><label>Budget</label><span class="amount">$2,500</span></div>
  <input type="range" min="0" max="10000" value="2500">
</div>
<div>
  <div class="slider-row"><label>Quality</label><span class="amount">High</span></div>
  <input type="range" min="0" max="100" value="80">
</div>

<div class="toggle">
  <label>Email notifications</label>
  <span class="switch">
    <input type="checkbox" checked>
    <span class="switch-slider"></span>
  </span>
</div>
<div class="toggle">
  <label>Dark mode</label>
  <span class="switch">
    <input type="checkbox">
    <span class="switch-slider"></span>
  </span>
</div>
<div class="toggle">
  <label>Auto-save</label>
  <span class="switch">
    <input type="checkbox" checked>
    <span class="switch-slider"></span>
  </span>
</div>
/* Style the range thumb (WebKit) */
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: linear-gradient(135deg, #ec4899, #8b5cf6);
  cursor: pointer;
}

/* Toggle switch — checkbox hack */
.switch { width: 46px; height: 26px; position: relative; }
.switch input { display: none; }
.switch-slider {
  position: absolute;
  inset: 0;
  background: #d4d4d8;
  border-radius: 999px;
}
.switch-slider::before {
  content: "";
  position: absolute;
  width: 20px; height: 20px;
  left: 3px; top: 3px;
  background: #fff;
  border-radius: 50%;
  transition: transform .2s;
}

/* When checked, slide the knob + color the track */
input:checked + .switch-slider { background: #10b981; }
input:checked + .switch-slider::before { transform: translateX(20px); }
6. Multi-Step Form — progress indicator + step-by-step fields
Step 3 of 4 About You
<div class="steps">
  <div class="step done"></div>
  <div class="step done"></div>
  <div class="step current"></div>
  <div class="step"></div>
</div>

<div class="step-label">
  <span>Step <strong>3</strong> of 4</span>
  <span>About You</span>
</div>

<form>
  <label>Company name</label>
  <input type="text" placeholder="Acme Inc.">

  <label>Team size</label>
  <select>
    <option>Just me</option>
    <option>2–10</option>
    <option>11–50</option>
    <option>50+</option>
  </select>

  <label>What brings you here?</label>
  <textarea rows="3"></textarea>

  <button type="button" class="primary">← Back</button>
  <button type="submit" class="primary">Next step →</button>
</form>
.steps {
  display: flex;
  gap: 8px;
}

.step {
  flex: 1;
  height: 6px;
  background: #e5e7eb;
  border-radius: 999px;
}

.step.done { background: #ec4899; }

/* Half-filled for current step */
.step.current {
  background: linear-gradient(90deg, #ec4899 50%, #e5e7eb 50%);
}
7. Search with Suggestions — icon inside input + dropdown list
  • Cards LabLab
  • Icon TutorialTutorial
  • Neumorphism LabLab
  • SVG LabLab
<div class="search">
  <i class="ph ph-magnifying-glass"></i>
  <input type="search" placeholder="Search components, labs, tutorials...">
</div>

<ul class="suggestions">
  <li><i class="ph ph-cube"></i> Cards Lab<span class="tag">Lab</span></li>
  <li><i class="ph ph-code"></i> Icon Tutorial<span class="tag">Tutorial</span></li>
  <li><i class="ph ph-palette"></i> Neumorphism Lab<span class="tag">Lab</span></li>
  <li><i class="ph ph-paint-brush"></i> SVG Lab<span class="tag">Lab</span></li>
</ul>
.search { position: relative; }

/* Push input text to the right of the icon */
.search input {
  padding-left: 44px;
  border-radius: 999px;
  background: #f4f4f5;
  border-color: transparent;
}

.search i {
  position: absolute;
  left: 16px;
  top: 50%;
  transform: translateY(-50%);
  color: #888;
}

.suggestions li {
  padding: 10px 16px;
  display: flex;
  gap: 10px;
  cursor: pointer;
}
.suggestions li:hover { background: #f9f9fa; }
8. File Upload — drag-and-drop zone with file preview list
sunset-draft.jpg 2.4 MB
brand-guide.pdf 8.1 MB
<!-- <label> wraps the input so whole area is clickable -->
<label class="drop">
  <div class="icon"><i class="ph-duotone ph-cloud-arrow-up"></i></div>
  <h4>Drop files here or click to browse</h4>
  <p>PNG, JPG, PDF up to 10MB</p>
  <input type="file" multiple>
</label>

<!-- preview list of selected files -->
<div class="files">
  <div class="file"><i class="ph-fill ph-file-image"></i> sunset-draft.jpg <span class="size">2.4 MB</span></div>
  <div class="file"><i class="ph-fill ph-file-pdf"></i> brand-guide.pdf <span class="size">8.1 MB</span></div>
</div>
.drop {
  border: 2px dashed #d4d4d8;
  border-radius: 14px;
  padding: 38px 20px;
  text-align: center;
  background: #fafafa;
  cursor: pointer;
  transition: all .2s;
}

.drop:hover {
  border-color: #ec4899;
  background: #fdf2f8;
}

/* Hide the real input — the label is the UI */
.drop input { display: none; }
9. All Input Types — every HTML5 input, select, textarea, and button, each with a label
Text inputs
Contact
Numbers & range
Dates & time
Pickers
Choices
Buttons & submit
<!-- All HTML5 input types pair with a <label for=""> -->

<label for="name">Name</label>
<input id="name" type="text">

<!-- Numbers & range -->
<input type="number" min="0" max="100">
<input type="range" min="0" max="100" value="50">

<!-- Date/time family -->
<input type="date">
<input type="time">
<input type="datetime-local">
<input type="month">
<input type="week">

<!-- Pickers -->
<input type="color" value="#ec4899">
<input type="file">

<!-- Autocomplete with datalist -->
<input list="frameworks">
<datalist id="frameworks">
  <option value="Astro">
  <option value="Next.js">
</datalist>

<!-- Choices -->
<select><option>Single</option></select>
<select multiple><option>Multi</option></select>
<input type="radio" name="plan">
<input type="checkbox">

<!-- Buttons -->
<button type="submit">Submit</button>
<button type="reset">Reset</button>
<button type="button">Plain</button>

<!-- Group related inputs with fieldset + legend -->
<fieldset>
  <legend>Contact info</legend>
  <!-- fields here -->
</fieldset>
/* Fieldset = visual group for related fields */
fieldset {
  border: 1px solid #e5e7eb;
  border-radius: 12px;
  padding: 16px 18px;
}

/* Legend sits on the border — nice accent */
legend {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: #ec4899;
  padding: 0 6px;
}

/* Range slider needs some special handling */
input[type="range"] {
  padding: 0;
  border: none;
  background: transparent;
}

/* Color input needs smaller padding */
input[type="color"] {
  padding: 2px;
  height: 36px;
}

/* <output> displays computed form values */
output {
  font-weight: 700;
  color: #ec4899;
  font-variant-numeric: tabular-nums;
}
10. Social Sign-Up — one-tap auth cuts onboarding friction
or sign up with email
<div class="social">
  <button class="sbtn google"><i class="ph-fill ph-google-logo"></i> Continue with Google</button>
  <button class="sbtn apple"><i class="ph-fill ph-apple-logo"></i> Continue with Apple</button>
  <button class="sbtn fb"><i class="ph-fill ph-facebook-logo"></i> Continue with Facebook</button>
</div>
<div class="divider">or sign up with email</div>
<label>Email</label>
<input type="email" placeholder="you@example.com">
<button class="primary" type="submit">Create account</button>
.sbtn {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  padding: 11px; border: 1px solid #d1d5db; border-radius: 10px;
}
/* "or" line with rules on both sides */
.divider { display: flex; align-items: center; gap: 12px; color: #9ca3af; }
.divider::before, .divider::after { content: ''; flex: 1; height: 1px; background: #e5e7eb; }
11. Success Screen — confirm the submission worked (press Send)

Message sent!

Thanks — we'll get back to you within a day.

<!-- wrapper toggles between form and success panel -->
<div class="signup" id="signup">
  <form class="signup-form">
    <p>Takes about a minute · we'll email you back.</p>
    <div class="field">
      <label>Name</label>
      <input type="text" placeholder="Your name">
    </div>
    <div class="field">
      <label>Message</label>
      <textarea rows="3" placeholder="How can we help?"></textarea>
    </div>
    <button class="primary" type="submit">Send message</button>
  </form>

  <!-- hidden until the form is submitted -->
  <div class="success">
    <div class="ring"></div>
    <h3>Message sent!</h3>
    <p>Thanks — we'll get back to you within a day.</p>
    <button class="again" type="button">Send another</button>
  </div>
</div>
/* swap the form for the success panel after submit */
.signup-form { display: grid; gap: 12px; }
.success { display: none; flex-direction: column; align-items: center; gap: 8px; text-align: center; }
.signup.sent .signup-form { display: none; }
.signup.sent .success { display: flex; }

.ring { width: 48px; height: 48px; border-radius: 50%;
  display: grid; place-items: center;
  background: #dcfce7; color: #16a34a; font-size: 26px; }
.primary { background: #ec4899; color: #fff; border: none;
  border-radius: 8px; padding: 10px 16px; cursor: pointer; }
.again { background: none; border: 1px solid #d1d5db;
  border-radius: 8px; padding: 8px 14px; cursor: pointer; }
// Grab the wrapper, the form, and the "send another" button.
const signup = document.getElementById('signup');
const form   = signup.querySelector('.signup-form');
const again  = signup.querySelector('.again');

// On submit: stop the page reload and reveal the success panel.
form.addEventListener('submit', e => {
  e.preventDefault();
  signup.classList.add('sent');   // CSS hides the form, shows .success
});

// "Send another" flips it back to the form.
again.addEventListener('click', () => {
  signup.classList.remove('sent');
});
12. Preset Suggestions — quick picks plus a custom option
$10 $25 $50 $100
$
<div class="chips">
  <button class="chip">$10</button>
  <button class="chip on">$25</button>  <!-- default -->
  <button class="chip">$50</button>
  <button class="chip">$100</button>
</div>
<div class="amount-wrap">
  <span class="cur">$</span>
  <input type="number" placeholder="Custom amount">
</div>
.chip {
  padding: 9px 16px; border: 1px solid #d1d5db;
  border-radius: 999px; cursor: pointer;
}
/* highlight the chosen preset */
.chip.on { background: #111827; color: #fff; border-color: #111827; }
13. Accordion Sections — break a long form into collapsible steps
1. Your details
2. Shipping address
3. Payment
<!-- native <details> — collapsible, no JS needed -->
<details class="acc" open>
  <summary><span class="ttl"><i class="ph-fill ph-check-circle done"></i> 1. Your details</span></summary>
  <div class="body"> … fields … </div>
</details>
<details class="acc">
  <summary><span class="ttl">2. Shipping address</span></summary>
  <div class="body"> … fields … </div>
</details>
<details class="acc">
  <summary><span class="ttl">3. Payment</span></summary>
  <div class="body"> … fields … </div>
</details>
.acc summary { cursor: pointer; list-style: none; padding: 14px 16px; }
/* +/− indicator that flips when open */
.acc summary::after { content: '+'; }
.acc[open] summary::after { content: '\2212'; }
/* green check marks a finished section */
.done { color: #16a34a; }
14. Review Screen — let people check & edit before submitting

Almost done — please review your details.

Contact

Edit
NameAda Lovelace
Emailada@example.com

Shipping

Edit
Address123 Main St, Annandale, VA
MethodStandard (3–5 days)

Payment

Edit
Card•••• 4242
<p>Almost done — please review your details.</p>
<div class="rev-sec">
  <div class="rev-head">
    <h4>Contact</h4>
    <a href="#">Edit</a>  <!-- jump back to that step -->
  </div>
  <div class="rev-row"><span class="k">Name</span><span class="v">Ada Lovelace</span></div>
  <div class="rev-row"><span class="k">Email</span><span class="v">ada@example.com</span></div>
</div>
<div class="rev-sec">
  <div class="rev-head"><h4>Shipping</h4><a href="#">Edit</a></div>
  <div class="rev-row"><span class="k">Address</span><span class="v">123 Main St, Annandale, VA</span></div>
  <div class="rev-row"><span class="k">Method</span><span class="v">Standard (3–5 days)</span></div>
</div>
<div class="rev-sec">
  <div class="rev-head"><h4>Payment</h4><a href="#">Edit</a></div>
  <div class="rev-row"><span class="k">Card</span><span class="v">•••• 4242</span></div>
</div>
.rev-sec { border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px 16px; }
.rev-head { display: flex; justify-content: space-between; }
.rev-row { display: flex; justify-content: space-between; padding: 4px 0; }
.rev-row .k { color: #6b7280; }  /* label muted, value bold */
15. Intro Microcopy — set expectations before the first field

This quick survey helps us tailor your account. There are no wrong answers, and you can change everything later.

~2 minutes 4 short questions Private
<!-- tell people what they're in for, up front -->
<div class="intro">
  <i class="ph-fill ph-info"></i>
  <p>This quick survey helps us tailor your account…</p>
</div>
<div class="meta">
  <span><i class="ph ph-clock"></i> ~2 minutes</span>
  <span><i class="ph ph-list-numbers"></i> 4 short questions</span>
  <span><i class="ph ph-lock-simple"></i> Private</span>
</div>
/* a friendly, low-pressure intro banner */
.intro {
  display: flex; gap: 12px;
  background: #f0f7ff; border: 1px solid #cfe5fb;
  border-radius: 10px; padding: 14px 16px;
}
.meta { display: flex; gap: 18px; font-size: 12px; color: #6b7280; }