Пара тегов <fieldset> и <legend> чаще всего вспоминается в контексте форм: ими принято группировать связанные поля и подписывать группу — например, «Адрес доставки» над набором из четырёх инпутов. Подробнее про эту прямую роль — в посте про советы по разметке форм. Но у этой пары есть особенность, ради которой её иногда тащат и за пределы форм: <legend> — единственный элемент в HTML, который браузер рендерит так, как будто он «разрезает» верхнюю границу <fieldset> и встаёт прямо в линию рамки.

Никаких position: absolute, никаких ::before с белым фоном поверх линии — это нативное поведение элемента. Из этого факта вырастает целое семейство декоративных эффектов: заголовки с линиями по бокам, подписанные рамки, многоугольные обводки с текстом. Разберём, как этот механизм устроен и где он реально полезен (а где — ломает семантику).

Почему <legend> ведёт себя не как обычный inline-элемент

Если открыть DevTools и посмотреть на дефолтные стили <fieldset> в Chrome, видно характерное:

fieldset {
  display: block;
  margin-inline: 2px;
  padding-block: 0.35em 0.625em;
  padding-inline: 0.75em;
  border: 2px groove rgb(192, 192, 192);
  min-inline-size: min-content;
}

А у <legend> — ничего особенного. Магия не в стилях, а в layout-движке: когда фрейм fieldset считает высоту своей верхней границы, он резервирует место под legend и «вырезает» ровно ту полоску пикселей, которую legend занимает по ширине. Никакой другой HTML-элемент так не умеет.

Управлять положением подписи можно следующими способами:

  • margin на самом <legend> — сдвигает текст вдоль границы влево или вправо. margin-inline: auto центрирует, margin-inline-start: 24px прижимает почти к углу.
  • padding на <legend> — задаёт «воздух» вокруг текста, то есть длину разрыва границы.
  • border и background на самом <fieldset> — решают, какой стороной рамка вообще существует, поэтому большая часть рецептов крутится именно вокруг них.

Этого достаточно, чтобы собрать четыре нетривиальных эффекта.

Рецепт 1: заголовок с линиями по бокам

Один из самых частых паттернов в дизайне: разделитель в виде надписи «РАЗДЕЛ» с уходящими в стороны линиями. Обычно его собирают через ::before/::after с background-color или через flex с двумя пустыми div. На <fieldset> то же самое получается из четырёх строчек CSS:

<fieldset class="divider">
  <legend>Раздел</legend>
</fieldset>
.divider {
  inline-size: 320px;
  block-size: 0;
  border: 1px solid transparent;
  border-block-start-color: #4a5568;
  padding: 0;
}
.divider legend {
  margin-inline: auto;
  padding-inline: 12px;
  color: #4a5568;
  font-size: 14px;
  text-transform: uppercase;
  letter-spacing: 2px;
}

Идея в трёх трюках. Во-первых, у рамки прозрачный border по всему периметру, и только border-block-start-color — видимый. Из четырёх сторон рамки остаётся одна. Во-вторых, block-size: 0 схлопывает высоту самого fieldset — линия становится одиночной горизонталью. В-третьих, margin-inline: auto центрирует подпись по этой линии, а её собственный padding-inline создаёт нужный разрыв в обводке.

Никаких флекс-обёрток, никаких пустых div-ов — одна семантическая пара тегов, и тот же визуал, что обычно собирают тремя элементами.

Рецепт 2: подпись на смещённой стороне рамки

Если рамка нужна целиком, а подпись хочется сдвинуть в произвольное место верхней линии — работает то же margin-inline-start:

.card {
  inline-size: 280px;
  padding: 24px;
  border: 2px solid #3182ce;
  border-radius: 8px;
}
.card legend {
  margin-inline-start: 16px;
  padding-inline: 8px;
  color: #3182ce;
  font-weight: 600;
}

Поведение margin-inline-start здесь логическое: на LTR-страницах подпись прижимается к левому углу, на RTL — к правому. Старое margin-left сработает не хуже, но потеряет автоматическую адаптацию под направление текста. Подробности про логические свойства — в мануале по логическим свойствам.

Рецепт 3: рамка-полигон с подписью на каждой стороне

Здесь начинается то, ради чего эту пару тегов в принципе вспоминают за пределами форм. Если нужна квадратная обводка, у которой на каждой из четырёх сторон своя подпись, обычно собирают четыре линии через четыре отдельных ::before/::after на двух разных элементах — и упираются в потолок (псевдоэлементов всего два на элемент). С fieldset эта задача решается прямолинейно: четыре <fieldset>, каждый рисует только верхнюю линию своей рамки, и три из них повёрнуты на 90deg, 180deg и -90deg.

<div class="polygon">
  <fieldset><legend>CSS</legend></fieldset>
  <fieldset><legend>HTML</legend></fieldset>
  <fieldset><legend>JavaScript</legend></fieldset>
  <fieldset><legend>TypeScript</legend></fieldset>
</div>
.polygon {
  position: relative;
  inline-size: 280px;
  block-size: 280px;
}
.polygon fieldset {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: 0;
  border: 8px solid transparent;
  border-block-start-color: #2d3748;
}
.polygon legend {
  margin-inline: auto;
  padding-inline: 12px;
  color: #2d3748;
  font-weight: 600;
}
.polygon fieldset:nth-of-type(2) { transform: rotate(90deg); }
.polygon fieldset:nth-of-type(3) { transform: rotate(180deg); }
.polygon fieldset:nth-of-type(3) legend { transform: rotate(180deg); }
.polygon fieldset:nth-of-type(4) { transform: rotate(-90deg); }

