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">

  <label>Message</label>
  <textarea rows="4"></textarea>

  <button 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>
.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

<label class="opt">
  <!-- Real input is hidden; .box is the visual -->
  <input type="checkbox">
  <span class="box"></span>
  Design
</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">
  <i class="ph-fill ph-check-circle state-icon"></i>
  <div class="msg">Username is available</div>
</div>

<div class="field error">
  <!-- same structure, different class -->
</div>
.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
<input type="range" min="0" max="10000" value="2500">

<span class="switch">
  <input type="checkbox" checked>
  <span class="switch-slider"></span>
</span>
/* 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>

<form><!-- fields for current step --></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...">
</div>

<ul class="suggestions">
  <li>Cards Lab</li>
  <li>Icon Tutorial</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>
.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;
}