HTML:
<form class="add-post" method="post" action="<?php echo admin_url('admin-post.php'); ?>" enctype="multipart/form-data">
<div class="form-steps" id="formSteps">
<button type="button" class="step is-active" data-step="0">
<span class="step-num">1</span><span class="step-title">Основное</span>
</button>
<button type="button" class="step" data-step="1">
<span class="step-num">2</span><span class="step-title">Параметры</span>
</button>
<button type="button" class="step" data-step="2">
<span class="step-num">3</span><span class="step-title">Цены</span>
</button>
<button type="button" class="step" data-step="3">
<span class="step-num">4</span><span class="step-title">Услуги</span>
</button>
<button type="button" class="step" data-step="4">
<span class="step-num">5</span><span class="step-title">Фото</span>
</button>
<button type="button" class="step" data-step="5">
<span class="step-num">6</span><span class="step-title">Размещение</span>
</button>
</div>
<section class="tab is-active" data-tab="0">
<label for="name">Имя <span class="required">*</span></label>
<input type="text" id="name" name="name" required minlength="1" maxlength="20" size="10" />
<label for="description">Описание <span class="required">*</span></label>
<textarea name="description" id="description" cols="30" rows="10" required></textarea>
<label for="phone">Номер телефона <span class="required">*</span></label>
<input type="text" id="phone" name="phone" required minlength="17" maxlength="17" size="10" />
<label for="whatsapp">WhatsApp</label>
<input type="text" id="whatsapp" name="whatsapp" minlength="11" maxlength="11" size="10" />
<label for="viber">Viber</label>
<input type="text" id="viber" name="viber" minlength="11" maxlength="11" size="10" />
<label for="telegram">Telegram ник</label>
<input type="text" id="telegram" name="telegram" minlength="3" maxlength="30" size="10" />
<label for="metro">Станция Метро <span class="required">*</span></label>
<select name="metro" id="metro" required>
<?php
$terms = get_terms([
'taxonomy' => 'metro',
'hide_empty' => false,
'orderby' => 'name',
'order' => 'ASC',
]);
if (! is_wp_error($terms)) {
foreach ($terms as $term) {
printf(
'<option value="%1$s">%2$s</option>',
$term->term_id,
esc_html($term->name),
);
}
}
?>
</select>
<label for="area">Район <span class="required">*</span></label>
<select name="area" id="area" required>
<?php
$terms = get_terms([
'taxonomy' => 'area',
'hide_empty' => false,
'orderby' => 'name',
'order' => 'ASC',
]);
if (! is_wp_error($terms)) {
foreach ($terms as $term) {
printf(
'<option value="%1$s">%2$s</option>',
$term->term_id,
esc_html($term->name),
);
}
}
?>
</select>
</section>
<section class="tab" data-tab="1">
...
</section>
<section class="tab" data-tab="2">
...
</section>
....
<input type="hidden" name="action" value="add_post">
<?php wp_nonce_field('add_post_nonce'); ?>
<div class="tab-actions">
<button type="button" class="btn-prev" id="btnPrev">Назад</button>
<button type="button" class="btn-next" id="btnNext">Следующий шаг</button>
<input type="submit" class="btn-submit" id="btnSubmit" name="submit" value="Опубликовать">
</div>
</form>
Внешний вид.
Первый шаг.

Второй шаг.

Последний шаг.

Валидация.
При переходе на следующий шаг, мы валидируем поля на текущем шаге, а не всю форму.