Логика тут такая: каждый <fieldset> накладывается поверх остальных через inset: 0, видит только своя верхняя граница, а transform: rotate() отправляет эту границу на нужную сторону квадрата. Подпись третьего fieldset (нижняя сторона) приходится отдельно перевернуть на 180 градусов, иначе текст пойдёт «вверх ногами».

Эффект, который сложно повторить псевдоэлементами, без оверхеда из восьми вспомогательных тегов.

Рецепт 4: бегущая подпись по верхней границе

Раз позицией legend управляет CSS-свойство margin, его можно анимировать — и подпись поедет вдоль рамки. Маленький, но запоминающийся эффект, например, для блока «Что нового»:

.ticker {
  inline-size: 360px;
  block-size: 80px;
  border: 1px solid #cbd5e0;
}
.ticker legend {
  padding-inline: 12px;
  background: #fff;
  color: #2b6cb0;
  font-weight: 600;
  animation: slide 6s ease-in-out infinite alternate;
}
@keyframes slide {
  from { margin-inline-start: 16px; }
  to   { margin-inline-start: 240px; }
}

Полный интерактивный пример с полигональной рамкой и бегущей подписью — в CodePen-демо к статье (ссылка добавится после публикации).

Анимация работает в любом браузере с поддержкой @keyframes — то есть в любом живом. Стоит заранее учесть пользователей с prefers-reduced-motion и обнулять анимацию для них — правило хорошего тона для любых движущихся элементов интерфейса.

Когда такой приём ломает семантику

Прежде чем тащить <fieldset> в каждую вторую секцию страницы, стоит вспомнить, для чего этот тег был придуман.

Согласно спецификации HTML, <fieldset> — это группа элементов управления формой, а <legend>подпись для этой группы. Скрин-ридеры обрабатывают пару особым образом: при фокусе на любом инпуте внутри группы озвучивается текст legend как часть имени поля. Это полезное поведение для радиокнопок и чекбоксов, где без контекста группы вопрос становится бессмысленным («Маленький»? Маленький что?).

Если использовать пару чисто декоративно — пустой <fieldset> без формы внутри — для скрин-ридера это превратится в «группа: Раздел, конец группы»: лишний шум, никак не связанный с контентом. Несколько правил, чтобы декоративное применение не ухудшало доступность:

  • Если внутри fieldset действительно есть группа полей — всё хорошо, используем смело, это и есть прямое назначение тега.
  • Если fieldset нужен только ради визуального разделителя из Рецепта 1 — лучше всё-таки взять <hr> с псевдоэлементами или div с flex-разметкой. <hr> семантически читается как тематический разрыв, что для разделителя правильней.
  • Если очень хочется fieldset вне формы (например, ради Рецепта 3 c полигональной рамкой и подписями) — накрыть его role="presentation" и убрать у legend семантику тоже. Тогда скрин-ридер прочитает только текст подписей, без «группа: …».
<fieldset role="presentation" class="polygon-side">
  <legend>CSS</legend>
</fieldset>

Альтернативные подходы

Декоративное поведение <fieldset>/<legend> — не единственный способ получить текст внутри рамки. Стоит знать и другие варианты.

  • Псевдоэлементы и flex. Для горизонтальной линии с заголовком посередине (Рецепт 1) минимальный вариант — display: flex на родителе и ::before/::after с flex: 1 и border-block-start. Семантически чище, элемент остаётся обычным <h2>, который и так у страницы есть.
  • border-image с SVG-маской. Полигональную рамку с подписями (Рецепт 3) можно сделать через нарисованный SVG-фон и одну обычную <div>. Это требует один раз нарисовать SVG, но даёт полный контроль над стилизацией линий и позволяет рамкам быть произвольной формы, не только квадрату.
  • CSS Grid. Для двух-трёхпозиционных подписей в рамке (например, угол и центр) можно собрать сетку из grid-template-areas, у которой только нужные ячейки имеют видимые границы. Так делают cheat-sheet-обложки в дизайн-системах.

Базовая мысль: пара fieldset/legend выигрывает там, где нужен один приём из четырёх рецептов выше с минимумом разметки. Как только в дизайне появляется хотя бы один нестандартный элемент — градиент в линии рамки, неровные углы, отзывчивая раскладка подписей — почти всегда выгоднее переезжать на flex с псевдоэлементами или на SVG.

Итог

Уникальное свойство пары <fieldset> и <legend> — нативная возможность встроить текст прямо в линию рамки. Без псевдоэлементов и абсолютного позиционирования получаются четыре характерных эффекта: разделитель с надписью посередине, карточка со сдвинутой подписью, многоугольная обводка с подписью на каждой стороне и анимированная бегущая подпись. У этого приёма есть цена: <fieldset> остаётся семантическим элементом формы, и его декоративное использование требует либо role="presentation", либо отказа в пользу <hr>, flex с псевдоэлементами или SVG-рамок. Когда визуал перевешивает — рецепты в статье экономят разметку; когда важна чистая семантика — на сцену выходят альтернативы.