Собственные стили оформления текста

Используйте нативные свойства text-decoration для стилизации подчеркиваний, надчеркиваний и зачеркиваний, вместо того чтобы имитировать их с помощью границ, фоновых градиентов или псевдоэлементов. Встроенное оформление следует за текстом по перенесенным строкам, отслеживает метрики шрифта, пропускает нижние выносные элементы глифов и наследуется через вложенные элементы, поэтому ничего из этого не нужно реализовывать заново. Используйте методы на основе блоков, описанные в конце этого руководства, только когда декоративный элемент должен очерчивать или обтекать текст, а не идти вдоль него.

Оформление текста применяется к любому тексту, а не только к ссылкам. Если подчеркнутый текст не является ссылкой, убедитесь, что его нельзя с ней перепутать. При декорировании ссылки используйте настоящий элемент <a href>, указывающий на целевую страницу (добавьте target="_blank" rel="noopener", чтобы открыть новую вкладку), а не href="javascript:void(0)" с обработчиком скрипта. Настоящая ссылка поддерживает нажатие средней кнопкой мыши, копирование ссылки и работу программы чтения с экрана.

Как настроить стиль линии

Задайте стиль, цвет и толщину линии с помощью сокращения text-decoration, а расстояние от текста – с помощью text-underline-offset. Оберните селектор в :where(), чтобы значения по умолчанию имели нулевую специфичность и любое последующее переопределение имело приоритет без необходимости использования пользовательских свойств.

:where(.decorated) {
  /* Thickness is relative to the font size (0.08em is an example value), so the line is not too heavy
     on small text or too light on large text, and max() keeps it at least 1px so it never disappears. */
  text-decoration: underline solid currentColor max(1px, 0.08em);
  /* Use em so the gap scales with the font size. 0.2em clears most descenders; example value. */
  text-underline-offset: 0.2em;
}

/* Example hover state: a softer line that firms up on hover and focus. The 50% mix and
   0.12em thickness are example values. */
:where(.decorated) {
  text-decoration-color: color-mix(in srgb, currentColor, transparent);
}

:where(.decorated:is(:hover, :focus-visible)) {
  text-decoration-color: currentColor;
  text-decoration-thickness: max(1px, 0.12em);
}

/* Other lines are a different text-decoration-line on the same properties */
.decorated-over    { text-decoration-line: overline; }
.decorated-through { text-decoration-line: line-through; }

text-decoration-style принимает solid, double, dotted, dashed и wavy. text-underline-offset применяется только к подчеркиванию.

Отправные точки для дизайна:

  • Толщина: от 0.06em до 0.1em для основного текста. Для заголовков и выделенных состояний используйте более толстые линии (0.1em–0.15em).
  • Смещение: от 0.15em до 0.25em. Пунктирным и волнистым линиям нужно больше места, чем сплошным, поэтому для них нужно увеличить смещение.
  • Цвет. currentColor сохраняет цвет линии, соответствующий тексту. Более мягкий цвет текста, который становится ярче при наведении указателя, позволяет не перегружать страницу ссылками.
  • Стиль. solid для ссылок, dotted для терминов и сокращений и wavy для аннотаций, например ошибок.

Как управлять пропуском строк

Пропуски чернил

По умолчанию подчеркивание и надчеркивание прерываются в местах пересечения с элементами глифов, например выносными элементами. Задайте для параметра text-decoration-skip-ink одно из следующих значений:

.decorated {
  /* auto: skip ink for most scripts (default). none: draw straight through.
     all: also skip ink in scripts where auto does not, such as Japanese or Chinese. */
  text-decoration-skip-ink: all;
}
  • Для латиницы оставьте auto и не объявляйте его, так как это значение по умолчанию.
  • Используйте all, когда подчеркивание находится достаточно близко к базовой линии, чтобы пересекать идеографические глифы, или когда текст на разных языках должен выглядеть единообразно.
  • Используйте none для очень толстых или декоративных линий, где пробелы будут выглядеть как разрывы.

Пропустить чат-группы

text-decoration-skip-spaces определяет, будет ли линия рисоваться под пробелами. По умолчанию используется значение start end, при котором начальные и конечные пробелы не оформляются.

