Niestandardowe ozdobniki tekstu

Używaj natywnych właściwości text-decoration do stylizowania podkreśleń, nadkreśleń i przekreśleń, zamiast tworzyć je za pomocą obramowań, gradientów tła lub pseudoelementów. Natywna dekoracja podąża za tekstem w zawijanych wierszach, śledzi dane czcionki, pomija wydłużenia dolne glifów i jest dziedziczona przez elementy zagnieżdżone, więc nie trzeba jej ponownie implementować. Z technik opartych na polach, które opisujemy na końcu tego przewodnika, korzystaj tylko wtedy, gdy dekoracja musi obramowywać lub otaczać tekst, a nie biec wzdłuż niego.

Dekoracje tekstu dotyczą dowolnego tekstu, nie tylko linków. Jeśli podkreślony tekst nie jest linkiem, upewnij się, że nie można go z nim pomylić. Podczas dekorowania linku użyj rzeczywistego elementu <a href> wskazującego miejsce docelowe (dodaj target="_blank" rel="noopener", aby otworzyć nową kartę), a nie elementu href="javascript:void(0)" z modułem obsługi skryptu. Prawdziwy link zachowuje działanie kliknięcia środkowym przyciskiem, kopiowania linku i czytnika ekranu.

Stylizowanie linii

Ustaw linię, styl, kolor i grubość za pomocą skrótu text-decoration, a odstęp od tekstu za pomocą text-underline-offset. Umieść selektor w :where(), aby wartości domyślne nie miały żadnej specyficzności i były zastępowane przez późniejsze wartości bez konieczności używania właściwości niestandardowych.

: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 akceptuje te formy płatności: solid, double, dotted, dashed i wavy. text-underline-offset dotyczy tylko podkreśleń.

Punkty wyjścia do projektu:

  • Grubość: od 0.06em do 0.1em w przypadku tekstu głównego. W przypadku nagłówków i stanów wyróżnionych używaj grubszych linii (od 0.1em do 0.15em).
  • Przesunięcie: od około 0.15em do 0.25em. Linie kropkowane i faliste potrzebują nieco więcej miejsca niż linie ciągłe, więc zwiększ dla nich przesunięcie.
  • Kolor: currentColor dopasowuje kolor linii do tekstu. A softer mix of the text color that firms up on hover keeps long runs of links calm.
  • Styl: solid w przypadku linków, dotted w przypadku terminów i skrótów oraz wavy w przypadku adnotacji, takich jak błędy.

Określanie, gdzie wiersz ma zostać pominięty

Przerywanie linii

Domyślnie podkreślenia i nadkreślenia są przerywane wokół części glifów, które je przecinają, takich jak wydłużenia dolne. Ustaw text-decoration-skip-ink, aby wybrać działanie:

.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;
}
  • W przypadku tekstu w alfabecie łacińskim zachowaj auto i nie deklaruj go, ponieważ jest to ustawienie domyślne.
  • Używaj wartości all, gdy podkreślenie znajduje się na tyle blisko linii bazowej, że przecina glify ideograficzne, lub gdy tekst w różnych pismach musi wyglądać spójnie.
  • Użyj none, aby uzyskać bardzo grubą linię lub celowo dekoracyjną, w której przerwy wyglądałyby jak przerwy.

Pomiń spacje

text-decoration-skip-spaces określa, czy linia jest rysowana pod spacjami. Domyślna wartość start end pozostawia spacje na początku i na końcu bez dekoracji.

.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;
}

Nie deklaruj text-decoration-skip-spaces, chyba że dekoracja ma widocznie pokrywać dopełnienie (none) lub łamać się między każdym słowem (all). Zwykły tekst wbudowany, taki jak tekst linku, nie ma spacji na początku ani na końcu, więc deklaracja nic nie daje.

Przycinanie lub wydłużanie linii za pomocą wstawki

text-decoration-inset przesuwa początek i koniec linii. Wartość dodatnia powoduje przesunięcie końca do wewnątrz, a ujemna – na zewnątrz. Jedna wartość przesuwa oba końce, a 2 wartości ustawiają początek i koniec osobno. Wartości procentowe odnoszą się do rozmiaru inline dekorowanego pola.

/* 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); }

Aby po najechaniu kursorem wyświetlić podkreślenie bez obramowania i gradientu tła, przytnij koniec o 100% i przywróć go do 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;
  }
}

Rysowanie niestandardowej linii za pomocą pseudoelementu

Użyj pseudoelementu, gdy linia musi mieć kształt, którego nie można narysować za pomocą text-decoration, np. falisty, i odsłoń go przez przycięcie. Nie są potrzebne żadne dodatkowe znaczniki, więc link pozostaje zwykłym elementem <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 */
  }
}

Zamiast tego obramuj tekst polem

Dwie właściwości pola dekorują tekst, obrysowując go, a nie rysując wzdłuż niego linię. Dodaj do tekstu dopełnienie, aby obrys go nie dotykał.

/* 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 zastępuje tylko obramowanie. Tło nadal jest prostokątnym polem, więc nie polegaj na nim, aby wypełnić kształt.

Jeśli tylko rogi mają mieć inny kształt, np. ścięty, zaokrąglony lub z wycięciem, użyj corner-shape z border-radius. Dzięki temu tło i obramowanie będą podążać za rogami, a nieobsługiwane przeglądarki będą używać zwykłych zaokrąglonych rogów.

.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 */
}