JavaScript
(function ($) {
$(document).ready(function () {
const $form = $('form.add-post');
const $tabs = $('.tab');
const $steps = $('#formSteps .step');
const $btnPrev = $('#btnPrev');
const $btnNext = $('#btnNext');
const $btnSubmit = $('#btnSubmit');
let current = 0;
function setActive(index) {
current = index;
$tabs.removeClass('is-active').eq(index).addClass('is-active');
$steps.each(function (i) {
$(this)
.toggleClass('is-active', i === index)
.toggleClass('is-done', i < index);
});
$btnPrev.toggle(index !== 0);
const isLast = index === ($tabs.length - 1);
$btnNext.toggle(!isLast);
$btnSubmit.toggle(isLast);
const stepsEl = document.getElementById('formSteps');
if (stepsEl) stepsEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function validateCurrentTab() {
const tabEl = $tabs.get(current);
if (!tabEl) return true;
const fields = tabEl.querySelectorAll('input, select, textarea');
for (const el of fields) {
if (el.disabled) continue;
if (!el.checkValidity()) {
el.reportValidity();
return false;
}
}
return true;
}
$btnNext.on('click', function () {
if (!validateCurrentTab()) return;
setActive(Math.min(current + 1, $tabs.length - 1));
});
$btnPrev.on('click', function () {
setActive(Math.max(current - 1, 0));
});
$steps.on('click', function () {
const target = Number($(this).data('step'));
if (Number.isNaN(target) || target === current) return;
if (target > current) {
if (!validateCurrentTab()) return;
if (target !== current + 1) return;
}
setActive(target);
});
$form.on('submit', function (e) {
const isLast = current === ($tabs.length - 1);
if (!isLast) {
e.preventDefault();
if (validateCurrentTab()) setActive(current + 1);
return;
}
if (!validateCurrentTab()) {
e.preventDefault();
}
});
// start
setActive(0);
});
})(jQuery);
Объяснение
По сути тут три механики: переключение табов, частичная валидация и контроль отправки формы.
1) Как переключаются табы
Вся смена шага идет через одну функцию setActive(index).
Что она делает:
- Показывает только нужный tab
$tabs.removeClass('is-active').eq(index).addClass('is-active');
2. Синхронизирует верхние “шаги”
.toggleClass('is-active', i === index) // текущий шаг
.toggleClass('is-done', i < index) // уже пройденные
То есть шаги просто получают классы, а внешний вид рисуется CSS-ом.
- Переключает кнопки внизу
Назадскрывается на первом табеСледующий шагпоказывается на всех, кроме последнегоОпубликоватьпоказывается только на последнем
Это делается через:
const isLast = index === ($tabs.length - 1);
$btnNext.toggle(!isLast);
$btnSubmit.toggle(isLast);
4. Подскролливает к шапке шагов, чтобы при переходе на следующий таб пользователь не оказывался “в середине” формы.
2) Как валидируется только часть формы (текущий таб)
Ключевой момент: validateCurrentTab() берет только текущую секцию:
const tabEl = $tabs.get(current);
const fields = tabEl.querySelectorAll('input, select, textarea');
И дальше проверяет только эти поля:
disabledпропускаются (важно, потому что у тебя куча полей включается/выключается чекбоксами)checkValidity()прогоняет стандартные HTML-правила (required, minlength, pattern и т.д.)reportValidity()показывает браузерное сообщение на первом проблемном поле и сразу останавливает проверку.
checkValidity() и reportValidity() это встроенные методы DOM для элементов формы (HTML5 constraint validation).
el.checkValidity()
Проверяет поле по его ограничениям (required,minlength,maxlength,min/max,pattern,type="email"и т.д.).
Возвращаетtrue/false. Ничего не показывает пользователю.el.reportValidity()
Делает то же самое, но если поле невалидно, браузер показывает стандартную подсказку/ошибку и фокусируется на поле.
Возвращаетtrue/false
То есть валидация “поштучно” и только внутри активного таба.
Почему это вообще работает с required в других табах:
- потому что при клике Next ты не отправляешь форму, а просто валидируешь текущий таб и переключаешься
- а при submit на не-последнем шаге submit перехватывается (см. ниже)
- плюс обычно на форме ставят
novalidate, чтобы браузер не пытался валидировать всё сразу при submit (Я не ставил).
3) Как контролируется отправка формы
Тут два сценария.
A) Нажали “Следующий шаг”
Это обычная кнопка type="button", она вообще не отправляет форму.
Код:
$btnNext.on('click', function () {
if (!validateCurrentTab()) return;
setActive(current + 1);
});
То есть “Next” = “проверь текущий таб → если ок, покажи следующий”.
B) Нажали “Опубликовать”
Это type="submit", браузер пытается отправить форму, и срабатывает обработчик:
$form.on('submit', function (e) {
const isLast = current === ($tabs.length - 1);
if (!isLast) {
e.preventDefault();
if (validateCurrentTab()) setActive(current + 1);
return;
}
if (!validateCurrentTab()) {
e.preventDefault();
}
});
Логика такая:
- Если submit случился НЕ на последнем шаге:
preventDefault()блокирует отправку- форма ведет себя как “Next”: валидирует текущий таб и перекидывает дальше
- это защита от случаев типа “пользователь нажал Enter в поле” или если где-то случайно появится submit-кнопка
- Если submit на последнем шаге:
- валидируется последний таб
- если он валиден,
preventDefault()не вызывается и форма реально уходит наadmin-post.php
Итого: реально форма отправляется только когда current = последний таб и текущий таб проходит проверку.