.decorated {
  /* Spaces collapse by default, so there is nothing to skip. Preserve them when the
     leading or trailing spaces are meaningful, such as padded input values. */
  white-space: pre;
  /* none: decorate every space. all: skip every space, so each word is underlined separately.
     start | end | start end: skip only leading, trailing, or both. */
  text-decoration-skip-spaces: all;
}

Не объявляйте text-decoration-skip-spaces, если только оформление не должно визуально покрывать отступы (none) или разрывы между словами (all). Обычный встроенный текст, например текст ссылки, не имеет начальных или конечных пробелов, поэтому объявление ничего не делает.

Обрезать или продлить линию с помощью вставки

text-decoration-inset перемещает начало и конец строки. Положительное значение сдвигает конец внутрь, отрицательное – наружу. Одно значение перемещает оба конца, а два значения задают начало и конец отдельно. Проценты относятся к размеру строки декорированного поля.

/* Small insets soften the ends of the line; larger ones read as a deliberately short line.
   Both values are examples. */
.trimmed  { text-decoration-inset: var(--deco-inset, 0.1em); }
.extended { text-decoration-inset: calc(var(--deco-inset, 0.1em) * -1); }

Чтобы при наведении указателя мыши появлялось подчеркивание без рамки или фонового градиента, обрежьте конец на 100% и верните его к 0:

.reveal {
  text-decoration: underline; /* Always visible, including for reduced motion */
}

/* Only hide and animate the line when the user has not asked for reduced motion. */
@media (prefers-reduced-motion: no-preference) {
  .reveal {
    /* Trim the end by the full inline size so the line starts with zero length. */
    text-decoration-inset: 0 100%;
    /* 150ms to 300ms feels responsive without lagging; ease-out starts quickly. Example values. */
    transition: text-decoration-inset var(--reveal-duration, 0.25s) ease-out;
  }

  /* Include :focus-visible so keyboard users get the same reveal as pointer users. */
  .reveal:is(:hover, :focus-visible) {
    text-decoration-inset: 0 0;
  }
}

Как нарисовать собственную линию с помощью псевдоэлемента

Используйте псевдоэлемент, когда линия должна быть такой формы, которую нельзя нарисовать с помощью text-decoration, например волнистой, и покажите ее, используя обрезку. Дополнительная разметка не требуется, поэтому ссылка остается обычной <a>.

<a class="draw" href="#">Hover to draw underline</a>
.draw {
  position: relative;
  display: inline-block;
  color: inherit;
  text-decoration: none;
  padding-block-end: 0.4em; /* Space for the line below the text; example value */
}

.draw::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  block-size: 0.4em; /* Height of the squiggle; example value */
  pointer-events: none;
  border: var(--deco-thickness, max(1px, 0.08em)) solid var(--deco-color, currentColor);
  /* A single shape strokes along its path, using the border width as the stroke. Example squiggle. */
  border-shape: shape(from 0% 50%,
    curve to 20% 50% with 5% 0% / 15% 100%,
    smooth to 40% 50% with 35% 100%,
    smooth to 60% 50% with 55% 100%,
    smooth to 80% 50% with 75% 100%,
    smooth to 100% 50% with 95% 100%);
}

/* Only hide and animate the line when the user has not asked for reduced motion;
   otherwise it stays fully drawn. */
@media (prefers-reduced-motion: no-preference) {
  .draw::after {
    /* Clip the line away from the right edge. The negative block insets keep the stroke
       from being cut off vertically. */
    clip-path: inset(-1em 100% -1em 0);
    /* A slightly slower draw than a hover reveal reads as drawing; example duration. */
    transition: clip-path var(--draw-duration, 0.6s) ease-out;
  }

  .draw:is(:hover, :focus-visible)::after {
    clip-path: inset(-1em 0 -1em 0); /* Fully drawn */
  }
}

Выделять текст рамкой

Два свойства блока оформляют текст, обводя его, а не проводя вдоль него линию. Добавьте отступы, чтобы текст не касался контура.

