Forms

One .joy-form-group per field, with a .joy-label and a .joy-input inside.

Basic field

<div class="joy-form-group">
    <label class="joy-label">Email</label>
    <input type="email" class="joy-input" placeholder="you@email.com">
</div>

With an error

.joy-form-error is hidden by default — add .is-visible via JS when validation fails:

Password is too short.
<div class="joy-form-group">
    <label class="joy-label">Password</label>
    <input type="password" class="joy-input" value="123">
    <span class="joy-form-error is-visible">Password is too short.</span>
</div>

Checkbox and link row

Forgot password?
<div class="joy-form-row">
    <label class="joy-checkbox-label"><input type="checkbox"> Remember me</label>
    <a href="#" class="joy-form-link">Forgot password?</a>
</div>

Form banner

Check your inbox to confirm your account.
<div class="joy-form-banner">Check your inbox to confirm your account.</div>

Form footer

<p class="joy-form-footer">No account yet? <a href="#" class="joy-form-link">Sign up</a></p>

Multi-step form

Each step is a .joy-form-step — only the one with .is-active is visible. Switching steps is handled by hand in JS (toggling the class), paired with .joy-step-dots to show progress:

<div class="joy-step-dots">
    <span class="joy-step-dot is-active"></span>
    <span class="joy-step-dot"></span>
    <span class="joy-step-dot"></span>
</div>
<div class="joy-form-step is-active">
    <div class="joy-form-group">
        <label class="joy-label">Name</label>
        <input type="text" class="joy-input" placeholder="Your name">
    </div>
</div>
function goToStep(index) {
    document.querySelectorAll('.joy-form-step').forEach((el, i) => el.classList.toggle('is-active', i === index));
    document.querySelectorAll('.joy-step-dot').forEach((el, i) => el.classList.toggle('is-active', i === index));
}