Форма регистрации и входа на CSS: мини-проект без JavaScript

Ниже - настоящая форма регистрации и входа, работающая прямо в статье (через iframe, без отдельной публикации). Она никуда не отправляет данные - бэкенда нет, это учебный макет. Переключение между вкладками "Вход" и "Регистрация" работает вообще без JavaScript - только на CSS. Под формой - разбор по блокам: что за что отвечает, кусок кода этого приёма и ссылка на урок курса, где он разобран подробно.

Показать полный код страницы (можно скопировать целиком)
forma-demo.html
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Форма регистрации и входа - демо</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@200;400;600;700;800&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
  :root {
    --paper: #faf6f0;
    --ink: #1c1a17;
    --dim: #7a7468;
    --accent: #e8590c;
    --line: #e4ddd0;
    --danger: #d64545;
    --ok: #2fae5b;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0;
    background: var(--paper);
    color: var(--ink);
    font-family: 'Manrope', 'Segoe UI', Arial, sans-serif;
    display: flex;
    justify-content: center;
    padding: 54px 20px;
  }
 
  .auth-page { width: 100%; max-width: 400px; }
 
  .brand { text-align: center; margin-bottom: 26px; }
  .brand-mark {
    display: block;
    font-family: 'JetBrains Mono', monospace;
    font-weight: 700;
    font-size: 14px;
    letter-spacing: .06em;
    color: var(--accent);
    text-transform: uppercase;
  }
  .brand-tag { display: block; margin-top: 6px; font-size: 13px; color: var(--dim); }
 
  .auth-card {
    background: #fff;
    border: 1px solid var(--line);
    border-radius: 20px;
    padding: 8px;
    box-shadow: 0 20px 44px rgba(28, 26, 23, .08);
    overflow: hidden;
  }
 
  /* hidden radios drive the whole tab switch below via CSS only */
  .tab-radio { display: none; }
 
  .tab-switch {
    position: relative;
    display: flex;
    background: var(--paper);
    border-radius: 14px;
    margin: 6px;
  }
  .tab-label {
    flex: 1;
    text-align: center;
    padding: 13px 10px;
    font-size: 12.5px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--dim);
    cursor: pointer;
    position: relative;
    z-index: 1;
    transition: color .25s ease;
  }
  .tab-indicator {
    position: absolute;
    top: 3px;
    bottom: 3px;
    left: 3px;
    width: calc(50% - 3px);
    background: #fff;
    border-radius: 11px;
    box-shadow: 0 4px 10px rgba(28, 26, 23, .08);
    transition: transform .3s ease;
  }
 
  #tab-register:checked ~ .tab-switch .tab-indicator { transform: translateX(100%); }
  #tab-login:checked ~ .tab-switch label[for="tab-login"] { color: var(--ink); }
  #tab-register:checked ~ .tab-switch label[for="tab-register"] { color: var(--ink); }
 
  .auth-form {
    display: none;
    flex-direction: column;
    padding: 6px 18px 22px;
  }
  .form-login { display: flex; }
  #tab-register:checked ~ .form-register { display: flex; }
  #tab-register:checked ~ .form-login { display: none; }
 
  .auth-form label {
    font-size: 12.5px;
    font-weight: 600;
    color: var(--dim);
    margin-top: 12px;
  }
  .auth-form input[type="email"],
  .auth-form input[type="password"],
  .auth-form input[type="text"] {
    margin-top: 6px;
    padding: 11px 13px;
    width: 100%;
    border: 1px solid var(--line);
    border-radius: 10px;
    font-size: 14px;
    font-family: inherit;
    color: var(--ink);
    background: var(--paper);
    transition: border-color .2s ease, box-shadow .2s ease;
  }
  .auth-form input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px rgba(232, 89, 12, .14);
  }
  .auth-form input:invalid:not(:placeholder-shown) {
    border-color: var(--danger);
  }
 
  .checkbox-row {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    font-weight: 500;
    color: var(--ink);
    margin-top: 16px;
    cursor: pointer;
  }
  .checkbox-row input { width: auto; margin: 0; }
 
  .btn {
    margin-top: 20px;
    padding: 13px;
    border: none;
    border-radius: 10px;
    background: var(--accent);
    color: #fff;
    font-size: 14px;
    font-weight: 700;
    cursor: pointer;
    transition: transform .15s ease, background .2s ease;
  }
  .btn:hover { background: #c94a09; }
  .btn:active { transform: scale(.97); }
 
  .success-msg {
    margin: 0;
    padding: 0 18px;
    max-height: 0;
    overflow: hidden;
    opacity: 0;
    color: var(--ok);
    font-size: 13.5px;
    font-weight: 600;
    text-align: center;
    transition: max-height .35s ease, opacity .35s ease, padding .35s ease;
  }
  .success-msg.show {
    max-height: 60px;
    opacity: 1;
    padding: 0 18px 20px;
  }
 
  @media (prefers-reduced-motion: reduce) {
    * { transition-duration: .001ms !important; animation-duration: .001ms !important; }
  }
</style>
</head>
<body>
 
<main class="auth-page">
 
  <div class="brand">
    <span class="brand-mark">HTML &amp; CSS</span>
    <span class="brand-tag">Демо-форма для практики вёрстки</span>
  </div>
 
  <div class="auth-card">
 
    <input type="radio" name="tab" id="tab-login" class="tab-radio" checked>
    <input type="radio" name="tab" id="tab-register" class="tab-radio">
 
    <div class="tab-switch">
      <label class="tab-label" for="tab-login">Вход</label>
      <label class="tab-label" for="tab-register">Регистрация</label>
      <span class="tab-indicator"></span>
    </div>
 
    <form class="auth-form form-login" novalidate>
      <label for="login-email">Email</label>
      <input type="email" id="login-email" placeholder="you@example.com" required>
 
      <label for="login-password">Пароль</label>
      <input type="password" id="login-password" placeholder="Минимум 6 символов" minlength="6" required>
 
      <label class="checkbox-row">
        <input type="checkbox">
        Запомнить меня
      </label>
 
      <button type="submit" class="btn">Войти</button>
    </form>
 
    <form class="auth-form form-register" novalidate>
      <label for="reg-name">Имя</label>
      <input type="text" id="reg-name" placeholder="Как вас зовут" required>
 
      <label for="reg-email">Email</label>
      <input type="email" id="reg-email" placeholder="you@example.com" required>
 
      <label for="reg-password">Пароль</label>
      <input type="password" id="reg-password" placeholder="Минимум 6 символов" minlength="6" required>
 
      <label class="checkbox-row">
        <input type="checkbox" required>
        Согласен с условиями использования
      </label>
 
      <button type="submit" class="btn">Зарегистрироваться</button>
    </form>
 
    <p class="success-msg" id="success-msg">Готово! Это демо-форма - данные никуда не отправлены.</p>
 
  </div>
 
</main>
 
<script>
  (function () {
    var forms = document.querySelectorAll('.auth-form');
    var msg = document.getElementById('success-msg');
    var hideTimer;
 
    forms.forEach(function (form) {
      form.addEventListener('submit', function (e) {
        e.preventDefault();
 
        if (!form.checkValidity()) {
          form.reportValidity();
          return;
        }
 
        msg.classList.add('show');
        clearTimeout(hideTimer);
        hideTimer = setTimeout(function () {
          msg.classList.remove('show');
          form.reset();
        }, 2600);
      });
    });
  })();
</script>
 
</body>
</html>

Переключатель вкладок без JavaScript

Клик по вкладке "Регистрация" переключает форму и сдвигает белую плашку-индикатор - и всё это без единой строчки скрипта. Секрет в двух спрятанных радиокнопках: <label> связаны с ними через for, а всё остальное реагирует на их состояние через CSS.

HTML: что выводит эта разметка

Радиокнопки type="radio" с одинаковым name - значит, включена может быть только одна из двух. Они спрятаны стилем display: none, но остаются в разметке и продолжают работать - клик по подписанному <label> переключает их как обычно.

HTML
<input type="radio" name="tab" id="tab-login" checked>
<input type="radio" name="tab" id="tab-register">
 
<div class="tab-switch">
  <label for="tab-login">Вход</label>
  <label for="tab-register">Регистрация</label>
  <span class="tab-indicator"></span>
</div>
 
<form class="form-login">...</form>
<form class="form-register">...</form>

Подробнее: select, textarea и checkbox/radio.

CSS: как это оформлено

Всё держится на трёх вещах сразу: псевдоклассе :checked (сработавшая радиокнопка), комбинаторе ~ ("соседний элемент дальше по коду") и селекторе по атрибуту label[for="tab-register"] (нужная подпись среди одинаковых <label>). Вместе они читаются как "если этот radio отмечен - найди среди следующих за ним соседей нужный и измени его".

CSS
#tab-register:checked ~ .tab-switch .tab-indicator {
  transform: translateX(100%);
}
#tab-register:checked ~ .tab-switch label[for="tab-register"] {
  color: var(--ink);
}
#tab-register:checked ~ .form-register { display: flex; }
#tab-register:checked ~ .form-login { display: none; }

