맞춤 텍스트 장식

테두리, 배경 그라데이션 또는 가상 요소로 밑줄, 윗줄, 취소선을 가짜로 만드는 대신 네이티브 text-decoration 속성을 사용하여 스타일을 지정하세요. 네이티브 장식은 줄바꿈된 줄에서 텍스트를 따르고, 글꼴의 측정항목을 추적하고, 글리프 디센더를 건너뛰고, 중첩된 요소를 통해 상속되므로 다시 구현할 필요가 없습니다. 장식이 텍스트를 따라 흐르는 것이 아니라 텍스트의 윤곽을 그리거나 텍스트를 감싸야 하는 경우에만 이 가이드의 끝에 있는 상자 기반 기법을 사용하세요.

텍스트 장식은 링크뿐만 아니라 모든 텍스트에 적용됩니다. 밑줄이 그어진 텍스트가 링크가 아닌 경우 링크로 오인되지 않도록 합니다. 링크를 꾸밀 때는 스크립트 핸들러가 있는 href="javascript:void(0)"가 아닌, 도착 페이지를 가리키는 실제 <a href>를 사용하세요 (새 탭을 열려면 target="_blank" rel="noopener" 추가). 실제 링크는 마우스 가운데 버튼 클릭, 링크 복사, 스크린 리더 동작을 유지합니다.

선 스타일 지정

text-decoration 약칭을 사용하여 선, 스타일, 색상, 두께를 설정하고 text-underline-offset를 사용하여 텍스트와의 간격을 설정합니다. 선택기를 :where()로 래핑하여 기본값의 특수성이 0이 되고 이후의 재정의가 맞춤 속성 없이 적용되도록 합니다.

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

장식이 패딩 (none)을 눈에 띄게 덮거나 모든 단어 사이에서 끊어지지 (all) 않는 한 text-decoration-skip-spaces를 선언하지 마세요. 링크의 텍스트와 같은 일반적인 인라인 텍스트에는 선행 또는 후행 공백이 없으므로 선언은 아무 작업도 하지 않습니다.

인셋으로 선 자르기 또는 연장하기

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를 사용합니다.
  • 인셋 마우스 오버 표시: 링크에 색상 변경 외에 마우스 오버 어포던스가 필요한 경우 추가할 가치가 있습니다. 상호작용이 없는 텍스트는 건너뜁니다.

대체 전략

각 대체는 텍스트를 읽을 수 있도록 유지하고 핵심 장식을 유지합니다. 기준 타겟의 관점에서 필요성을 구성합니다. 속성이 없는 브라우저가 포함된 경우 일치하는 대체 값을 적용합니다.

잉크 건너뛰기 all

기준 타겟이 text-decoration-skip-ink: all를 지원하지 않으면 선언이 무시되고 브라우저에서 auto를 유지합니다. 라틴어 디센더는 여전히 건너뜁니다. auto에서 다루지 않는 스크립트(예: 일본어)만 잉크 건너뛰기가 손실됩니다. 이는 단계적 성능 저하이므로 추가 코드가 필요하지 않습니다.

스페이스 건너뛰기

기준 타겟이 text-decoration-skip-spaces를 지원하지 않는 경우 브라우저는 자체 기본 공백 처리를 사용합니다. 이것이 단계적 성능 저하입니다. 브라우저가 건너뛰지 않는 공백 아래에 선이 그려집니다.

인셋

기준 타겟이 text-decoration-inset를 지원하지 않으면 선언이 무시되므로 링크는 정상적으로 완전히 표시되는 밑줄을 유지하고 전환에는 애니메이션을 적용할 항목이 없습니다. 이것이 단계적 성능 저하입니다. 마우스 오버 표시가 사라지고 링크에 밑줄이 표시됩니다.

국경 지역

기준 타겟이 background-clip: border-area를 지원하지 않으면 선언이 무시되고 배경이 전체 테두리 상자 아래에 그려져 윤곽선만 채우는 대신 텍스트를 채웁니다. 그라데이션이 디자인의 일부인 경우 일반 단색 테두리를 사용하지 마세요. 대신 그라데이션을 재현하고 @supports 내부에만 background-clip: border-area을 적용합니다.

요소의 배경이 알려진 단색인 경우 불투명 레이어를 사용합니다. 패딩 상자 위에 표면 색상을 칠하고 테두리 상자 아래에 그라데이션을 칠하여 투명한 테두리를 통해서만 그라데이션이 표시되도록 합니다.

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

테두리 모양

기준 타겟이 border-shape를 지원하지 않으면 선언이 무시되고 요소는 직사각형 테두리를 유지합니다. 모서리가 둥근 직사각형은 도형이 순전히 장식용인 경우에만 허용되는 대체입니다. 태그, 화살표, 콜아웃과 같이 도형에 의미가 있는 경우 최대한 가깝게 재현하고 @supports 내부에만 border-shape를 적용합니다.

클립된 가상 요소로 윤곽선을 그립니다. 상자가 지원되는 경우와 동일한 크기가 되도록 투명한 테두리를 유지하고, 의사 요소를 윤곽선 색상으로 채우고, 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(...)를 요소에 직접 배치합니다.

의사 요소 밑줄의 경우 .draw::after에 border-block-end로 직선을 그리고 @supports (border-shape: circle(50%)) 내부에만 block-size, border, border-shape 물결선을 적용합니다. 선은 직선이지만 clip-path 애니메이션이 도형에 종속되지 않으므로 계속 그려집니다.

clip-path는 포커스 링과 box-shadow도 클립합니다. 클립된 요소의 윤곽선에 의존하여 포커스를 표시하지 마세요. 대신 색상이나 배경 변경으로 :focus-visible의 스타일을 지정하세요.