/* Gradient outline with a see-through middle */
.ring {
  padding: 0.2em 0.75em; /* Example values: enough room that the outline does not touch the text */
  /* The border width is the visible outline thickness. Relative to the font size, with a 2px
     minimum so it stays visible on small text; example value. */
  border: var(--ring-width, max(2px, 0.12em)) solid transparent;
  background: linear-gradient(135deg, var(--ring-start, #2563eb), var(--ring-end, #db2777)) border-box;
  /* Paint the background only under the border, so the inside stays transparent. */
  background-clip: border-area;
}

/* Border that follows a shape that border-radius cannot draw, such as a tag with a pointed end */
.tag {
  padding: 0.2em 1.5em 0.2em 0.9em; /* Example values: extra room at the point so the text is not clipped */
  border: var(--shape-stroke, max(2px, 0.08em)) solid var(--shape-color, currentColor); /* Example stroke width */
  /* A single shape strokes along its path. Two shapes fill the area between them.
     This polygon is an example; use shape() for curves. */
  border-shape: polygon(0 0, 85% 0, 100% 50%, 85% 100%, 0 100%);
}

border-shape – заменяет только границу. Фон по-прежнему представляет собой прямоугольное поле, поэтому не полагайтесь на него для заполнения фигуры.

Если нужно изменить только углы, например сделать их скошенными, закругленными или с выемками, используйте corner-shape с border-radius. В этом случае фон и границы будут следовать за углами, а в неподдерживаемых браузерах будут использоваться обычные закругленные углы.

.label {
  padding: 0.2em 0.9em; /* Example values */
  border: var(--shape-stroke, max(2px, 0.08em)) solid var(--shape-color, currentColor);
  border-radius: 0.6em; /* Corner size; example value */
  corner-shape: bevel; /* round (default) | scoop | bevel | notch | square | squircle */
}

Выберите технику

  • text-decoration (линия, пропуск чернил, пробелы, вставка): линия должна проходить вдоль текста, переноситься вместе с ним и соответствовать его шрифту. Этот вариант выбран по умолчанию.
  • Псевдоэлемент с border-shape и clip-path: линия имеет нестандартную форму или требует анимации рисования. Выбирайте его, только если text-decoration-style не позволяет добиться нужного вида, поскольку требуется дополнительное позиционирование.
  • background-clip: border-area: тексту нужен градиентный контур. Используйте обычный border-color, если достаточно сплошного контура.
  • corner-shape – только углы должны иметь другую форму, например скошенную, закругленную или с выемкой. Используйте его вместе с border-radius, чтобы задать размер угла.
  • border-shape: для контура требуется фигура, которую border-radius не может нарисовать, например тег или стрелка. Используйте border-radius, если достаточно скругленных углов.
  • Внутреннее выделение при наведении. Стоит добавить, если для ссылки нужно что-то большее, чем просто изменение цвета при наведении. Не добавляйте его для неинтерактивного текста.

Резервные стратегии

Каждый резервный вариант сохраняет читаемость текста и основное оформление. Определите, что вам нужно, исходя из целевого набора браузеров Baseline. Если в него входят браузеры, не поддерживающие свойство, примените соответствующий резервный вариант.

Пропуск чернил all

Если целевой браузер Baseline не поддерживает text-decoration-skip-ink: all, объявление игнорируется и браузер сохраняет auto. Выносные элементы латиницы по-прежнему пропускаются. Пропуск чернил отключается только для письменностей, которые не охватывает auto, например для японской. Это постепенное сокращение, поэтому дополнительный код не требуется.

Пропустить чат-группы

Если целевой набор Baseline не поддерживает text-decoration-skip-spaces, браузер использует собственную обработку пробелов по умолчанию. Это постепенное сокращение: линия проводится под пробелами, которые браузер не пропускает.

Внутри

Если ваша цель Baseline не поддерживает text-decoration-inset, объявление игнорируется, поэтому ссылка сохраняет свое обычное, полностью видимое подчеркивание, а переход не имеет ничего для анимации. Это постепенное сокращение: раскрытие при наведении теряется, а ссылка остается подчеркнутой.

Приграничный регион

Если целевой Baseline не поддерживает background-clip: border-area, объявление игнорируется и фон закрашивается под всем полем границы, заполняя текст, а не только его контур. НЕ используйте простую сплошную рамку, если в дизайне предусмотрен градиент. Вместо этого воспроизведите градиент и примените background-clip: border-area только внутри @supports.

Используйте непрозрачный слой, если фон элемента имеет известный плоский цвет. Закрасьте поле отступа цветом поверхности, а поле границы – градиентом, чтобы градиент был виден только через прозрачную границу:

.ring {
  border: var(--ring-width, max(2px, 0.12em)) solid transparent; /* Example width */
  /* Fallback: an opaque surface layer hides the gradient inside the border. */
  background:
    linear-gradient(var(--surface, canvas) 0 100%) padding-box,
    linear-gradient(135deg, var(--ring-start, #2563eb), var(--ring-end, #db2777)) border-box;
}

@supports (background-clip: border-area) {
  .ring {
    background: linear-gradient(135deg, var(--ring-start, #2563eb), var(--ring-end, #db2777)) border-box;
    background-clip: border-area; /* The inside stays transparent */
  }
}

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

@supports not (background-clip: border-area) {
  .ring {
    position: relative;
    border-color: transparent;
  }

  .ring::before {
    content: "";
    position: absolute;
    inset: calc(var(--ring-width, max(2px, 0.12em)) * -1); /* Cover the border box */
    padding: var(--ring-width, max(2px, 0.12em)); /* Ring thickness */
    border-radius: inherit;
    background: linear-gradient(135deg, var(--ring-start, #2563eb), var(--ring-end, #db2777));
    /* Keep only the area between the content box and the border box. */
    mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    mask-composite: exclude;
    pointer-events: none;
  }
}

Форма границы

Если целевая версия Baseline не поддерживает border-shape, объявление игнорируется и элемент сохраняет прямоугольную границу. Прямоугольник со скругленными углами можно использовать только в качестве резервного варианта, когда форма носит чисто декоративный характер. Если фигура имеет значение, например это тег, стрелка или выноска, воспроизведите ее как можно точнее и примените border-shape только внутри @supports.

Нарисуйте контур с помощью обрезанного псевдоэлемента. Оставьте прозрачную границу, чтобы поле было того же размера, что и в поддерживаемом случае, заполните псевдоэлемент цветом контура и вырежьте его в кольцо с помощью evenodd clip-path: внешняя фигура, затем та же фигура, вставленная по ширине обводки.

.tag {
  --stroke: max(2px, 0.08em); /* Example stroke width */
  position: relative;
  padding: 0.2em 1.5em 0.2em 0.9em; /* Example values */
  border: var(--stroke) solid transparent;
}

@supports not (border-shape: circle(50%)) {
  .tag::before {
    content: "";
    position: absolute;
    inset: calc(var(--stroke) * -1); /* Cover the border box */
    background: var(--shape-color, currentColor);
    pointer-events: none;
    /* Outer shape, then the inner shape inset by the stroke. The inner points are approximate on
       diagonal edges, so the stroke looks slightly thinner there. */
    clip-path: polygon(evenodd,
      0 0, 85% 0, 100% 50%, 85% 100%, 0 100%,
      var(--stroke) var(--stroke),
      calc(85% - var(--stroke)) var(--stroke),
      calc(100% - var(--stroke) * 2) 50%,
      calc(85% - var(--stroke)) calc(100% - var(--stroke)),
      var(--stroke) calc(100% - var(--stroke)));
  }
}

@supports (border-shape: circle(50%)) {
  .tag {
    border-color: var(--shape-color, currentColor);
    border-shape: polygon(0 0, 85% 0, 100% 50%, 85% 100%, 0 100%);
  }
}

Чтобы получить закрашенную фигуру, а не контур, добавьте clip-path: polygon(...) с теми же точками непосредственно в элемент.

Для подчеркивания псевдоэлемента нарисуйте прямую линию с помощью border-block-end в .draw::after и примените волнистую линию block-size, border и border-shape только внутри @supports (border-shape: circle(50%)). Линия прямая, но все равно рисуется, потому что анимация clip-path не зависит от формы.

clip-path также обрезает кольца фокуса и box-shadow. Не полагайтесь на контур обрезанного элемента, чтобы показать фокус. Вместо этого используйте для :focus-visible изменение цвета или фона.