Подробнее: Комбинаторы: потомок, дочерний, соседние элементы, Псевдоклассы: :hover, :focus, :nth-child и другие и Селекторы по атрибуту: [attr] и [attr=value].

Поля ввода и подписи

У каждого поля - своя подпись сверху и подсказка-плейсхолдер внутри. При клике в поле граница подсвечивается акцентным цветом с мягким свечением вокруг - это подсказывает, куда сейчас идёт ввод.

HTML: что выводит эта разметка

<label for="login-email"> связан с полем по его id - клик по подписи ставит курсор в поле, а скринридер озвучивает подпись при фокусе. Тип поля (email, password) не просто для вида: браузер сам подключает нужную клавиатуру на телефоне и скрывает пароль звёздочками.

HTML
<label for="login-email">Email</label>
<input
  type="email"
  id="login-email"
  placeholder="you@example.com"
  required
>
 
<label for="login-password">Пароль</label>
<input
  type="password"
  id="login-password"
  placeholder="Минимум 6 символов"
  minlength="6"
  required
>

Подробнее: Поля ввода: input и его типы и label, placeholder и доступность форм.

CSS: как это оформлено

Свечение вокруг поля в фокусе - это не граница, а box-shadow с прозрачным цветом: обычная тень, только не серая и не смещённая, а ровным ореолом по всем сторонам сразу.

