Trang trí văn bản tuỳ chỉnh

Sử dụng các thuộc tính text-decoration gốc để tạo kiểu cho đường gạch dưới, đường gạch trên và đường gạch ngang chữ, thay vì giả mạo chúng bằng đường viền, dải chuyển màu nền hoặc phần tử giả. Trang trí gốc theo sau văn bản trên các dòng được gói, theo dõi các chỉ số của phông chữ, bỏ qua các phần tử giảm dần của ký tự và kế thừa thông qua các phần tử lồng nhau, vì vậy không cần phải triển khai lại bất kỳ phần tử nào trong số đó. Chỉ sử dụng các kỹ thuật dựa trên hộp ở cuối hướng dẫn này khi phần trang trí phải phác thảo hoặc bao bọc văn bản thay vì chạy dọc theo văn bản.

Trang trí văn bản áp dụng cho mọi văn bản, không chỉ các đường liên kết. Nếu văn bản được gạch chân không phải là đường liên kết, hãy đảm bảo rằng người dùng không nhầm lẫn văn bản đó với đường liên kết. Khi trang trí một đường liên kết, hãy sử dụng một <a href> thực tế trỏ đến đích đến (thêm target="_blank" rel="noopener" để mở thẻ mới), chứ không phải href="javascript:void(0)" có trình xử lý tập lệnh. Đường liên kết thực tế giữ lại hành vi nhấp chuột giữa, sao chép đường liên kết và trình đọc màn hình.

Tạo kiểu cho đường kẻ

Đặt đường kẻ, kiểu, màu và độ dày bằng thuộc tính viết tắt text-decoration, còn khoảng cách với văn bản bằng text-underline-offset. Gói bộ chọn trong :where() để các giá trị mặc định không có tính cụ thể và mọi cơ chế ghi đè sau này đều thắng mà không cần thuộc tính tuỳ chỉnh.

: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 chấp nhận solid, double, dotted, dashed và wavy. text-underline-offset chỉ áp dụng cho gạch dưới.

Điểm bắt đầu cho thiết kế:

  • Độ dày: khoảng 0.06em đến 0.1em cho văn bản nội dung. Sử dụng nét dày hơn (0.1em đến 0.15em) cho tiêu đề và trạng thái được nhấn mạnh.
  • Bù trừ: khoảng 0.15em đến 0.25em. Đường chấm và đường lượn sóng cần nhiều không gian hơn một chút so với đường liền, vì vậy, hãy tăng độ lệch cho các đường này.
  • Màu: currentColor giữ cho đường kẻ khớp với văn bản. Màu văn bản nhẹ nhàng hơn và đậm lên khi di chuột giúp các đường liên kết dài trông dễ chịu hơn.
  • Kiểu: solid cho đường liên kết, dotted cho thuật ngữ và từ viết tắt, và wavy cho chú thích như lỗi.

Kiểm soát vị trí bỏ qua dòng

Mực bị đứt nét

Theo mặc định, đường gạch dưới và đường gạch trên sẽ ngắt xung quanh các phần ký tự cắt ngang chúng, chẳng hạn như phần đuôi chữ. Đặt text-decoration-skip-ink để chọn hành vi:

.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;
}
  • Giữ nguyên auto cho văn bản Latinh và không khai báo: đây là giá trị mặc định.
  • Sử dụng all khi đường gạch dưới nằm đủ gần đường cơ sở để cắt ngang các ký tự tượng hình hoặc khi văn bản có nhiều hệ chữ viết cần phải nhất quán.
  • Sử dụng none cho đường kẻ rất dày hoặc đường kẻ trang trí có chủ đích, trong đó các khoảng trống sẽ trông giống như các điểm ngắt.

Bỏ qua không gian

text-decoration-skip-spaces quyết định xem đường kẻ có được vẽ dưới dấu cách hay không. Giá trị mặc định, start end, không trang trí cho các khoảng trắng ở đầu và cuối.

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

