Dekorasi teks kustom

Gunakan properti text-decoration native untuk menata gaya garis bawah, garis atas, dan coretan, daripada memalsukannya dengan batas, gradien latar belakang, atau elemen pseudo. Dekorasi native mengikuti teks di seluruh baris yang digabungkan, melacak metrik font, melewati descender glyph, dan mewarisi melalui elemen bertingkat, sehingga tidak ada yang perlu diimplementasikan ulang. Gunakan teknik berbasis kotak di akhir panduan ini hanya jika dekorasi harus menguraikan atau menggabungkan teks, bukan berjalan di sepanjang teks.

Dekorasi teks berlaku untuk teks apa pun, bukan hanya link. Jika teks yang digarisbawahi bukan link, pastikan teks tersebut tidak disalahartikan sebagai link. Saat mendekorasi link, gunakan <a href> asli yang mengarah ke tujuan (tambahkan target="_blank" rel="noopener" untuk membuka tab baru), bukan href="javascript:void(0)" dengan pengendali skrip. Link yang sebenarnya mempertahankan perilaku klik tengah, salin link, dan pembaca layar.

Menata gaya garis

Tetapkan garis, gaya, warna, dan ketebalan dengan singkatan text-decoration, dan jarak dari teks dengan text-underline-offset. Gabungkan pemilih dalam :where() sehingga default memiliki spesifisitas nol dan penggantian berikutnya akan menang, tanpa memerlukan properti kustom.

: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 menerima solid, double, dotted, dashed, dan wavy. text-underline-offset hanya berlaku untuk garis bawah.

Titik awal untuk desain:

  • Ketebalan: sekitar 0.06em hingga 0.1em untuk teks isi. Gunakan ketebalan yang lebih besar (0.1em hingga 0.15em) untuk judul dan status yang ditekankan.
  • Offset: sekitar 0.15em hingga 0.25em. Garis putus-putus dan bergelombang memerlukan sedikit lebih banyak ruang daripada garis solid, jadi naikkan offset untuk garis tersebut.
  • Warna: currentColor membuat garis tetap cocok dengan teks. Perpaduan warna teks yang lebih lembut dan menguat saat kursor diarahkan ke link akan membuat link yang panjang terlihat tenang.
  • Gaya: solid untuk link, dotted untuk istilah dan singkatan, serta wavy untuk anotasi seperti error.

Mengontrol tempat baris dilewati

Tinta terputus-putus

Secara default, garis bawah dan garis atas terputus di sekitar bagian glyph yang melintasinya, seperti descender. Tetapkan text-decoration-skip-ink untuk memilih perilaku:

.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;
}
  • Pertahankan auto untuk teks Latin, dan jangan deklarasikan: ini adalah default.
  • Gunakan all jika garis bawah berada cukup dekat dengan garis dasar untuk melewati glif ideografis, atau jika teks skrip campuran harus terlihat konsisten.
  • Gunakan none untuk garis yang sangat tebal, atau garis yang sengaja dibuat dekoratif, yang celahnya akan terlihat seperti putus-putus.

Lewati ruang

text-decoration-skip-spaces menentukan apakah garis digambar di bawah spasi. Defaultnya, start end, membiarkan spasi di awal dan di akhir tidak didekorasi.

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

Jangan mendeklarasikan text-decoration-skip-spaces kecuali jika dekorasi harus terlihat menutupi padding (none) atau jeda di antara setiap kata (all). Teks inline biasa, seperti teks link, tidak memiliki spasi di awal atau di akhir, sehingga deklarasi tidak akan berpengaruh.

Memangkas atau memperpanjang garis dengan inset

text-decoration-inset memindahkan awal dan akhir garis. Nilai positif menarik ujung ke dalam, nilai negatif mendorongnya ke luar. Satu nilai memindahkan kedua ujung; dua nilai menetapkan awal dan akhir secara terpisah. Persentase mengacu pada ukuran inline kotak yang didekorasi.

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

Untuk menampilkan garis bawah saat kursor diarahkan tanpa batas atau gradien latar belakang, pangkas bagian akhir sebesar 100% dan transisikan kembali ke 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;
  }
}

Menggambar garis kustom dengan elemen pseudo

Gunakan elemen semu jika garis harus berupa bentuk yang tidak dapat digambar oleh text-decoration, seperti garis bergelombang, dan tampilkan dengan kliping. Tidak diperlukan markup tambahan, sehingga link tetap berupa <a> biasa.

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

Membuat garis batas teks dengan kotak

Dua properti kotak menghias teks dengan membuat garis batas di sekelilingnya, bukan membuat garis di sepanjang teks. Beri padding pada teks agar garis batas tidak menyentuhnya.

