Ниже - настоящая форма регистрации и входа, работающая прямо в статье (через iframe, без отдельной публикации). Она никуда не отправляет данные - бэкенда нет, это учебный макет. Переключение между вкладками "Вход" и "Регистрация" работает вообще без JavaScript - только на CSS. Под формой - разбор по блокам: что за что отвечает, кусок кода этого приёма и ссылка на урок курса, где он разобран подробно.
В этом разборе
Показать полный код страницы (можно скопировать целиком)
<!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 & 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> переключает их как обычно.
<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 отмечен - найди среди следующих за ним соседей нужный и измени его".
#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) не просто для вида: браузер сам подключает нужную клавиатуру на телефоне и скрывает пароль звёздочками.
<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 с прозрачным цветом: обычная тень, только не серая и не смещённая, а ровным ореолом по всем сторонам сразу.
.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 символов. Браузер следит за этим сам - без единой строчки скрипта форма уже не даст отправить явно неверные данные.
<input
type="email"
required
placeholder="you@example.com"
>
<input
type="password"
required
minlength="6"
placeholder="Минимум 6 символов"
>
Подробнее: Встроенная валидация форм.
CSS: как это оформлено
Псевдокласс :invalid сам появляется у поля, если браузер считает его содержимое неверным. Добавка :not(:placeholder-shown) нужна, чтобы не подсвечивать пустое поле красным ещё до того, как пользователь вообще начал печатать - плейсхолдер виден только у пустого поля.
.auth-form input:invalid:not(:placeholder-shown) {
border-color: var(--danger);
}
Подробнее: Псевдоклассы: :hover, :focus, :nth-child и другие - тот же приём, что и в переключателе вкладок выше, только с другими псевдоклассами.
Чекбокс согласия и кнопка отправки
В форме регистрации чекбокс согласия обязателен - без него кнопка технически нажимается, но браузер не даст форме отправиться, как и с пустым email. Сама кнопка чуть "проседает" при нажатии - лишнее тактильное подтверждение клика.
HTML: что выводит эта разметка
<label> оборачивает и чекбокс, и текст - значит, кликабельна вся строка целиком, а не крошечный квадратик. Атрибут required на чекбоксе работает так же, как и на текстовых полях выше. Кнопка - это <button type="submit">, а не <input>: так проще стилизовать текст внутри неё.
<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) - на долю секунды становится чуть меньше и сразу возвращается обратно, имитируя нажатие настоящей кнопки.
.btn:hover { background: #c94a09; }
.btn:active { transform: scale(.97); }
Подробнее: Анимированные кнопки и ссылки: практика.
Сообщение об успехе на чистом JS
Раз бэкенда нет, при нажатии "Войти" или "Зарегистрироваться" данные некуда отправлять по-настоящему. Вместо этого скрипт перехватывает отправку, сам проверяет форму и просто показывает зелёное сообщение об успехе.
HTML: что выводит эта разметка
У формы стоит атрибут novalidate - он отключает всплывающие браузерные подсказки об ошибках (они всё равно появляются вручную через JS ниже), а разметка ошибок из блока про валидацию продолжает работать через CSS. Рядом с формой лежит пустой на вид <p> для сообщения об успехе.
<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.
.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-переходы и как они работают и Встроенная валидация форм.