Đừng khai báo text-decoration-skip-spaces trừ phi phần trang trí phải che phủ khoảng đệm (none) hoặc ngắt giữa mỗi từ (all). Văn bản cùng dòng thông thường, chẳng hạn như văn bản của một đường liên kết, không có dấu cách ở đầu hoặc cuối, vì vậy, nội dung khai báo không có tác dụng gì.

Cắt bớt hoặc kéo dài đường kẻ bằng một phần lồng ghép

text-decoration-inset di chuyển điểm bắt đầu và điểm kết thúc của đường thẳng. Giá trị dương sẽ kéo một đầu vào trong, giá trị âm sẽ đẩy đầu đó ra ngoài. Một giá trị sẽ di chuyển cả hai đầu; hai giá trị sẽ đặt điểm bắt đầu và điểm kết thúc riêng biệt. Tỷ lệ phần trăm đề cập đến kích thước nội tuyến của hộp được trang trí.

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

Để hiển thị đường gạch dưới khi di chuột mà không có đường viền hoặc dải chuyển màu nền, hãy cắt bớt phần cuối 100% và chuyển về 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;
  }
}

Vẽ một đường tuỳ chỉnh bằng phần tử giả

Sử dụng phần tử giả khi đường kẻ phải là hình dạng mà text-decoration không thể vẽ, chẳng hạn như đường ngoằn ngoèo và hiển thị đường kẻ đó bằng cách cắt bớt. Bạn không cần thêm mã đánh dấu, vì vậy, đường liên kết vẫn là <a> thuần tuý.

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

Thay vào đó, hãy tạo đường viền cho văn bản bằng một hộp

Hai thuộc tính hộp trang trí văn bản bằng cách tạo đường viền thay vì chạy một đường dọc theo văn bản. Thêm khoảng đệm cho văn bản để đường viền không chạm vào văn bản.

