การตกแต่งข้อความที่กำหนดเอง

ใช้พร็อพเพอร์ตี้ 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 จะย้ายจุดเริ่มต้นและจุดสิ้นสุดของเส้น ค่าบวกจะดึงปลายเข้าด้านใน ส่วนค่าลบจะดันปลายออกด้านนอก ค่าเดียวจะย้ายทั้ง 2 ปลาย ส่วน 2 ค่าจะตั้งค่าเริ่มต้นและสิ้นสุดแยกกัน เปอร์เซ็นต์หมายถึงขนาดในบรรทัดของกล่องที่ตกแต่ง

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

แสดงข้อความด้วยกล่องแทน

พร็อพเพอร์ตี้กล่อง 2 รายการจะตกแต่งข้อความโดยการขีดเส้นรอบข้อความแทนที่จะขีดเส้นใต้ข้อความ เพิ่มระยะห่างจากขอบให้ข้อความเพื่อให้เส้นขอบไม่สัมผัสกับข้อความ

/* 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 target ไม่รองรับ 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 ด้วยการเปลี่ยนสีหรือพื้นหลังแทน