CSS
.auth-form input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(232, 89, 12, .14);
}

Подробнее: Тени: box-shadow.

Проверка полей без JavaScript

Если ввести не-email в поле почты или пароль короче 6 символов, поле подсвечивается красным - и всё это ещё до всякого JavaScript, только атрибуты HTML и одно правило CSS.

HTML: что выводит эта разметка

Атрибут required запрещает пустое поле, type="email" заставляет браузер проверить формат адреса, а minlength="6" требует минимум 6 символов. Браузер следит за этим сам - без единой строчки скрипта форма уже не даст отправить явно неверные данные.

HTML
<input
  type="email"
  required
  placeholder="you@example.com"
>
<input
  type="password"
  required
  minlength="6"
  placeholder="Минимум 6 символов"
>

Подробнее: Встроенная валидация форм.

CSS: как это оформлено

Псевдокласс :invalid сам появляется у поля, если браузер считает его содержимое неверным. Добавка :not(:placeholder-shown) нужна, чтобы не подсвечивать пустое поле красным ещё до того, как пользователь вообще начал печатать - плейсхолдер виден только у пустого поля.

CSS
.auth-form input:invalid:not(:placeholder-shown) {
  border-color: var(--danger);
}

Подробнее: Псевдоклассы: :hover, :focus, :nth-child и другие - тот же приём, что и в переключателе вкладок выше, только с другими псевдоклассами.

Чекбокс согласия и кнопка отправки

В форме регистрации чекбокс согласия обязателен - без него кнопка технически нажимается, но браузер не даст форме отправиться, как и с пустым email. Сама кнопка чуть "проседает" при нажатии - лишнее тактильное подтверждение клика.

HTML: что выводит эта разметка

<label> оборачивает и чекбокс, и текст - значит, кликабельна вся строка целиком, а не крошечный квадратик. Атрибут required на чекбоксе работает так же, как и на текстовых полях выше. Кнопка - это <button type="submit">, а не <input>: так проще стилизовать текст внутри неё.

HTML
<label class="checkbox-row">
  <input type="checkbox" required>
  Согласен с условиями использования
</label>
 
<button type="submit" class="btn">
  Зарегистрироваться
</button>

Подробнее: select, textarea и checkbox/radio и Кнопки: button vs input type=submit.

CSS: как это оформлено

При наведении фон кнопки чуть темнеет, а в момент нажатия (:active) кнопка сжимается свойством transform: scale(.97) - на долю секунды становится чуть меньше и сразу возвращается обратно, имитируя нажатие настоящей кнопки.

CSS
.btn:hover { background: #c94a09; }
.btn:active { transform: scale(.97); }

Подробнее: Анимированные кнопки и ссылки: практика.

Сообщение об успехе на чистом JS

Раз бэкенда нет, при нажатии "Войти" или "Зарегистрироваться" данные некуда отправлять по-настоящему. Вместо этого скрипт перехватывает отправку, сам проверяет форму и просто показывает зелёное сообщение об успехе.

HTML: что выводит эта разметка

У формы стоит атрибут novalidate - он отключает всплывающие браузерные подсказки об ошибках (они всё равно появляются вручную через JS ниже), а разметка ошибок из блока про валидацию продолжает работать через CSS. Рядом с формой лежит пустой на вид <p> для сообщения об успехе.

HTML
<form class="auth-form" novalidate>
  ...
</form>
 
<p class="success-msg" id="success-msg">
  Готово! Это демо-форма...
</p>

Подробнее: Тег form и как отправляются данные.

CSS + JS: как это оформлено

JavaScript отменяет реальную отправку (e.preventDefault()), сам проверяет форму методом checkValidity() - тем же самым, что использует браузер внутри для :invalid из блока выше - и либо показывает подсказки об ошибке (reportValidity()), либо добавляет класс show. Дальше всю анимацию появления и исчезновения делает CSS-переход по max-height и opacity.

CSS + JS
.success-msg {
  max-height: 0;
  opacity: 0;
  transition: max-height .35s ease, opacity .35s ease, padding .35s ease;
}
.success-msg.show {
  max-height: 60px;
  opacity: 1;
  padding: 0 18px 20px;
}
 
form.addEventListener('submit', function (e) {
  e.preventDefault();
 
  if (!form.checkValidity()) {
    form.reportValidity();
    return;
  }
 
  msg.classList.add('show');
  setTimeout(function () {
    msg.classList.remove('show');
    form.reset();
  }, 2600);
});

Подробнее: Что такое CSS-переходы и как они работают и Встроенная валидация форм.

Категория
Поделиться в социальных сетях

Отправить комментарий

0 Комментарии

Отправить комментарий (0)

#buttons=(Хорошо) #days=(20)

Мы используем файлы cookie для улучшения работы сайта. Подробнее
Хорошо
×