/* 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 chỉ thay thế đường viền. Nền vẫn là hộp hình chữ nhật, vì vậy, đừng dựa vào nền để lấp đầy hình dạng.

Khi chỉ các góc cần có hình dạng khác, chẳng hạn như nhãn vát, nhãn lõm hoặc nhãn có khía, hãy sử dụng corner-shape với border-radius. Thuộc tính này giữ cho nền và đường viền theo các góc, còn các trình duyệt không được hỗ trợ sẽ quay lại các góc tròn thông thường.

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

Chọn một kỹ thuật

  • text-decoration (đường kẻ, bỏ qua mực, dấu cách, phần lồng ghép): đường kẻ phải chạy dọc theo văn bản, xuống dòng cùng với văn bản và tuân theo các chỉ số phông chữ của văn bản. Đây là lựa chọn mặc định.
  • Phần tử giả có border-shape và clip-path: đường kẻ là một hình dạng tuỳ chỉnh hoặc cần có ảnh động vẽ trên. Chỉ chọn thuộc tính này khi text-decoration-style không thể tạo ra giao diện, vì thuộc tính này cần thêm vị trí.
  • background-clip: border-area: văn bản cần có đường viền chuyển màu. Sử dụng border-color đơn giản khi chỉ cần một đường viền liền nét.
  • corner-shape: chỉ các góc mới cần hình dạng khác, chẳng hạn như vát, lõm hoặc khía. Kết hợp với border-radius để đặt kích thước góc.
  • border-shape: đường viền cần một hình dạng mà border-radius không thể vẽ, chẳng hạn như thẻ hoặc mũi tên. Sử dụng border-radius khi chỉ cần góc tròn.
  • Hiển thị khi di chuột vào phần lồng ghép: nên thêm khi một đường liên kết cần có khả năng di chuột ngoài việc thay đổi màu sắc. Bỏ qua đối với văn bản không tương tác.

Chiến lược dự phòng

Mỗi phương án dự phòng đều giúp văn bản dễ đọc và giữ lại thành phần trang trí cốt lõi. Xác định nhu cầu dựa trên mục tiêu Baseline: nếu mục tiêu đó bao gồm các trình duyệt không có thuộc tính, hãy áp dụng phương án dự phòng phù hợp.

Bỏ qua mực all

Nếu Baseline mục tiêu của bạn không hỗ trợ text-decoration-skip-ink: all, thì nội dung khai báo sẽ bị bỏ qua và trình duyệt sẽ giữ lại auto. Các nét kéo xuống trong chữ cái Latinh vẫn được bỏ qua; chỉ những chữ viết mà auto không bao gồm, chẳng hạn như tiếng Nhật, mới bị mất tính năng bỏ qua nét mực. Đây là tính năng xuống cấp nhẹ nên bạn không cần thêm mã.

Bỏ qua không gian

Nếu mục tiêu Baseline của bạn không hỗ trợ text-decoration-skip-spaces, thì trình duyệt sẽ sử dụng cách xử lý dấu cách mặc định của riêng mình. Đây là xuống cấp nhẹ: đường kẻ được vẽ dưới các dấu cách mà trình duyệt không bỏ qua.

Lồng ghép

Nếu mục tiêu Baseline của bạn không hỗ trợ text-decoration-inset, thì nội dung khai báo sẽ bị bỏ qua, vì vậy, đường liên kết sẽ giữ nguyên đường gạch dưới bình thường, hiển thị đầy đủ và hiệu ứng chuyển cảnh không có gì để tạo ảnh động. Đây là sự xuống cấp nhẹ: hiệu ứng di chuột qua để hiện nội dung bị mất và đường liên kết vẫn được gạch chân.

Khu vực biên giới

Nếu mục tiêu Baseline của bạn không hỗ trợ background-clip: border-area, thì nội dung khai báo sẽ bị bỏ qua và nền sẽ tạo điểm ảnh dưới toàn bộ hộp đường viền, tô màu nền văn bản thay vì chỉ bố cục của văn bản. ĐỪNG chọn đường viền đơn sắc khi dải chuyển màu là một phần của thiết kế. Thay vào đó, hãy tái tạo dải chuyển màu và chỉ áp dụng background-clip: border-area bên trong @supports.

Sử dụng một lớp mờ khi nền của phần tử là một màu phẳng đã biết. Tô màu nền cho hộp khoảng đệm và dải chuyển màu dưới hộp đường viền để dải chuyển màu chỉ xuất hiện qua đường viền trong suốt:

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

Sử dụng phần tử giả được che khi bên trong phải giữ nguyên độ trong suốt. Phần tử giả mang dải chuyển màu và một mặt nạ cắt bỏ mọi thứ trừ vòng đệm:

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

Hình dạng đường viền

Nếu Baseline mục tiêu của bạn không hỗ trợ border-shape, thì nội dung khai báo sẽ bị bỏ qua và phần tử sẽ giữ nguyên đường viền hình chữ nhật. Hình chữ nhật bo tròn chỉ là một phương án dự phòng được chấp nhận khi hình dạng chỉ mang tính trang trí. Khi hình dạng mang ý nghĩa, chẳng hạn như thẻ, mũi tên hoặc chú thích, hãy tái tạo hình dạng đó càng giống càng tốt và chỉ áp dụng border-shape bên trong @supports.

Vẽ đường viền bằng một phần tử giả được cắt bớt. Giữ đường viền trong suốt để hộp có cùng kích thước như trong trường hợp được hỗ trợ, tô màu đường viền cho phần tử giả và cắt phần tử giả thành một vòng bằng evenodd clip-path: hình dạng bên ngoài, sau đó là cùng một hình dạng được lồng vào theo chiều rộng nét vẽ.

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

Đối với hình dạng được tô màu thay vì đường viền, hãy đặt clip-path: polygon(...) có cùng điểm trực tiếp trên phần tử.

Đối với phần tử giả gạch chân, hãy vẽ một đường thẳng có border-block-end trên .draw::after, đồng thời chỉ áp dụng đường lượn sóng block-size, border và border-shape bên trong @supports (border-shape: circle(50%)). Đường thẳng nhưng vẫn vẽ tiếp vì ảnh động clip-path không phụ thuộc vào hình dạng.

clip-path cũng cắt bớt vòng tròn tiêu điểm và box-shadow. Đừng dựa vào đường viền trên phần tử bị cắt để cho thấy tiêu điểm; thay vào đó, hãy tạo kiểu cho :focus-visible bằng cách thay đổi màu hoặc nền.