DEV Community

Cover image for Creating modern forms with form.fscss — pure CSS
FSCSS tutorial for FSCSS tutorial

Posted on • Originally published at invites.devtem.org

Creating modern forms with form.fscss — pure CSS

Floating labels. Inline validation. Custom checkboxes, radios, and a toggle switch. A gradient button with a press-down micro-interaction. Every bit of it below is CSS — no form library, no useState, no event listener wiring up a class toggle.

That's form.fscss — the module in the FSCSS ecosystem. Same philosophy each time: solve the hard visual problem once, ship it as importable mixins, let the browser do the actual work.

<script src="https://cdn.jsdelivr.net/npm/fscss@1.1.24/exec.min.js" defer></script>

<style>
@import((*) from form)

@form-root()
@form-group(.form-group)
@form-input(.form-input)
@form-label(.form-label)
@form-float(.form-group, .form-input, .form-label)
@form-checkbox(.form-checkbox)
@form-btn(.form-btn)
@form-btn-primary(.form-btn-primary)
</style>

<div class="form-group">
  <input class="form-input" type="text" placeholder=" ">
  <label class="form-label">Full name</label>
</div>

<label class="form-checkbox">
  <input type="checkbox" checked><span></span> I agree to the Terms
</label>

<button class="form-btn form-btn-primary">Create account</button>
Enter fullscreen mode Exit fullscreen mode

The two tricks doing all the work

Forms feel like they need JavaScript because most tutorials reach for it immediately. Two native CSS mechanisms cover almost everything a "modern" form needs.

Floating labels run entirely on :placeholder-shown. Give the input placeholder=" " — a literal space, not empty — and the browser now knows, purely in CSS, whether the field is empty and unfocused:

.form-input:focus + .form-label,
.form-input:not(:placeholder-shown) + .form-label {
  top: -9px;
  font-size: 11px;
  color: var(--form-accent);
}
Enter fullscreen mode Exit fullscreen mode

No state, no class toggling on keyup. The label just reacts to what the browser already knows about the input.

Checkboxes, radios, and the switch all use the classic checkbox-hack: the real <input> stays in the DOM (so it keeps native keyboard support and form submission) but is visually hidden, and a sibling <span> becomes the thing you actually see:

.form-checkbox input { opacity: 0; position: absolute; }
.form-checkbox input:checked + span::after {
  transform: rotate(45deg) scale(1); /* the checkmark, drawn from two borders */
}
Enter fullscreen mode Exit fullscreen mode

The checkmark itself isn't an icon font or an SVG — it's two border sides on a pseudo-element, rotated 45 degrees. Same trick powers the radio dot and the switch thumb slide, just with different shapes and transforms.

Validation

Add .is-error or .is-success straight to the field wrapper. One mixin call registers the rule; every field just reacts to its own class:

<div class="form-group is-error">
  <input class="form-input" type="email" placeholder=" ">
  <label class="form-label">Email address</label>
  <span class="form-help">Please enter a valid email</span>
</div>
Enter fullscreen mode Exit fullscreen mode

Your actual validation logic — regex, an API check, whatever — still needs real JS somewhere. But the presentation layer of "this field is wrong, here's why" needs nothing but toggling one class name. no state management for something.

Full example

<script src="https://cdn.jsdelivr.net/npm/fscss@1.1.24/exec.min.js" defer></script>

<style>
@import((*) from form)

@form-root()

body {
  min-height: 100vh;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--form-bg);
  font-family: 'Syne', sans-serif;
}

.card {
  width: 100%;
  max-width: 380px;
  background: #1c1a2e;
  border: 1px solid var(--form-border);
  border-radius: 20px;
  padding: 32px;
}

@form-group(.form-group)
@form-input(.form-input)
@form-label(.form-label)
@form-float(.form-group, .form-input, .form-label)
@form-help(.form-help)
@form-validation(.form-group)
@form-select(.form-select)
@form-select-arrow(.form-select-wrap)
@form-checkbox(.form-checkbox)
@form-switch(.form-switch)
@form-btn(.form-btn)
@form-btn-primary(.form-btn-primary)
</style>

<div class="card">
  <div class="form-group">
    <input class="form-input" type="text" placeholder=" ">
    <label class="form-label">Full name</label>
  </div>

  <div class="form-group is-success">
    <input class="form-input" type="text" placeholder=" " value="devtemple_creator">
    <label class="form-label">Username</label>
    <span class="form-help">Username is available</span>
  </div>

  <div class="form-select-wrap">
    <select class="form-select">
      <option>Creator</option>
      <option>Buyer</option>
    </select>
  </div>

  <label class="form-checkbox">
    <input type="checkbox" checked><span></span> I agree to the Terms
  </label>

  <button class="form-btn form-btn-primary" style="width:100%;">Create account</button>
</div>
Enter fullscreen mode Exit fullscreen mode

Every visual state in that form — resting, focused, filled, valid, invalid, checked, hovered, pressed — is handled by CSS pseudo-classes doing what they were always built to do. JavaScript's only job left is your logic: submit handling, async validation, whatever's actually specific to your product.

Try it

<script src="https://cdn.jsdelivr.net/npm/fscss@1.1.24/exec.min.js" defer></script>
<style>
  @import((*) from form)
</style>
Enter fullscreen mode Exit fullscreen mode

Full docs, token reference, and markup gotchas (input-before-label ordering, the placeholder=" " requirement, sibling-selector rules):

github.com/fscss-ttr/form.fscss


Top comments (0)