Wybieranie techniki

  • text-decoration (linia, pomijanie atramentu, odstępy, wcięcie): linia powinna biec wzdłuż tekstu, zawijać się wraz z nim i być zgodna z parametrami czcionki. Jest to opcja domyślna.
  • Pseudoelement z border-shape i clip-path: linia ma niestandardowy kształt lub wymaga animacji rysowania. Wybierz tę opcję tylko wtedy, gdy text-decoration-style nie może uzyskać odpowiedniego wyglądu, ponieważ wymaga dodatkowego pozycjonowania.
  • background-clip: border-area: tekst musi mieć kontur z gradientem. Używaj zwykłego elementu border-color, gdy wystarczy jednolity kontur.
  • corner-shape: tylko rogi muszą mieć inny kształt, np. ścięty, zaokrąglony lub z wycięciem. Używaj go razem z atrybutem border-radius, który określa rozmiar rogu.
  • border-shape: kontur wymaga kształtu, którego border-radius nie może narysować, np. tagu lub strzałki. Używaj border-radius, gdy wystarczą zaokrąglone rogi.
  • Wstawka wyświetlana po najechaniu kursorem: warto dodać, gdy link wymaga dodatkowego efektu po najechaniu kursorem, oprócz zmiany koloru. Pomiń go w przypadku tekstu, który nie jest interaktywny.

Strategie zastępcze

Każda opcja zastępcza zachowuje czytelność tekstu i główne elementy dekoracyjne. Określ potrzebę w odniesieniu do celu podstawowego: jeśli obejmuje on przeglądarki bez właściwości, zastosuj pasującą kreację zastępczą.

Przeskakiwanie rysika all

Jeśli docelowa wersja Baseline nie obsługuje text-decoration-skip-ink: all, deklaracja jest ignorowana, a przeglądarka zachowuje auto. Wydłużenia dolne w alfabecie łacińskim są nadal pomijane. Tylko skrypty, których nie obejmuje auto, takie jak japoński, tracą pomijanie atramentu. Jest to łagodna degradacja, więc nie jest potrzebny żaden dodatkowy kod.

Pomiń spacje

Jeśli Twój cel Baseline nie obsługuje text-decoration-skip-spaces, przeglądarka używa własnego domyślnego sposobu obsługi spacji. Jest to łagodna degradacja: linia jest rysowana pod spacjami, których przeglądarka nie pomija.

Odcięcie

Jeśli Twój cel Baseline nie obsługuje text-decoration-inset, deklaracja jest ignorowana, więc link zachowuje normalne, w pełni widoczne podkreślenie, a przejście nie ma nic do animowania. To jest łagodna degradacja: efekt najechania kursorem znika, a link pozostaje podkreślony.

Obszar graniczny

Jeśli Twój cel Baseline nie obsługuje background-clip: border-area, deklaracja jest ignorowana, a tło jest malowane pod całym obramowaniem, wypełniając tekst, a nie tylko jego kontur. NIE używaj zwykłej jednolitej ramki, jeśli gradient jest częścią projektu. Zamiast tego odtwórz gradient i zastosuj background-clip: border-area tylko wewnątrz @supports.

Użyj warstwy nieprzezroczystej, gdy tło elementu jest jednolite. Pomaluj kolor powierzchni nad polem dopełnienia i gradient pod polem obramowania, aby gradient był widoczny tylko przez przezroczyste obramowanie:

.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 */
  }
}

Jeśli wnętrze musi pozostać przezroczyste, użyj zamaskowanego pseudoelementu. Pseudoelement zawiera gradient, a maska wycina wszystko oprócz pierścienia dopełnienia:

@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;
  }
}

Kształt obramowania

Jeśli Twój cel Baseline nie obsługuje border-shape, deklaracja jest ignorowana, a element zachowuje prostokątne obramowanie. Zaokrąglony prostokąt jest akceptowalnym rozwiązaniem zastępczym tylko wtedy, gdy kształt jest czysto dekoracyjny. Jeśli kształt ma znaczenie, np. jest tagiem, strzałką lub objaśnieniem, odtwórz go jak najdokładniej i zastosuj border-shape tylko wewnątrz @supports.

Narysuj kontur za pomocą przyciętego pseudoelementu. Zachowaj przezroczyste obramowanie, aby pole miało taki sam rozmiar jak w obsługiwanym przypadku, wypełnij pseudoelement kolorem konturu i wytnij go w pierścień za pomocą evenodd clip-path: najpierw kształt zewnętrzny, a następnie ten sam kształt wstawiony według szerokości obrysu.

.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%);
  }
}

Aby uzyskać wypełniony kształt, a nie kontur, umieść clip-path: polygon(...) z tymi samymi punktami bezpośrednio na elemencie.

W przypadku podkreślenia pseudoelementu narysuj linię prostą za pomocą border-block-end na .draw::after i zastosuj falę block-size, border i border-shape tylko wewnątrz @supports (border-shape: circle(50%)). Linia jest prosta, ale nadal się rysuje, ponieważ animacja clip-path nie zależy od kształtu.

clip-path przycina też pierścienie podświetlenia i box-shadow. Nie polegaj na obramowaniu przyciętego elementu, aby pokazać zaznaczenie. Zamiast tego użyj stylu :focus-visible ze zmianą koloru lub tła.