/* 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 hanya mengganti batas. Latar belakangnya masih berupa kotak persegi panjang, jadi jangan mengandalkannya untuk mengisi bentuk.

Jika hanya sudut yang memerlukan bentuk berbeda, seperti label miring, cekung, atau berlekuk, gunakan corner-shape dengan border-radius. Fitur ini membuat latar belakang dan batas mengikuti sudut, dan browser yang tidak didukung akan kembali ke sudut membulat biasa.

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

Memilih teknik

  • text-decoration (garis, ink skipping, spasi, inset): garis harus mengikuti teks, digabungkan dengan teks, dan mengikuti metrik font-nya. Ini adalah pilihan default.
  • Elemen semu dengan border-shape dan clip-path: garisnya adalah bentuk kustom atau memerlukan animasi gambar. Pilih hanya jika text-decoration-style tidak dapat menghasilkan tampilan, karena memerlukan pemosisian tambahan.
  • background-clip: border-area: teks memerlukan garis batas gradien. Gunakan border-color polos jika garis batas solid sudah cukup.
  • corner-shape: hanya sudut yang memerlukan bentuk berbeda, seperti miring, melengkung, atau berlekuk. Gabungkan dengan border-radius, yang menetapkan ukuran sudut.
  • border-shape: garis batas memerlukan bentuk yang tidak dapat digambar oleh border-radius, seperti tag atau panah. Gunakan border-radius jika sudut membulat sudah cukup.
  • Inset hover reveal: perlu ditambahkan saat link memerlukan affordance pengarahan kursor selain perubahan warna. Lewati untuk teks yang tidak interaktif.

Strategi Penggantian

Setiap penggantian membuat teks tetap dapat dibaca dan mempertahankan dekorasi inti. Sesuaikan kebutuhan dengan target Baseline Anda: jika target tersebut mencakup browser tanpa properti, terapkan penggantian yang sesuai.

Tinta terputus-putus all

Jika target Dasar Pengukuran Anda tidak mendukung text-decoration-skip-ink: all, deklarasi akan diabaikan dan browser akan mempertahankan auto. Descender Latin masih dilewati; hanya skrip yang tidak dicakup oleh auto, seperti bahasa Jepang, yang kehilangan fitur lewati tinta. Ini adalah degradasi halus, jadi tidak diperlukan kode tambahan.

Lewati ruang

Jika target Dasar pengukuran Anda tidak mendukung text-decoration-skip-spaces, browser akan menggunakan penanganan spasi defaultnya sendiri. Ini adalah degradasi halus: garis digambar di bawah spasi yang tidak dilewati browser.

Inset

Jika target Dasar Pengukuran Anda tidak mendukung text-decoration-inset, deklarasi akan diabaikan, sehingga link mempertahankan garis bawah normal yang terlihat sepenuhnya dan transisi tidak memiliki apa pun untuk dianimasikan. Ini adalah degradasi halus: pengungkapan saat kursor diarahkan hilang dan link tetap digarisbawahi.

Area perbatasan

Jika target Dasar Pengukuran Anda tidak mendukung background-clip: border-area, deklarasi akan diabaikan dan latar belakang akan digambar di bawah seluruh kotak batas, mengisi teks, bukan hanya garis tepinya. JANGAN gunakan batas solid polos jika gradien adalah bagian dari desain. Sebagai gantinya, buat ulang gradien, dan terapkan background-clip: border-area hanya di dalam @supports.

Gunakan lapisan buram jika latar belakang elemen adalah warna datar yang diketahui. Warnai warna permukaan di atas kotak padding dan gradien di bawah kotak batas, sehingga gradien hanya terlihat melalui batas transparan:

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

Gunakan elemen pseudo bertopeng jika bagian dalamnya harus tetap transparan. Elemen semu membawa gradien, dan mask memotong semuanya kecuali padding ring-nya:

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

Bentuk batas

Jika target Dasar Pengukuran Anda tidak mendukung border-shape, deklarasi akan diabaikan dan elemen akan mempertahankan batas persegi panjangnya. Persegi panjang dengan sudut membulat hanya dapat diterima sebagai pengganti jika bentuknya murni dekoratif. Jika bentuk memiliki makna, seperti tag, panah, atau info, buat ulang semirip mungkin, dan terapkan border-shape hanya di dalam @supports.

Gambar garis batas dengan elemen semu yang dipangkas. Pertahankan batas transparan agar kotak memiliki ukuran yang sama seperti dalam kasus yang didukung, isi elemen semu dengan warna garis batas, dan potong menjadi cincin dengan evenodd clip-path: bentuk luar, lalu bentuk yang sama disisipkan dengan lebar goresan.

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

Untuk bentuk yang terisi, bukan garis batas, tempatkan clip-path: polygon(...) dengan titik yang sama langsung pada elemen.

Untuk garis bawah pseudo-elemen, gambar garis lurus dengan border-block-end pada .draw::after, dan terapkan coretan block-size, border, dan border-shape hanya di dalam @supports (border-shape: circle(50%)). Garisnya lurus, tetapi masih digambar, karena animasi clip-path tidak bergantung pada bentuknya.

clip-path juga memotong cincin fokus dan box-shadow. Jangan mengandalkan garis batas pada elemen yang dipotong untuk menampilkan fokus; sebagai gantinya, beri gaya pada :focus-visible dengan perubahan warna atau latar belakang.