Usa las propiedades nativas de text-decoration para aplicar diseño a los subrayados, las líneas superiores y los tachados, en lugar de simularlos con bordes, gradientes de fondo o seudoelementos. Una decoración nativa sigue el texto a lo largo de las líneas ajustadas, hace un seguimiento de las métricas de la fuente, omite los trazos descendentes de los glifos y hereda a través de elementos anidados, por lo que no es necesario volver a implementar nada de eso. Usa las técnicas basadas en cuadros que se encuentran al final de esta guía solo cuando la decoración deba delinear o unir el texto, en lugar de ejecutarse junto con él.
Las decoraciones de texto se aplican a cualquier texto, no solo a los vínculos. Cuando el texto subrayado no sea un vínculo, asegúrate de que no se pueda confundir con uno. Cuando decores un vínculo, usa un <a href> real que apunte al destino (agrega target="_blank" rel="noopener" para abrir una pestaña nueva), no un href="javascript:void(0)" con un controlador de secuencias de comandos. Un vínculo real conserva el comportamiento de hacer clic con el botón central, copiar el vínculo y el lector de pantalla.
Cómo aplicar diseño a la línea
Establece la línea, el estilo, el color y el grosor con la abreviatura text-decoration, y el espacio desde el texto con text-underline-offset. Une el selector en :where() para que los valores predeterminados tengan especificidad cero y cualquier anulación posterior gane, sin necesidad de propiedades personalizadas.
: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 acepta solid, double, dotted, dashed y wavy. text-underline-offset solo se aplica a los subrayados.
Puntos de partida para el diseño:
- Grosor: Alrededor de
0.06ema0.1empara el texto del cuerpo. Usa un grosor mayor (de0.1ema0.15em) para los encabezados y los estados destacados. - Compensación: de
0.15ema0.25emaproximadamente. Las líneas punteadas y onduladas necesitan un poco más de espacio que las sólidas, por lo que debes aumentar el desplazamiento para ellas. - Color:
currentColormantiene la línea alineada con el texto. Una mezcla más suave del color del texto que se intensifica cuando se coloca el cursor sobre él mantiene la calma en las listas largas de vínculos. - Estilo:
solidpara vínculos,dottedpara términos y abreviaturas, ywavypara anotaciones, como errores.
Controla dónde se omite la línea
Tinta que se corta
De forma predeterminada, los subrayados y las líneas superiores se interrumpen alrededor de las partes de los glifos que los cruzan, como las astas descendentes. Establece text-decoration-skip-ink para elegir el comportamiento:
.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;
}
- Mantén
autopara el texto en latín y no lo declares, ya que es el valor predeterminado. - Usa
allcuando el subrayado esté lo suficientemente cerca de la línea de base como para cruzar glifos ideográficos o cuando el texto con escritura mixta deba verse coherente. - Usa
nonepara una línea muy gruesa o una decorativa, en la que los espacios se verían como cortes.
Omitir espacios
text-decoration-skip-spaces decide si la línea se dibuja debajo de los espacios. El valor predeterminado, start end, deja los espacios iniciales y finales sin decorar.
.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;
}
No declares text-decoration-skip-spaces, a menos que la decoración deba cubrir visiblemente el padding (none) o dividirse entre cada palabra (all). El texto intercalado común, como el texto de un vínculo, no tiene espacios iniciales ni finales, por lo que la declaración no hace nada.
Recorta o extiende la línea con un recuadro
text-decoration-inset mueve el inicio y el final de la línea. Un valor positivo tira de un extremo hacia adentro, mientras que un valor negativo lo empuja hacia afuera. Un valor mueve ambos extremos; dos valores establecen el inicio y el final por separado. Los porcentajes se refieren al tamaño intercalado de la caja decorada.
/* 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); }
Para revelar un subrayado al colocar el cursor sobre un elemento sin un borde o un gradiente de fondo, recorta el final en un 100% y vuelve a establecerlo en 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;
}
}
Dibuja una línea personalizada con un seudoelemento
Usa un seudoelemento cuando la línea deba ser una forma que text-decoration no pueda dibujar, como un garabato, y muéstrala recortándola. No se necesita lenguaje de marcado adicional, por lo que el vínculo sigue siendo un <a> simple.
<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 */
}
}
Delinear el texto con un cuadro
Dos propiedades de caja decoran el texto delineándolo en lugar de trazar una línea a lo largo de él. Agrega padding al texto para que el contorno no lo toque.
/* 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 reemplaza solo el borde. El fondo sigue siendo el cuadro rectangular, por lo que no debes confiar en él para rellenar una forma.
Cuando solo las esquinas necesitan una forma diferente, como una etiqueta biselada, redondeada o con muescas, usa corner-shape con border-radius. Mantiene el fondo y el borde siguiendo las esquinas, y los navegadores no compatibles recurren a las esquinas redondeadas comunes.
.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 */
}
Elige una técnica
text-decoration(línea, salto de tinta, espacios, inserción): la línea debe extenderse a lo largo del texto, ajustarse a él y seguir sus métricas de fuente. Esta es la opción predeterminada.- Pseudoelemento con
border-shapeyclip-path: La línea es una forma personalizada o necesita una animación de dibujo. Elígelo solo cuandotext-decoration-styleno pueda producir el aspecto, ya que necesita un posicionamiento adicional. background-clip: border-area: El texto necesita un contorno con gradiente. Usaborder-colorsimple cuando un contorno sólido sea suficiente.corner-shape: Solo las esquinas necesitan una forma diferente, como biselada, redondeada o con muescas. Combínalo conborder-radius, que establece el tamaño de la esquina.border-shape: El contorno necesita una forma queborder-radiusno pueda dibujar, como una etiqueta o una flecha. Usaborder-radiuscuando las esquinas redondeadas sean suficientes.- Revelación de inserción al colocar el cursor: Vale la pena agregarla cuando un vínculo necesita una affordance al colocar el cursor más allá de un cambio de color. Omítelo para el texto que no sea interactivo.
Estrategias de resguardo
Cada resguardo mantiene el texto legible y la decoración principal. Enmarca la necesidad en función de tu objetivo de Baseline: si incluye navegadores sin una propiedad, aplica el resguardo correspondiente.
Tinta que se salta all
Si tu objetivo de Baseline no admite text-decoration-skip-ink: all, se ignora la declaración y el navegador conserva auto. Los trazos descendentes latinos se siguen omitiendo; solo las escrituras que auto no cubre, como el japonés, pierden la omisión de tinta. Esta es una degradación elegante, por lo que no se necesita código adicional.
Omitir espacios
Si tu objetivo de Baseline no admite text-decoration-skip-spaces, el navegador usa su propio manejo predeterminado de los espacios. Esto es una degradación elegante: la línea se dibuja debajo de los espacios que el navegador no omite.
Inserción
Si tu objetivo de Baseline no admite text-decoration-inset, se ignora la declaración, por lo que el vínculo mantiene su subrayado normal y completamente visible, y la transición no tiene nada que animar. Esto es la degradación elegante: se pierde la revelación al colocar el cursor sobre el elemento y el vínculo permanece subrayado.
Área de borde
Si tu objetivo de Baseline no admite background-clip: border-area, se ignora la declaración y el fondo se pinta debajo de todo el cuadro de borde, lo que rellena el texto en lugar de solo su contorno. NO te conformes con un borde liso y sólido cuando el gradiente sea parte del diseño. En su lugar, reproduce el gradiente y aplica background-clip: border-area solo dentro de @supports.
Usa una capa opaca cuando el fondo del elemento sea un color plano conocido. Pinta el color de la superficie sobre el cuadro de relleno y el gradiente debajo del cuadro de borde, de modo que el gradiente solo se muestre a través del borde transparente:
.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 */
}
}
Usa un seudoelemento enmascarado cuando el interior deba permanecer transparente. El pseudoelemento lleva el gradiente, y una máscara corta todo, excepto su anillo de relleno:
@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;
}
}
Forma del borde
Si tu objetivo de Baseline no admite border-shape, se ignorará la declaración y el elemento mantendrá su borde rectangular. Un rectángulo redondeado solo es un resguardo aceptable cuando la forma es puramente decorativa. Cuando la forma tiene un significado, como una etiqueta, una flecha o un cuadro de texto, reprodúcela lo más fielmente posible y aplica border-shape solo dentro de @supports.
Dibuja el contorno con un pseudoelemento recortado. Mantén un borde transparente para que el cuadro tenga el mismo tamaño que en el caso admitido, rellena el seudoelemento con el color del contorno y córtalo en un anillo con un evenodd clip-path: la forma exterior y, luego, la misma forma insertada por el ancho del trazo.
.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%);
}
}
Para obtener una forma rellena en lugar de un contorno, coloca clip-path: polygon(...) con los mismos puntos directamente en el elemento.
Para el subrayado del pseudoelemento, dibuja una línea recta con border-block-end en .draw::after y aplica el garabato block-size, border y border-shape solo dentro de @supports (border-shape: circle(50%)). La línea es recta, pero se sigue dibujando, ya que la animación clip-path no depende de la forma.
clip-path también recorta los anillos de enfoque y box-shadow. No uses un contorno en el elemento recortado para mostrar el enfoque; en su lugar, aplica diseño a :focus-visible con un cambio de color o de fondo.