Módulo 04: Tipografía

Objetivos del módulo

  • Entender qué borra Preflight en el texto y por qué un <h1> sin clases parece un párrafo
  • Usar las familias font-sans, font-serif y font-mono, y añadir una fuente propia alojada en tu proyecto
  • Cambiar la fuente de todo el documento con el tema
  • Controlar el peso, la cursiva y la anchura con fuentes variables
  • Dominar la escala text-*, sus alturas de línea emparejadas y la notación text-sm/6
  • Predecir qué altura de línea se aplica, incluso cuando hay leading-* en un contenedor
  • Ajustar el espaciado entre letras, las mayúsculas, la alineación y los subrayados
  • Recortar texto con truncate y line-clamp-*, y arreglar el fallo típico dentro de Flexbox
  • Decidir cómo se parten las líneas: whitespace-*, wrap-*, break-*, hyphens-* y text-wrap
  • Añadir sombras al texto con text-shadow-* y entender su modificador de opacidad
  • Alinear cifras con tabular-nums y activar rasgos OpenType con font-features-*
  • Controlar los tabuladores con tab-* y dar estilo a listas y marcadores
  • Maquetar artículos completos con @tailwindcss/typography
  • Saber qué cambia en cada apartado si trabajas con Tailwind 3.4

1. Preparación del laboratorio

Usa el proyecto Vite del módulo 01. Para este módulo necesitas además el plugin de tipografía y una fuente variable completa:

npm install -D @tailwindcss/typography
npm install inter-ui
mkdir -p public/fonts
cp node_modules/inter-ui/variable/InterVariable.woff2 public/fonts/

Lo que hay en public/ se sirve tal cual desde la raíz del sitio, así que el fichero queda disponible en /fonts/InterVariable.woff2. Tu app.css empieza así; irás ampliándolo a lo largo del módulo:

@import "tailwindcss";

Si una clase nueva no aparece, recuerda el aviso del módulo 02: guarda app.css sin cambiar nada para que Tailwind vuelva a escanear el proyecto.

Los ejemplos usan clases de módulos que aún no has visto. Con esta tabla te basta por ahora:

Clases Qué hacen Dónde se estudian
bg-gray-50, text-gray-500, text-indigo-600 Color de fondo y de texto Color y fondos
border, rounded-lg, divide-y Bordes y separadores Bordes, sombras y efectos
flex, items-center, shrink-0, flex-1 Filas flexibles Flexbox
md:, dark: Aplican una clase con una condición Diseño responsive y Modo oscuro

Si necesitas repasar las propiedades de texto de CSS, las tienes en el Curso de HTML y CSS.


2. Cómo se organiza la tipografía

Preflight empieza desde cero

Sin Tailwind, el navegador da a un <h1> un tamaño grande y negrita, y a un <ul> viñetas y sangría. Preflight, la hoja base que estudiaste en el módulo 02, elimina todo eso. El resultado sorprende la primera vez:

Título, párrafo, enlace y lista sin clases: todo se ve con el mismo tamaño y sin viñetas

Figura 1. Un artículo sin clases. El <h1>, el <h2>, el enlace y la lista parecen párrafos normales.

No es un error: es la decisión de diseño más importante de este módulo. Tailwind separa el significado del aspecto. Eliges <h2> porque es un encabezado de segundo nivel, y eliges text-xl font-bold porque quieres que se vea así. Una tarjeta puede tener un <h3> pequeño sin pelearse con los estilos del navegador.

Preflight sí deja dos cosas útiles: una altura de línea de 1.5 en <html> y la fuente del tema en todo el documento. Además, <b> y <strong> siguen en negrita, y <code> y <pre> usan la fuente monoespaciada.

💡 Consejo: Si escribes texto largo o llega desde un gestor de contenidos, no podrás poner clases en cada etiqueta. Para ese caso existe el plugin de la sección 17.

Las tres medidas de una línea

Casi todo el módulo gira alrededor de tres medidas. Conviene verlas juntas antes de entrar en las clases:

Una línea de texto con la caja del tamaño de fuente dentro de la caja de la altura de línea, y el espaciado entre letras marcado

Figura 2. El tamaño de fuente, la altura de línea y el espaciado de letras. El navegador reparte el sobrante de la altura de línea a medias, por arriba y por abajo.

Mapa de utilidades

Quieres controlar Clases Propiedad CSS
Familia font-sans, font-serif, font-mono font-family
Peso font-normal, font-bold… font-weight
Estilo italic, not-italic font-style
Anchura font-stretch-* font-stretch
Tamaño text-sm, text-2xl… font-size y line-height
Altura de línea leading-* line-height
Espaciado de letras tracking-* letter-spacing
Mayúsculas uppercase, lowercase, capitalize, normal-case text-transform
Alineación text-left, text-center, text-start… text-align
Subrayado underline, decoration-*, underline-offset-* text-decoration-*, text-underline-offset
Recorte truncate, line-clamp-*, text-ellipsis text-overflow, -webkit-line-clamp
Espacios whitespace-* white-space
Corte de palabras wrap-*, break-*, hyphens-* overflow-wrap, word-break, hyphens
Reparto de líneas text-wrap, text-balance, text-pretty, text-nowrap text-wrap
Sombra text-shadow-* text-shadow
Cifras y rasgos tabular-nums, font-features-*… font-variant-numeric, font-feature-settings
Tabuladores tab-* tab-size
Listas list-*, marker: list-style-*, ::marker

⚠️ Importante: El prefijo text- sirve a la vez para el tamaño (text-lg), el color (text-gray-500), la alineación (text-center) y el reparto de líneas (text-balance). Tailwind los distingue por el valor, así que text-lg text-gray-500 text-center no entra en conflicto. El prefijo font- hace lo mismo con la familia y el peso.


3. Familias de fuente

Las tres familias del tema

Tailwind trae tres pilas de fuentes. Una pila es una lista de fuentes por orden de preferencia: el navegador usa la primera que tenga instalada.

<h2 class="font-sans text-2xl font-bold">Portátil UltraBook 14</h2>
<h2 class="font-serif text-2xl font-bold">Portátil UltraBook 14</h2>
<h2 class="font-mono text-2xl font-bold">Portátil UltraBook 14</h2>

El nombre de un producto en cuatro fuentes: de palo seco, con remates, monoespaciada e Inter

Figura 3. font-sans, font-serif, font-mono y una fuente propia, font-marca, que crearás enseguida. Observa las cifras de la fuente con remates, que suben y bajan de la línea como las letras: son cifras elzevirianas.

font-sans genera esto en la 4.3.3:

:root {
  --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
    'Noto Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
    'Noto Color Emoji';
}
.font-sans {
  font-family: var(--font-sans);
}

Lo más importante es que la clase no contiene la lista: lee una variable del tema. Si cambias --font-sans, cambias todas las clases font-sans y también el valor por defecto de la página.

💡 Consejo: La pila por defecto no es igual en las dos versiones. En 3.4.19, font-sans empieza por ui-sans-serif, system-ui. En 4.3.3 empieza por -apple-system, BlinkMacSystemFont, 'Segoe UI'. En los dos casos obtienes la fuente del sistema, pero si tu diseño depende de una fuente concreta, decláralo en el tema y no confíes en la pila por defecto.

Cargar una fuente propia

Una fuente propia necesita dos pasos que conviene no mezclar:

  1. Cargarla con @font-face. Esto es CSS normal y le dice al navegador dónde está el fichero.
  2. Darle nombre en el tema, para que exista una clase que la use.

El primer paso va fuera de @theme:

@import "tailwindcss";

@font-face {
  font-family: "Inter";
  src: url("/fonts/InterVariable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

Fíjate en dos líneas:

  • font-weight: 100 900 indica un rango. Es una fuente variable: un solo fichero contiene todos los pesos, y por eso no hace falta un @font-face para cada uno.
  • font-display: swap pide al navegador que muestre el texto con una fuente de reserva mientras descarga la tuya. Sin esa línea, el texto puede quedar invisible durante la carga.

Declarar la familia en el tema 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) el tema no se escribe en CSS. Si usas la LTS: añade la familia en tailwind.config.js, dentro de theme.extend.fontFamily, como en el ejemplo de esta sección.

Toda variable --font-* dentro de @theme crea una clase font-*:

@theme {
  --font-marca: "Inter", ui-sans-serif, system-ui, sans-serif;
}

Con eso ya puedes escribir class="font-marca". El equivalente en v3, que he compilado con 3.4.19, es:

// tailwind.config.js (Tailwind 3.4)
module.exports = {
  content: ['./*.html'],
  theme: {
    extend: {
      fontFamily: {
        marca: ['Inter', 'ui-sans-serif', 'system-ui', 'sans-serif'],
      },
    },
  },
};

⚠️ Importante: Termina siempre la pila con una familia genérica como sans-serif. Si el fichero no carga, el navegador no se queda sin fuente, y además la reserva se parece a la que querías.

Cambiar la fuente de todo el documento 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no existe @theme, aunque Preflight también lee la familia sans del tema. Si usas la LTS: define theme.extend.fontFamily.sans en tailwind.config.js y obtendrás el mismo efecto.

Si toda la web usa la misma fuente, no hace falta poner una clase en <body>. Basta con redefinir --font-sans:

@theme {
  --font-sans: "Inter", sans-serif;
  --font-mono: "Fira Code", monospace;
}

Preflight lee --default-font-family, que a su vez apunta a --font-sans. Lo he comprobado en el CSS compilado:

--font-sans: "Inter", sans-serif;
--default-font-family: var(--font-sans);

html {
  font-family: var(--default-font-family, ...);
}

Si quieres una fuente por defecto distinta de font-sans, redefine directamente --default-font-family. Lo mismo vale para el código: --font-mono alimenta a --default-mono-font-family, que es lo que usan <code>, <pre>, <kbd> y <samp>.

Otras formas de cargar fuentes

Forma Cómo Ventaja Inconveniente
Fichero en public/ @font-face con url("/fonts/...") Control total, eliges qué fichero sirves Tú gestionas los ficheros
Paquete Fontsource @import "@fontsource-variable/inter"; Se actualiza con npm y trae subconjuntos Algunos subconjuntos pierden rasgos OpenType (sección 13)
Google Fonts @import url("https://fonts.googleapis.com/...") No instalas nada Dependes de un tercero y el navegador se conecta a él

Con Fontsource, el nombre de la familia lleva Variable, y el @import va antes que Tailwind:

npm install @fontsource-variable/inter
@import "@fontsource-variable/inter";
@import "tailwindcss";

@theme {
  --font-sans: "Inter Variable", sans-serif;
}

Con Google Fonts, el orden importa de verdad. Una regla @import debe ir antes que cualquier otra regla, y @import "tailwindcss" se sustituye por miles de líneas de CSS. He compilado las dos variantes con Vite:

/* Mal: aviso del compilador y el @import desaparece de la hoja final */
@import "tailwindcss";
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap");

/* Bien: el @import sobrevive como primera línea de la hoja */
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap");
@import "tailwindcss";

El aviso que da la compilación en el primer caso es este:

Found 1 warning while optimizing generated CSS:
@import rules must precede all rules aside from @charset and @layer statements

💡 Consejo: Para evitar el salto visual al cargar la fuente principal, pide su descarga cuanto antes con <link rel="preload" href="/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin> en el <head>. El atributo crossorigin es obligatorio aunque el fichero esté en tu propio dominio.

Valores arbitrarios de familia

Para un caso aislado puedes evitar el tema:

<p class="font-[Georgia]">Una sola vez</p>
<p class="font-[family-name:var(--fuente-titulo)]">Desde una variable</p>

El segundo ejemplo necesita la pista family-name: porque, sin ella, Tailwind no sabe si la variable contiene una familia o un peso. En v4 existe además la forma corta con paréntesis, font-(family-name:--fuente-titulo), que no compila en v3. La sintaxis de paréntesis se explicó en el módulo 02.


4. Peso, cursiva y anchura

Peso

Clase Valor Uso habitual
font-thin 100 Rótulos enormes
font-extralight 200 Rótulos enormes
font-light 300 Texto grande y discreto
font-normal 400 Texto corriente
font-medium 500 Etiquetas, botones, celdas destacadas
font-semibold 600 Títulos de tarjeta
font-bold 700 Títulos
font-extrabold 800 Titulares
font-black 900 Precios, portadas

Un mismo texto con los nueve pesos, un peso arbitrario, cursiva y anchura condensada

Figura 4. Los nueve pesos con Inter variable, font-[650], italic y font-stretch-condensed.

font-[650] funciona porque Inter es variable: admite cualquier peso de su rango. Con una fuente estática solo hay los pesos que hayas cargado, y el navegador escoge el más cercano.

⚠️ Importante: Si cargas solo el peso 400 de una fuente estática y usas font-bold, el navegador inventa la negrita engordando las letras. Se llama síntesis y se nota. Lo mismo ocurre con la cursiva: la línea italic de la figura 4 usa una cursiva inclinada artificialmente, porque no he cargado el fichero InterVariable-Italic.woff2. Carga los pesos y estilos que uses, o declara un rango con una fuente variable.

italic y not-italic son independientes del peso. not-italic es útil para quitar la cursiva dentro de un <em> o de un <address>, que el navegador pone en cursiva por defecto.

Por dentro: el peso también escribe una variable 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) font-bold solo genera font-weight: 700. Si usas la LTS: el resultado visual es el mismo; solo cambia que no puedes leer --font-weight-bold desde tu CSS, y debes usar theme('fontWeight.bold') en su lugar.

.font-bold {
  --tw-font-weight: var(--font-weight-bold);
  font-weight: var(--font-weight-bold);
}

Puedes crear pesos con nombre en el tema. Yo he añadido este, que genera la clase font-titular:

@theme {
  --font-weight-titular: 650;
}

Anchura con font-stretch-* 🆕 v4.0

🆕 Desde Tailwind 4.0. No existe en la LTS (Tailwind 3.4). Si usas la LTS: escribe la propiedad con un valor arbitrario, por ejemplo [font-stretch:condensed], que compila y da el mismo CSS.

Algunas fuentes variables tienen un eje de anchura. font-stretch-condensed genera font-stretch: condensed, y también admite porcentajes como font-stretch-75%. Solo tiene efecto si la fuente incluye ese eje: he usado Open Sans con su eje wdth, declarado con font-stretch: 75% 100% en su @font-face. Con Inter no pasaría nada, porque no tiene ese eje.

Los nombres disponibles son ultra-condensed, extra-condensed, condensed, semi-condensed, normal, semi-expanded, expanded, extra-expanded y ultra-expanded.

Suavizado

antialiased y subpixel-antialiased cambian cómo se dibujan los bordes de las letras. Generan -webkit-font-smoothing y -moz-osx-font-smoothing, propiedades que solo tienen efecto en macOS. Muchos proyectos ponen antialiased en <body> porque el texto claro sobre fondo oscuro se ve menos grueso en ese sistema.


5. Tamaños de texto

La escala

Clase Tamaño Píxeles Altura de línea En píxeles
text-xs 0.75rem 12 1 / 0.75 16
text-sm 0.875rem 14 1.25 / 0.875 20
text-base 1rem 16 1.5 / 1 24
text-lg 1.125rem 18 1.75 / 1.125 28
text-xl 1.25rem 20 1.75 / 1.25 28
text-2xl 1.5rem 24 2 / 1.5 32
text-3xl 1.875rem 30 2.25 / 1.875 36
text-4xl 2.25rem 36 2.5 / 2.25 40
text-5xl 3rem 48 1 48
text-6xl 3.75rem 60 1 60
text-7xl 4.5rem 72 1 72
text-8xl 6rem 96 1 96
text-9xl 8rem 128 1 128

Las equivalencias en píxeles suponen la configuración normal del navegador. El tamaño está en rem, de modo que respeta a quien aumenta la letra en los ajustes: por eso conviene usar la escala y no píxeles fijos.

Escala de tamaños desde text-xs hasta text-6xl

Figura 5. La escala de text-xs a text-6xl. Los escalones no son iguales: crecen más cuanto mayor es el texto.

Tamaño y altura de línea van juntos

Aquí está la primera diferencia con CSS normal. text-lg no solo fija el tamaño:

.text-lg {
  font-size: var(--text-lg);                                        /* 1.125rem */
  line-height: var(--tw-leading, var(--text-lg--line-height));      /* 1.75 / 1.125 */
}

El motivo es práctico: el texto pequeño necesita más aire entre líneas, y el grande, menos. Fíjate en la tabla: text-sm usa una proporción de 1.43, y text-4xl, de 1.11. Con un solo line-height de 1.5 para todo, los titulares se verían separados. Tailwind empareja cada tamaño con su altura de línea para que no tengas que decidirlo cada vez.

La notación text-sm/6

Si quieres elegir otra altura de línea, puedes añadirla con una barra:

<p class="text-sm/6">14 px con 24 px de altura de línea</p>
<p class="text-lg/loose">18 px con altura de línea 2</p>
<p class="text-sm/[17px]">14 px con 17 px exactos</p>
<p class="text-[32px]/tight">Tamaño arbitrario con altura con nombre</p>
.text-sm\/6 {
  font-size: var(--text-sm);
  line-height: calc(var(--spacing) * 6);
}

El número usa la escala de espaciado: 6 son 1.5rem, es decir, 24 px. Es la misma escala de p-6 que viste en el módulo 03. Tras la barra también valen los nombres none, tight, snug, normal, relaxed y loose.

Valores dinámicos en la altura de línea 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) leading-* y text-sm/* solo admiten los números 3 a 10; leading-11 o text-sm/11 no generan CSS. Si usas la LTS: usa un valor arbitrario, como leading-[2.75rem] o text-sm/[2.75rem].

En v4 cualquier número es válido porque se calcula con --spacing. Esto tiene una consecuencia: si cambias --spacing en el tema, leading-6 y text-sm/6 cambian con él. En v3, leading-6 es siempre 1.5rem.

Tamaños arbitrarios y la ambigüedad de text-

<p class="text-[15px]">Un tamaño suelto</p>
<p class="text-[length:var(--tam)]">Tamaño desde una variable</p>

text-[15px] es claramente un tamaño. Con una variable, Tailwind no puede saber si contiene un tamaño o un color, y por defecto supone que es un color. Lo he comprobado:

Clase Tailwind 3.4.19 Tailwind 4.3.3
text-[--tam] color: var(--tam) color: --tam, CSS no válido
text-(--tam) No genera nada color: var(--tam)
text-[length:var(--tam)] font-size: var(--tam) font-size: var(--tam)
text-(length:--tam) No genera nada font-size: var(--tam)

⚠️ Importante: text-[15px] no fija la altura de línea. Solo las clases de la escala y la notación con barra lo hacen. Con un valor arbitrario sin altura, heredas la del contenedor, y eso se comporta de forma distinta en v3 y v4, como verás en la sección 6.

Tamaños propios en el tema 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no hay variables --text-*. Si usas la LTS: añade el tamaño en theme.extend.fontSize de tailwind.config.js, con un arreglo [tamaño, { lineHeight, letterSpacing, fontWeight }].

Una variable --text-* crea una clase, y puede llevar valores asociados con un doble guion:

@theme {
  --text-titular: 2.5rem;
  --text-titular--line-height: 1.1;
  --text-titular--letter-spacing: -0.02em;
  --text-titular--font-weight: 700;
}

Con una sola clase, text-titular, obtienes cuatro propiedades. Medido en el navegador: 40 px, altura de línea de 44 px, espaciado de -0.8 px y peso 700. Son valores por defecto: text-titular leading-normal font-normal tracking-normal los anula sin problema y da 60 px de altura y peso 400.

// tailwind.config.js (Tailwind 3.4)
fontSize: {
  titular: ['2.5rem', { lineHeight: '1.1', letterSpacing: '-0.02em', fontWeight: '700' }],
},

6. Altura de línea

La escala leading-*

Clase Valor Uso habitual
leading-none 1 Botones de una línea, cifras grandes
leading-tight 1.25 Titulares
leading-snug 1.375 Subtítulos
leading-normal 1.5 Texto de interfaz
leading-relaxed 1.625 Párrafos largos
leading-loose 2 Listas con mucho aire
leading-6 1.5rem Valor fijo de la escala de espaciado

El mismo párrafo con las seis alturas de línea

Figura 6. Las seis alturas con nombre sobre el mismo texto de 14 px. Con leading-none las líneas casi se tocan: resérvalo para texto de una línea.

Hay una diferencia entre los dos tipos de valor. leading-relaxed es proporcional, y leading-6 es una longitud fija. Si escribes leading-6 y luego agrandas el tamaño a text-5xl, las líneas se pisarán. Para párrafos cuyo tamaño puede cambiar, usa los nombres.

Quién gana entre text-* y leading-*

Las dos clases fijan line-height. Como aprendiste en el módulo 02, el orden en el atributo class no decide nada. Aquí no hay conflicto gracias a una variable:

Dos columnas que muestran cómo text-lg usa su valor emparejado y cómo text-lg leading-tight usa el valor de leading

Figura 7. text-lg mira si existe --tw-leading. Si en el mismo elemento hay un leading-*, usa ese valor; si no, usa el emparejado con el tamaño.

Trampa: leading-* en el contenedor

Es un error muy frecuente. Pones leading-loose en un contenedor y esperas que se aplique a todo lo de dentro:

<div class="leading-loose">
  <p class="text-sm">Este párrafo NO se ve con altura 2.</p>
  <p>Este sí.</p>
</div>
<p class="text-sm leading-loose">Este sí.</p>
<p class="text-sm/loose">Este también.</p>

Medido en el navegador:

Elemento Tamaño Altura de línea
text-sm dentro del contenedor 14 px 20 px
Sin clase de tamaño, dentro del contenedor 16 px 32 px
text-sm leading-loose 14 px 28 px
text-sm/loose 14 px 28 px

La causa está en la figura 7. La regla .text-sm se aplica directamente al párrafo y fija su line-height, y una regla directa gana siempre a un valor heredado. Además, --tw-leading se registra con @property e inherits: false, de modo que la variable tampoco baja a los hijos.

Regla práctica: pon la altura de línea en el mismo elemento que el tamaño. Tailwind 3.4 da el mismo resultado en este caso.

Alturas sin unidades 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) las alturas de text-xs a text-4xl son longitudes en rem; en v4 son proporciones sin unidad. Un hijo sin altura propia hereda la longitud en v3, y por eso sus líneas pueden pisarse. Si usas la LTS: da siempre altura de línea a los tamaños arbitrarios, por ejemplo text-[32px]/tight o text-[32px] leading-tight.

Este cambio de la tabla de tamaños parece un detalle, pero se ve:

Clase Tailwind 3.4.19 Tailwind 4.3.3
text-xs line-height: 1rem line-height: calc(1 / 0.75)
text-lg line-height: 1.75rem line-height: calc(1.75 / 1.125)
text-5xl line-height: 1 line-height: 1

Las dos versiones dan la misma altura en el propio elemento. La diferencia aparece en sus descendientes. El CSS hereda una longitud como un valor ya calculado, pero hereda un número sin unidad como un multiplicador:

<div class="text-xs">
  <p class="text-gray-500">Contenedor con text-xs</p>
  <h2 class="text-[32px] font-bold">Portátil UltraBook 14 con garantía ampliada</h2>
</div>

El mismo HTML en Tailwind 3 con las líneas del título pisándose y en Tailwind 4 bien separadas

Figura 8. El mismo HTML compilado con las dos versiones. En 3.4.19 el título de 32 px hereda 16 px de altura de línea. En 4.3.3 hereda la proporción y obtiene 42,67 px.

La guía oficial de actualización no menciona este cambio. Si migras un proyecto, revisa los tamaños arbitrarios: v4 corregirá solapes que tuvieras disimulados, pero también cambiará alturas que hubieras ajustado a ojo.

Alturas propias en el tema 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) no existe @theme. Si usas la LTS: define theme.extend.lineHeight y theme.extend.letterSpacing en tailwind.config.js.

@theme {
  --leading-etiqueta: 1.1;
  --tracking-etiqueta: 0.12em;
}

Estas variables crean leading-etiqueta y tracking-etiqueta. La segunda pertenece a la sección siguiente.


7. Espaciado de letras y mayúsculas

tracking-*

Clase Valor
tracking-tighter -0.05em
tracking-tight -0.025em
tracking-normal 0em
tracking-wide 0.025em
tracking-wider 0.05em
tracking-widest 0.1em

Los valores están en em, es decir, son proporcionales al tamaño de la letra. Un tracking-tight funciona igual en un titular de 60 px que en uno de 30 px.

Títulos con distinto tracking y etiquetas en mayúsculas con tracking creciente

Figura 9. A la izquierda, el espaciado en un titular. A la derecha, una etiqueta en mayúsculas y las transformaciones de texto.

Dos reglas salen de mirar la figura:

  • El texto grande pide apretar. A partir de text-2xl, las letras se ven separadas. tracking-tight lo corrige.
  • Las mayúsculas pequeñas piden abrir. Las mayúsculas se diseñan para ir al principio de palabra, no juntas. uppercase tracking-wider es la combinación clásica para las etiquetas de categoría.

Mayúsculas

<p class="text-xs font-semibold tracking-wider text-indigo-600 uppercase">Portátiles</p>
Clase Efecto
uppercase TODO EN MAYÚSCULAS
lowercase todo en minúsculas
capitalize Primera Letra De Cada Palabra
normal-case Quita cualquiera de las anteriores

💡 Consejo: Escribe el texto normal en el HTML y deja que uppercase lo transforme. Quien copie el texto o lo use en otro diseño seguirá teniendo la versión original. En el HTML de la figura pone Portátiles, y la tilde se conserva al transformar: PORTÁTILES.

⚠️ Importante: capitalize pone en mayúscula cada palabra, como en los títulos ingleses. Por eso la figura 9 muestra Ratón Óptico Inalámbrico. En español solo lleva mayúscula la primera palabra, de modo que casi nunca te servirá. Para eso existe first-letter:uppercase, que verás en la sección 16.


8. Alineación

<p class="text-center">Centrado</p>

Seis cajas con texto alineado a la izquierda, centrado, a la derecha, justificado y con text-start en ambos sentidos

Figura 10. Las cuatro alineaciones físicas y text-start con los dos sentidos de escritura.

Clase Valor
text-left Izquierda
text-center Centro
text-right Derecha
text-justify Justificado
text-start Inicio de la línea según el idioma
text-end Final de la línea según el idioma

text-start es la versión lógica de text-left, igual que ps-* lo era de pl-* en el módulo 03. En un documento árabe o hebreo, con dir="rtl", el inicio está a la derecha. Si tu web puede traducirse a esos idiomas, usa text-start y text-end.

Mira en la figura el text-justify: con columnas estrechas abre huecos desiguales entre palabras. Si lo usas, combínalo con hyphens-auto (sección 11) o, mejor, déjalo para columnas anchas.

💡 Consejo: Alinea a la derecha las cifras de una tabla y a la izquierda el texto. Las cifras se comparan de un vistazo solo si las unidades, decenas y centenas quedan en columna. La sección 13 lo completa.


9. Subrayado y decoración

Texto con subrayado, tachado, sobrerrayado y subrayados con color, grosor, estilo y separación distintos

Figura 11. Las líneas y sus modificadores. La línea se interrumpe en las letras que bajan, como la g.

Clases Propiedad Valores
underline, overline, line-through, no-underline text-decoration-line  
decoration-indigo-500 text-decoration-color Cualquier color del tema
decoration-1, decoration-2, decoration-4, decoration-8 text-decoration-thickness En píxeles, y decoration-auto, decoration-from-font
decoration-solid, decoration-dotted, decoration-dashed, decoration-double, decoration-wavy text-decoration-style  
underline-offset-2, underline-offset-4 text-underline-offset En píxeles, y underline-offset-auto

Un caso típico de la tienda es un enlace con subrayado discreto y un precio tachado:

<a href="#" class="underline decoration-indigo-300 decoration-2 underline-offset-4">Ver la ficha</a>
<span class="text-sm text-gray-500 line-through">999 €</span>

Preflight quita el aspecto de los enlaces, así que un enlace dentro de un párrafo debe recuperar alguna señal visual. El color por sí solo no basta para quienes no distinguen bien los colores: un subrayado sí.

💡 Consejo: decoration-slice y overflow-ellipsis pertenecen a la v3, y la guía oficial los da por eliminados. En 4.3.3 aún generan CSS, pero son nombres antiguos. Usa box-decoration-slice y text-ellipsis, que existen en las dos versiones.


10. Recortar texto

truncate: una línea con puntos suspensivos

truncate es un atajo de tres propiedades que deben ir juntas:

.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

Las tres son necesarias. white-space: nowrap impide partir la línea, overflow: hidden oculta lo que sobra y text-overflow: ellipsis pone los puntos. Las piezas sueltas existen: text-ellipsis y text-clip cambian solo text-overflow.

line-clamp-*: varias líneas

Para limitar un texto a un número de líneas:

<p class="line-clamp-2 text-sm">Estación de trabajo para diseño y CAD con garantía...</p>
.line-clamp-2 {
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

Texto que se sale de su caja, texto con truncate, y párrafos limitados a dos y tres líneas

Figura 12. Sin recorte el texto rebasa la caja. truncate lo corta en una línea, y line-clamp-2 y line-clamp-3 en varias.

Las propiedades tienen el prefijo -webkit- por motivos históricos, pero funcionan en todos los navegadores actuales.

line-clamp-none deshace el recorte, y sirve para mostrar más texto en pantallas anchas:

<p class="line-clamp-2 md:line-clamp-none">...</p>

Medido en el navegador con el texto del ejercicio 3: con 500 px de ventana se ven 2 líneas, y con 800 px se ven las 5.

⚠️ Importante: line-clamp-* necesita display: -webkit-box, y por tanto choca con flex o grid en el mismo elemento. Ponlo siempre en el párrafo, no en su contenedor. Además, line-clamp-none restablece display: block.

Número de líneas dinámico 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) solo existen line-clamp-1 a line-clamp-6 y line-clamp-none. Si usas la LTS: escribe un valor arbitrario, como line-clamp-[7], que compila en las dos versiones.

En v4, cualquier número es válido: line-clamp-7, line-clamp-12.

El fallo clásico: truncate dentro de Flexbox

Este es el error de tipografía más consultado. Tienes una fila con icono, nombre y precio, pones truncate al nombre, y en lugar de recortarse, el texto empuja la fila y se sale:

<div class="flex w-96 items-center gap-3">
  <span class="size-8 shrink-0 rounded bg-indigo-500"></span>
  <div>
    <p class="truncate text-sm font-medium">Ordenador de sobremesa para oficina con garantía ampliada de tres años</p>
  </div>
  <span class="shrink-0 text-sm font-bold">649 €</span>
</div>

Dos filas: en la primera el texto se sale de la caja, en la segunda se recorta con puntos suspensivos

Figura 13. El mismo código sin min-w-0 y con min-w-0. La segunda fila solo añade esa clase al <div> intermedio.

Medido en el navegador, la fila roja mide 382 px por dentro, pero su contenido ocupa 570 px. La verde mide 382 px y su contenido también.

El motivo está en Flexbox: un hijo flexible tiene min-width: auto, que significa “no te encojas por debajo del ancho de tu contenido”. Con white-space: nowrap, el ancho del contenido es la frase entera en una línea. El <div> se niega a encogerse, y el <p> de dentro nunca llega a desbordarse, de modo que truncate no tiene nada que recortar.

La solución es decirle al hijo flexible que sí puede encogerse:

<div class="min-w-0">
  <p class="truncate text-sm font-medium">...</p>
</div>

min-w-0 va en el hijo directo del contenedor flex, no en el elemento con truncate. Si truncate está directamente en el hijo flexible, no hace falta, porque overflow: hidden ya cambia su mínimo automático a cero. Estudiarás el mecanismo completo en Flexbox.


11. Cómo se parten las líneas

Tres problemas distintos se confunden a menudo:

  • Qué hacer con los espacios y saltos del código fuente: whitespace-*.
  • Qué hacer con una palabra que no cabe: wrap-*, break-* y hyphens-*.
  • Cómo repartir las palabras entre las líneas: text-wrap, text-balance y text-pretty.

Espacios en blanco: whitespace-*

Clase Colapsa espacios Respeta saltos Parte líneas largas
whitespace-normal Sí No Sí
whitespace-nowrap Sí No No
whitespace-pre No Sí No
whitespace-pre-line Sí Sí Sí
whitespace-pre-wrap No Sí Sí
whitespace-break-spaces No Sí Sí, también en espacios finales

Seis cajas con el mismo texto de varias líneas y cada valor de whitespace

Figura 14. El mismo texto, con espacios repetidos y saltos de línea, en cada modo.

El caso más práctico es whitespace-pre-line. Si el texto viene de un <textarea> o de una API, con saltos de línea dentro, esta clase los muestra sin que tengas que convertirlos en <br>.

Palabras que no caben

Las URL, las referencias de producto y los nombres de fichero no tienen espacios. Si no caben, se salen de la caja:

Cuatro cajas con una URL larga: desbordada, con wrap-break-word, con wrap-anywhere y con break-all

Figura 15. Por defecto la URL rebasa la caja. wrap-break-word y wrap-anywhere solo la parten si no cabe, y break-all parte cualquier palabra.

wrap-* 🆕 v4.1

🆕 Desde Tailwind 4.1. No existe en la LTS (Tailwind 3.4). Si usas la LTS: en lugar de wrap-break-word usa break-words, que genera el mismo CSS, y en lugar de wrap-anywhere usa el valor arbitrario [overflow-wrap:anywhere].

Clase CSS Efecto
wrap-normal overflow-wrap: normal Solo corta en espacios y guiones
wrap-break-word overflow-wrap: break-word Corta una palabra si no cabe en ninguna línea
wrap-anywhere overflow-wrap: anywhere Igual, y además reduce el ancho mínimo del elemento
break-normal overflow-wrap: normal; word-break: normal Restablece los dos
break-all word-break: break-all Corta en cualquier letra, aunque la palabra cupiera en otra línea
break-keep word-break: keep-all No corta entre letras en chino, japonés y coreano

break-words sigue generándose en 4.3.3, pero el registro de cambios de Tailwind lo trata como obsoleto: npx @tailwindcss/upgrade lo sustituye por wrap-break-word.

La diferencia entre wrap-break-word y wrap-anywhere no se ve en una caja de ancho fijo: en la figura 15 son idénticas. Se ve dentro de Flexbox, por el mismo motivo que en la sección anterior:

Dos filas flexibles con una URL: con wrap-break-word se sale de la caja, con wrap-anywhere se parte dentro

Figura 16. Las dos filas tienen 318 px por dentro. Con wrap-break-word el contenido ocupa 402 px; con wrap-anywhere, 318 px.

overflow-wrap: break-word no afecta al ancho mínimo del elemento, de modo que el hijo flexible mantiene el ancho de la palabra entera. anywhere sí lo reduce. Por eso, para una URL dentro de una fila flexible, usa wrap-anywhere, o wrap-break-word junto con min-w-0.

Guiones: hyphens-*

Clase Efecto
hyphens-none No usa guiones, ni siquiera en los &shy;
hyphens-manual Parte solo donde tú has puesto un guion opcional, &shy;. Es el valor por defecto
hyphens-auto El navegador parte las palabras con su diccionario

Tres cajas estrechas con palabras largas: partidas con ­, desbordadas y partidas por el navegador

Figura 17. Con hyphens-none la palabra rebasa el relleno. Con hyphens-auto el navegador encuentra por sí solo dónde partir.

⚠️ Importante: hyphens-auto depende del idioma. El navegador necesita saber en qué idioma está el texto, de modo que el documento debe tener <html lang="es">, o el elemento un atributo lang. Sin él, no parte nada. Además, cada navegador incluye diccionarios distintos: comprueba tu idioma en los navegadores que te importen. La figura está hecha con Chromium y español.

Reparto de líneas: text-wrap, text-balance y text-pretty

Clase CSS Efecto
text-wrap text-wrap: wrap Comportamiento normal
text-nowrap text-wrap: nowrap No parte la línea
text-balance text-wrap: balance Iguala la longitud de todas las líneas
text-pretty text-wrap: pretty Evita que la última línea quede muy corta

Títulos y párrafos con text-wrap, text-balance y text-pretty

Figura 18. Arriba, un título con text-balance reparte mejor las tres líneas. Abajo, text-pretty evita que España. quede sola en la última línea.

Criterio sencillo: usa text-balance en titulares y text-pretty en párrafos. text-balance no es adecuado para textos largos, porque deja las líneas más cortas de lo necesario, como se ve en la figura. Estas clases son una mejora progresiva: un navegador que no las conozca parte el texto de la forma normal, sin errores.

text-nowrap y whitespace-nowrap se parecen: la primera solo toca text-wrap, y la segunda fija white-space, que también afecta a los espacios. truncate usa white-space: nowrap.


12. Sombras de texto: text-shadow-* 🆕 v4.1

🆕 Desde Tailwind 4.1. No existe en la LTS (Tailwind 3.4). Si usas la LTS: escribe un valor arbitrario como [text-shadow:0_1px_2px_rgb(0_0_0/0.3)], o crea la utilidad con matchUtilities en un plugin. Pierdes el color text-shadow-<color> y el modificador /N.

Tamaños y color

Clase Sombra
text-shadow-2xs 0px 1px 0px rgb(0 0 0 / 0.15)
text-shadow-xs 0px 1px 1px rgb(0 0 0 / 0.2)
text-shadow-sm Tres capas al 7,5 % hasta 0px 2px 2px
text-shadow-md Tres capas al 10 % hasta 0px 2px 4px
text-shadow-lg Tres capas al 10 % hasta 0px 4px 8px
text-shadow-none Sin sombra

Nueve variantes de sombra de texto, incluidas varias con color y opacidad

Figura 19. Los cinco tamaños, un tamaño con /30, el color índigo con y sin opacidad, y un color sin tamaño, que no produce sombra.

Un tamaño y un color se combinan con dos clases:

<p class="text-shadow-lg text-shadow-indigo-500">Oferta</p>
<p class="text-shadow-lg text-shadow-indigo-500/40">Oferta</p>

Dos detalles de la figura 19 sorprenden:

  • El color solo no hace nada. text-shadow-indigo-500 solo escribe la variable --tw-text-shadow-color, y el tamaño es quien la lee. Necesitas las dos clases.
  • /30 no significa “el 30 % de la sombra”. Sustituye la opacidad, no la reduce. text-shadow-lg tiene opacidad 0.1, y text-shadow-lg/30 la sube a 0.3, de modo que queda más oscura. Compara las dos tarjetas de la fila central.

También existen text-shadow-current, que usa el color del texto, y text-shadow-inherit.

El uso real: texto sobre una imagen

Las sombras de texto rara vez deben notarse como adorno. Su mejor uso es asegurar que un texto claro se lea sobre una imagen:

@theme {
  --text-shadow-portada: 0 2px 8px rgb(0 0 0 / 0.6);
}
<header class="rounded-xl bg-[url(/foto.svg)] bg-cover p-8 text-center text-white">
  <h2 class="text-3xl font-bold text-shadow-portada">Vuelta al cole</h2>
  <p class="mt-1 text-sm text-shadow-portada">Portátiles desde 499 €</p>
</header>

Dos portadas con el mismo fondo: el texto sin sombra apenas se lee, con sombra se lee bien

Figura 20. El mismo texto blanco sobre el mismo fondo. La variable --text-shadow-portada crea la clase text-shadow-portada.

Para la LTS, este plugin en tailwind.config.js da una clase equivalente. Lo he compilado con 3.4.19:

const plugin = require('tailwindcss/plugin');

module.exports = {
  content: ['./*.html'],
  theme: {
    extend: {
      textShadow: {
        md: '0 1px 2px rgb(0 0 0 / 0.3)',
        portada: '0 2px 8px rgb(0 0 0 / 0.6)',
      },
    },
  },
  plugins: [
    plugin(({ matchUtilities, theme }) => {
      matchUtilities(
        { 'text-shadow': (valor) => ({ textShadow: valor }) },
        { values: theme('textShadow') },
      );
    }),
  ],
};

13. Cifras y rasgos OpenType

Las fuentes modernas guardan variantes de sus letras: cifras alineadas, ceros con barra, versalitas. Son los rasgos OpenType, y se activan desde CSS.

Cifras: font-variant-numeric

Clase Efecto
tabular-nums Todas las cifras miden lo mismo
proportional-nums Cada cifra mide lo suyo
lining-nums Cifras de la misma altura
oldstyle-nums Cifras elzevirianas, que suben y bajan
slashed-zero Cero con barra
ordinal Letras voladas en ordinales
diagonal-fractions Fracciones con barra diagonal
stacked-fractions Fracciones apiladas
normal-nums Quita todas las anteriores

La de uso diario es tabular-nums. Con cifras proporcionales, un 1 ocupa menos que un 8, y los importes no quedan en columna aunque estén alineados a la derecha:

Dos tablas de precios: con cifras proporcionales las columnas no coinciden, con tabular-nums sí

Figura 21. Mismos precios con Inter. A la izquierda, 19,90 y 79,90 no empiezan en la misma posición. A la derecha, con tabular-nums, las cifras forman columnas.

Pon la clase una sola vez en la fila, el <tbody> o la tabla: la propiedad se hereda. También evita que un contador o un reloj baile a cada cambio.

Las clases se pueden combinar. Por dentro, cada una escribe una variable, como --tw-numeric-spacing, y todas se unen en una sola propiedad. Por eso tabular-nums slashed-zero activa las dos cosas.

font-features-* 🆕 v4.2

🆕 Desde Tailwind 4.2. No existe en la LTS (Tailwind 3.4). Si usas la LTS: escribe la propiedad con un valor arbitrario, por ejemplo [font-feature-settings:'ss02'], que genera el mismo CSS.

Para cualquier otro rasgo, font-features-* genera font-feature-settings. Solo admite valores arbitrarios o una variable:

<p class="font-features-['zero']">Cero con barra</p>
<p class="font-features-['ss02','zero']">Dos rasgos a la vez</p>
<p class="font-features-(--mis-rasgos)">Desde una variable</p>

Una forma como font-features-smcp, sin corchetes, no genera nada.

También puedes asociar rasgos a una familia en el tema, con el mismo doble guion que los tamaños:

@theme {
  --font-marca: "Inter", ui-sans-serif, system-ui, sans-serif;
  --font-marca--font-feature-settings: "cv11";
}

Cuatro cajas con caracteres de Inter: sin rasgos, con cv11, con cero con barra y con dos rasgos

Figura 22. Rasgos de Inter. cv11 cambia la a, zero añade la barra al cero, y ss02 distingue la l y la I.

Observa la figura con atención: en las dos cajas de abajo, la a ha vuelto a su forma normal, aunque están dentro de font-marca. font-feature-settings sustituye el valor anterior, no lo suma. Si quieres cv11 y zero, tienes que escribir los dos: font-features-['cv11','zero']. En el mismo elemento, font-features-* gana a la fuente del tema, sea cual sea el orden en class.

⚠️ Importante: Un rasgo solo funciona si está en el fichero de la fuente. He probado primero con el subconjunto latino de @fontsource-variable/inter (48 KB), y zero y ss02 no hacían nada, sin ningún aviso. Con InterVariable.woff2 completa (352 KB) funcionan. Si un rasgo no se aplica, sospecha de la fuente antes que de la clase, y ten en cuenta que más rasgos implican un fichero más grande.

💡 Consejo: Para cifras, prefiere tabular-nums a font-features-['tnum']. La propiedad font-variant-numeric es de más alto nivel y no borra los demás rasgos.


14. Tabuladores: tab-* 🆕 v4.3

🆕 Desde Tailwind 4.3. No existe en la LTS (Tailwind 3.4). Si usas la LTS: escribe un valor arbitrario, como [tab-size:2], que genera el mismo CSS.

tab-size indica cuántos espacios ocupa un carácter de tabulación. Solo se nota donde los tabuladores se conservan, como en un <pre>:

<pre class="tab-2">...</pre>

Tres bloques con el mismo JSON sangrado con tabuladores: tab-2, sin clase y tab-8

Figura 23. El mismo JSON sangrado con tabuladores reales. Sin clase mide 4, el valor que fija Preflight.

tab-* acepta cualquier número (tab-2, tab-8) y valores arbitrarios (tab-[3]). Preflight ya pone tab-size: 4 en <html>, también en v3, de modo que solo necesitas la clase para otro valor.


15. Listas

Viñetas y números

Preflight quita los marcadores y la sangría de <ul> y <ol>. Recuperarlos tiene una trampa:

Nueve listas con distintas combinaciones: sin clases, list-disc, con padding, list-inside, numeradas y con marcadores propios

Figura 24. En la segunda caja, list-disc sin relleno deja las viñetas fuera. La sangría también hay que devolverla.

<ul class="list-disc pl-5">
  <li>Garantía de tres años</li>
  <li>Envío en 48 horas</li>
</ul>
Clase CSS
list-disc list-style-type: disc
list-decimal list-style-type: decimal
list-none list-style-type: none
list-[upper-roman] Cualquier tipo de CSS
list-['✓_'] Una cadena como marcador; _ equivale a un espacio
list-outside Marcador fuera del texto, valor por defecto
list-inside Marcador dentro de la línea
list-image-[url(/flecha.svg)] Imagen como marcador
list-image-none Quita la imagen

list-disc por sí sola deja el marcador en una zona que ya no existe, porque Preflight puso padding: 0. Por eso necesitas pl-5, o mejor ps-5, la propiedad lógica del módulo 03. Las listas numeradas necesitan un poco más, pl-6, y las romanas más todavía, como se ve en la figura.

list-inside también lo resuelve, pero con textos largos la segunda línea queda debajo del marcador y no del texto. Para contenido de varias líneas, usa list-outside con relleno.

Color y forma del marcador: marker:

La variante marker: apunta al pseudoelemento ::marker, y permite dar estilo solo al número o a la viñeta:

<ol class="list-decimal pl-6 marker:font-bold marker:text-indigo-600">
  <li>Elige tu equipo</li>
  <li>Confirma el pedido</li>
</ol>

Puedes poner marker: en la lista, y no solo en cada <li>, porque la regla generada selecciona también los descendientes:

.marker\:text-red-500 ::marker,
.marker\:text-red-500::marker {
  color: var(--color-red-500);
}

::marker solo admite unas pocas propiedades: color, fuente y contenido. Para algo más elaborado, como un círculo con fondo, quita el marcador con list-none y dibújalo con un elemento propio.

💡 Consejo: Safari con VoiceOver deja de anunciar como lista un <ul> con list-style: none, como se avisó en el módulo 02. Si es una lista de contenido, añade role="list". Hazlo también si usas un marcador de texto en una lista importante, como en el ejercicio 9.


16. Otros recursos de texto

Sangría de primera línea, superíndice y subíndice, y una letra capitular

Figura 25. indent-8, align-super y align-sub, y una capitular hecha con first-letter:.

Clases Para qué
indent-4, indent-8 Sangría de la primera línea (text-indent), con la escala de espaciado
align-baseline, align-top, align-middle, align-sub, align-super Alineación vertical (vertical-align) en línea o en celdas de tabla
first-letter: Da estilo a la primera letra
first-line: Da estilo a la primera línea
selection: Color del texto seleccionado

La capitular de la figura se escribe así:

<p class="first-letter:float-left first-letter:mr-2 first-letter:text-5xl
          first-letter:font-bold first-letter:text-indigo-700">
  Bienvenido a nuestra tienda...
</p>

selection: no se aprecia en una captura estática. Prueba selection:bg-amber-300 selection:text-amber-950 en un párrafo y selecciona el texto. Se hereda: si lo pones en <body>, afecta a toda la página.


17. Artículos completos con @tailwindcss/typography

El problema

Las utilidades funcionan cuando controlas cada etiqueta. Pero si un artículo viene de un CMS o de un Markdown, recibes <h2>, <p> y <ul> sin clases, y Preflight los deja como en la figura 1. El plugin oficial @tailwindcss/typography resuelve exactamente eso: una clase en el contenedor da buenos estilos a todo lo que hay dentro.

Instalación 🔄 v4.0

🔄 Cambia en Tailwind 4.0. En la LTS (Tailwind 3.4) los plugins se registran en la configuración de JavaScript. Si usas la LTS: añade plugins: [require('@tailwindcss/typography')] a tailwind.config.js; las clases prose funcionan igual.

npm install -D @tailwindcss/typography
@import "tailwindcss";
@plugin "@tailwindcss/typography";

La clase prose

<article class="prose dark:prose-invert">
  <h1>Cómo elegir tu portátil de empresa</h1>
  <p class="lead">Una guía rápida para no equivocarte...</p>
  <p>Antes de comprar conviene revisar <a href="#">el catálogo</a>:</p>
  <ul>
    <li>La memoria</li>
    <li>La autonomía</li>
  </ul>
  <!-- encabezados, código, citas y tablas, sin ninguna clase -->
</article>

Artículo con título, entradilla, lista, código, cita y tabla bien maquetados

Figura 26. El mismo tipo de contenido de la figura 1, ahora con prose. Ninguna etiqueta interior lleva clases, salvo lead en la entradilla.

prose limita el ancho a 65ch, unos 65 caracteres por línea, que es una medida cómoda para leer. Si el contenedor ya tiene su ancho, añade max-w-none. Sin esa clase, un artículo dentro de una columna ancha se queda estrecho.

Modo oscuro

prose-invert cambia a la paleta para fondos oscuros. Se usa con la variante dark::

El mismo artículo en modo oscuro con textos claros

Figura 27. El mismo artículo con el sistema en modo oscuro, y bg-gray-950 en el <body>.

Tamaños y grises

Clase Efecto
prose-sm, prose-base, prose-lg, prose-xl, prose-2xl Escala todos los elementos en proporción
prose-gray, prose-slate, prose-zinc, prose-neutral, prose-stone Elige la gama de grises
prose-invert Paleta para fondo oscuro

Tres artículos con prose-sm, prose y prose-lg

Figura 28. prose-sm, prose y prose-lg. Cambian a la vez el texto, los márgenes y los títulos.

Los tamaños se combinan con variantes para que el texto crezca con la pantalla: prose md:prose-lg.

Personalizar elementos concretos

Para retocar un elemento sin salir del HTML hay variantes propias:

<article class="prose prose-slate max-w-none
                prose-headings:font-marca prose-headings:tracking-tight
                prose-h2:text-indigo-700
                prose-a:text-indigo-600 prose-a:no-underline hover:prose-a:underline
                prose-code:rounded prose-code:bg-indigo-50 prose-code:px-1
                prose-code:before:content-none prose-code:after:content-none">
  <h2>Ficha técnica</h2>
  <p>Consulta <a href="#">todos los modelos</a> o llama a la ruta <code>/api/productos</code>.</p>

  <div class="not-prose my-4 rounded-lg border border-gray-200 p-3">
    <h3 class="text-sm font-bold">Portátil UltraBook 14</h3>
    <p class="text-sm text-gray-600">Tarjeta dentro del artículo: <a href="#">sin estilos de prose</a>.</p>
  </div>

  <p>El texto del artículo continúa aquí con sus estilos.</p>
</article>

Artículo con títulos índigo, enlaces sin subrayado, código con fondo y una tarjeta sin estilos de prose

Figura 29. Las variantes prose-* y una tarjeta de producto aislada con not-prose.

Las variantes disponibles en la versión 0.5.20 son: prose-headings, prose-h1 a prose-h6, prose-lead, prose-p, prose-a, prose-blockquote, prose-figure, prose-figcaption, prose-strong, prose-em, prose-kbd, prose-code, prose-pre, prose-ol, prose-ul, prose-li, prose-dl, prose-dt, prose-dd, prose-table, prose-thead, prose-tr, prose-th, prose-td, prose-img, prose-picture, prose-video y prose-hr.

Hay tres detalles que explican este ejemplo:

  • Las comillas invertidas. El plugin añade por defecto una ` antes y después del código en línea, como en la figura 26. Parece un fallo, pero no lo es. prose-code:before:content-none prose-code:after:content-none las elimina.
  • not-prose. Excluye un bloque: dentro de él no se aplica ninguna regla de prose. Sirve para insertar componentes con utilidades.
  • La herencia sigue actuando. not-prose quita las reglas, pero el color y el tamaño que el bloque hereda del artículo siguen ahí. El título de la tarjeta de la figura hereda el gris de prose-slate. Si quieres otro color, dalo tú.

Para repetir el cambio en varios artículos, o cambiar colores que no tienen variante, prose expone variables CSS:

<article class="prose [--tw-prose-bullets:var(--color-indigo-500)]
                [--tw-prose-links:var(--color-indigo-700)]
                [--tw-prose-quote-borders:var(--color-indigo-300)]">

Lista con viñetas índigo, enlace índigo y cita con borde índigo

Figura 30. Las viñetas, los enlaces y el borde de la cita, cambiados con variables --tw-prose-*.

Si prose choca con otra clase de tu proyecto, cambia el nombre con una opción del plugin. Tras compilarlo, el CSS solo contiene .articulo:

@plugin "@tailwindcss/typography" {
  className: "articulo";
}

💡 Consejo: Usa prose para el contenido que no controlas y utilidades para la interfaz. Una tarjeta o un formulario no deben ir dentro de prose, y un artículo largo no debe escribirse clase a clase.


18. Un sistema tipográfico en la práctica

Las utilidades permiten cualquier combinación, y por eso conviene limitarse. Estas decisiones, tomadas una vez, dan coherencia a toda la tienda:

Papel Clases Dónde
Titular de página text-3xl font-bold tracking-tight text-balance Una vez por página
Título de sección text-xl font-semibold Secciones
Título de tarjeta text-lg font-semibold Tarjetas
Texto normal text-base Párrafos
Texto secundario text-sm text-gray-600 Descripciones
Detalle text-xs text-gray-500 Fechas, referencias
Etiqueta text-xs font-semibold tracking-wider uppercase Categorías
Precio text-2xl font-bold tabular-nums Fichas
Cifras en tabla text-sm tabular-nums text-right Tablas

Algunas pautas que evitan los problemas más comunes:

  • Usa pocos tamaños y pocos pesos. Cuatro tamaños y dos pesos bastan en la mayoría de las pantallas. La jerarquía sale del contraste, no de la variedad.
  • Crea jerarquía también con el color. Un font-medium oscuro junto a un text-sm text-gray-600 separa dos datos mejor que aumentar el tamaño.
  • Limita el ancho del texto. Más de unos 75 caracteres por línea cansa. max-w-prose limita a 65ch sin usar el plugin.
  • Evita el texto muy pequeño. text-xs es para detalles, no para información importante.
  • No uses el peso font-thin en tamaños pequeños. En la figura 4 se ve frágil incluso a 24 px.

En Reutilización y componentes verás cómo evitar repetir estas combinaciones.


19. Diagnóstico

Síntoma Causa Solución
Un <h1> se ve como un párrafo Preflight quita sus estilos text-3xl font-bold, o prose
Una lista no tiene viñetas Preflight list-disc pl-5
list-disc no muestra viñetas Se dibujan fuera de la caja, recortadas por el contenedor Añade pl-5 o list-inside
font-marca no cambia nada La familia no se llama igual en @font-face y en @theme Comprueba el nombre y mira la pestaña Red
La fuente descarga pero se ve igual Ruta incorrecta en url() Debe empezar por / si está en public/
La negrita se ve artificial Falta ese peso Carga el peso o una fuente variable
La fuente de Google no carga @import url() después de Tailwind Ponlo antes de @import "tailwindcss"
leading-* en el contenedor no actúa El hijo tiene text-* Ponlo en el mismo elemento
text-[--tam] da un color Tailwind supone color text-(length:--tam) en v4, text-[length:var(--tam)] en ambas
truncate no recorta en una fila flex min-width: auto min-w-0 en el hijo flexible
truncate no recorta en bloque El elemento no tiene ancho limitado Limita su ancho o el del contenedor
line-clamp-* no recorta Hay flex o grid en el mismo elemento Ponlo en el párrafo interior
hyphens-auto no parte palabras Falta lang <html lang="es">
Una URL se sale de la tarjeta No hay puntos de corte wrap-anywhere
text-shadow-indigo-500 no hace nada Falta el tamaño Añade text-shadow-md
text-shadow-lg/30 queda más oscura /N sustituye la opacidad Usa un valor por debajo de la original, como /5
font-features-* no hace nada La fuente no tiene el rasgo Usa la fuente completa
Un rasgo del tema desaparece font-features-* lo sustituye Escribe todos los rasgos juntos
Comillas invertidas en code dentro de prose Estilo del plugin prose-code:before:content-none prose-code:after:content-none

La herramienta principal es la pestaña Elementos de las DevTools. La sección de estilos calculados dice qué valor final tiene line-height o font-family, y la de fuentes, qué fuente se está dibujando de verdad, que puede no ser la que pediste.


20. Ejercicios

Los ejercicios usan el app.css que has ido construyendo. Todas las soluciones se han compilado con Tailwind 4.3.3 y se han comprobado en Chromium.

Ejercicio 1: Cabecera de ficha

Maqueta la cabecera de la ficha del Portátil UltraBook 14 (899 €): una etiqueta de categoría en mayúsculas, un titular equilibrado, la descripción, un precio antiguo tachado y el precio actual en grande, y una línea final con el IVA y el stock.

Ver solución
<article class="rounded-xl bg-white p-6 shadow">
  <p class="text-xs font-semibold tracking-wider text-indigo-600 uppercase">Portátiles</p>
  <h1 class="mt-1 text-3xl font-bold tracking-tight text-balance">Portátil UltraBook 14</h1>
  <p class="mt-2 text-gray-600">Portátil ligero de 14 pulgadas, 16 GB RAM</p>
  <p class="mt-4">
    <span class="text-sm text-gray-500 line-through">999 €</span>
    <span class="ml-2 text-4xl font-black tabular-nums">899 €</span>
  </p>
  <p class="mt-1 text-xs text-gray-500">IVA incluido · 25 unidades en stock</p>
</article>

Ficha con etiqueta, título, descripción, precio tachado y precio grande

Figura 31. Solución del ejercicio 1.

La jerarquía sale de cuatro tamaños y de dos colores. El precio es lo más pesado de la ficha (font-black), aunque el título tiene más anchura.

Ejercicio 2: Lista con nombres largos

Crea una lista de productos con una caja de icono, nombre, referencia y precio. Con un nombre muy largo, el texto debe recortarse con puntos suspensivos y el precio debe seguir visible y alineado.

Ver solución
<ul class="divide-y divide-gray-200 rounded-xl bg-white">
  <li class="flex items-center gap-3 p-3">
    <span class="size-10 shrink-0 rounded-lg bg-indigo-100"></span>
    <div class="min-w-0 flex-1">
      <p class="truncate text-sm font-medium">Estación de trabajo para diseño y CAD con garantía ampliada de tres años</p>
      <p class="truncate text-xs text-gray-500">Referencia TW-1899-EMPRESA-CAD-2026-ES-PRO-XL</p>
    </div>
    <span class="shrink-0 text-sm font-bold tabular-nums">1899 €</span>
  </li>
  <li class="flex items-center gap-3 p-3">
    <span class="size-10 shrink-0 rounded-lg bg-indigo-100"></span>
    <div class="min-w-0 flex-1">
      <p class="truncate text-sm font-medium">Ratón inalámbrico</p>
      <p class="truncate text-xs text-gray-500">Referencia TW-0019</p>
    </div>
    <span class="shrink-0 text-sm font-bold tabular-nums">19,90 €</span>
  </li>
</ul>

min-w-0 permite que la columna central se encoja, flex-1 hace que ocupe el espacio libre, y shrink-0 protege al icono y al precio. Cada <p> recorta por su cuenta.

Ejercicio 3: Descripción de longitud limitada

En una tarjeta de catálogo, muestra solo dos líneas de la descripción en pantallas pequeñas y el texto completo desde el punto de ruptura md. Fija la altura de línea en 24 px.

Ver solución
<article class="rounded-xl bg-white p-4">
  <h2 class="text-lg font-bold">Torre Workstation</h2>
  <p class="mt-1 line-clamp-2 text-sm/6 text-gray-600 md:line-clamp-none">
    Estación de trabajo para diseño y CAD con procesador de alto rendimiento,
    64 GB de memoria, gráfica profesional certificada y garantía de tres años
    con soporte en el lugar de trabajo.
  </p>
</article>

Medido: con 500 px de ventana, -webkit-line-clamp vale 2 y hay 2 líneas. Con 800 px vale none y hay 5 líneas en una tarjeta de 300 px. text-sm/6 fija tamaño y altura en una sola clase. Los puntos de ruptura se estudian en Diseño responsive.

Ejercicio 4: Tabla de precios

Maqueta una tabla con producto, stock y precio. Las cifras deben quedar en columna, y la cabecera, en mayúsculas pequeñas.

Ver solución
<table class="w-full rounded-xl bg-white text-sm">
  <thead>
    <tr class="text-left text-xs tracking-wider text-gray-500 uppercase">
      <th class="p-3 font-medium">Producto</th>
      <th class="p-3 text-right font-medium">Stock</th>
      <th class="p-3 text-right font-medium">Precio</th>
    </tr>
  </thead>
  <tbody class="divide-y divide-gray-100 tabular-nums">
    <tr><td class="p-3 font-medium">Ratón inalámbrico</td><td class="p-3 text-right">200</td><td class="p-3 text-right">19,90 €</td></tr>
    <tr><td class="p-3 font-medium">Teclado mecánico</td><td class="p-3 text-right">60</td><td class="p-3 text-right">79,90 €</td></tr>
    <tr><td class="p-3 font-medium">Portátil Gaming 16</td><td class="p-3 text-right">10</td><td class="p-3 text-right">1499,00 €</td></tr>
    <tr><td class="p-3 font-medium">Torre Workstation</td><td class="p-3 text-right">4</td><td class="p-3 text-right">1899,00 €</td></tr>
  </tbody>
</table>

tabular-nums se hereda, de modo que basta con ponerla una vez en <tbody>. El navegador pone <th> en negrita y centrado, y Preflight no lo cambia. Por eso cada cabecera repite la alineación de su columna y baja el peso con font-medium.

Ejercicio 5: Un tamaño con personalidad

Crea una clase text-titular de 2.5rem, con altura de línea 1.1, espaciado -0.02em y peso 700. Luego demuestra que se puede anular en un elemento concreto. Da también la versión para Tailwind 3.4.

Ver solución

En Tailwind 4:

@theme {
  --text-titular: 2.5rem;
  --text-titular--line-height: 1.1;
  --text-titular--letter-spacing: -0.02em;
  --text-titular--font-weight: 700;
}
<h1 class="text-titular">Ofertas de otoño</h1>
<h2 class="text-titular mt-4 leading-normal font-normal tracking-normal">Ofertas de otoño</h2>

En Tailwind 3.4:

// tailwind.config.js
module.exports = {
  content: ['./*.html'],
  theme: {
    extend: {
      fontSize: {
        titular: ['2.5rem', { lineHeight: '1.1', letterSpacing: '-0.02em', fontWeight: '700' }],
      },
    },
  },
};

Valores medidos en las dos versiones para el <h1>: 40 px, altura de 44 px, espaciado de -0.8 px y peso 700. El <h2> en 4.3.3 da altura de 60 px y peso 400.

Ejercicio 6: La fuente de la marca

Aloja Inter en tu proyecto, crea la clase font-marca con el rasgo cv11 activado, y usa un peso intermedio de 650 en un título.

Ver solución
@import "tailwindcss";

@font-face {
  font-family: "Inter";
  src: url("/fonts/InterVariable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@theme {
  --font-marca: "Inter", ui-sans-serif, system-ui, sans-serif;
  --font-marca--font-feature-settings: "cv11";
}
<section class="font-marca">
  <h2 class="text-2xl font-[650] tracking-tight">Teclado mecánico</h2>
  <p class="text-gray-600">Teclado mecánico retroiluminado. Precio: 79,90 €.</p>
  <p class="font-black">Peso 900 con la misma fuente variable.</p>
</section>

Comprobado en el navegador: document.fonts da Inter: loaded, y el elemento tiene font-family: Inter, ... y font-feature-settings: "cv11". La clase de familia y sus rasgos se heredan, así que basta con ponerla en el contenedor.

En Tailwind 3.4, la familia con rasgos se escribe con un arreglo anidado:

fontFamily: {
  marca: [['Inter', 'ui-sans-serif', 'system-ui', 'sans-serif'], { fontFeatureSettings: '"cv11"' }],
},

Ejercicio 7: Enlace para compartir

En una fila flexible con un icono, muestra un título y una URL muy larga que debe partirse dentro de la tarjeta.

Ver solución
<div class="flex gap-3 rounded-xl bg-white p-4 text-sm">
  <span class="size-10 shrink-0 rounded-lg bg-indigo-100"></span>
  <div class="min-w-0">
    <p class="font-medium">Enlace para compartir la ficha</p>
    <p class="wrap-anywhere text-gray-600">https://empresa.example/catalogo/portatiles/ultrabook-14-16gb-ram-512gb-ssd-plata?utm_source=boletin</p>
  </div>
</div>

Aquí min-w-0 sobra si usas wrap-anywhere, pero es buena costumbre en columnas de texto y te protege si otro hijo cambia. En Tailwind 3.4, sustituye la clase por [overflow-wrap:anywhere], o usa break-words junto con min-w-0.

Ejercicio 8: Portada legible

Crea una portada con una imagen de fondo y texto blanco. El título debe usar una sombra propia del tema, y el subtítulo, una sombra de la escala con color negro al 60 %.

Ver solución
@theme {
  --text-shadow-portada: 0 2px 8px rgb(0 0 0 / 0.6);
}
<header class="rounded-xl bg-[url(/foto.svg)] bg-cover p-10 text-center text-white">
  <h1 class="text-4xl font-black text-shadow-portada">Vuelta al cole</h1>
  <p class="mt-2 font-medium text-shadow-lg text-shadow-black/60">Portátiles desde 499 €</p>
</header>

Portada con texto blanco y sombra sobre un fondo de colores claros

Figura 32. Solución del ejercicio 8.

En el subtítulo, text-shadow-lg da la forma y text-shadow-black/60 da el color y su opacidad. En Tailwind 3.4, usa el plugin de la sección 12 para text-shadow-portada, y [text-shadow:0_2px_4px_rgb(0_0_0/0.6)] para el subtítulo. En un proyecto real, comprueba el contraste sobre la foto: si es irregular, un degradado oscuro encima del fondo funciona mejor que una sombra.

Ejercicio 9: Listas de la compra

Crea una lista de ventajas con marcas de verificación verdes, en la que las líneas largas queden alineadas con el texto, y una lista numerada con el número en negrita.

Ver solución
<div class="rounded-xl bg-white p-5 text-sm">
  <h2 class="font-bold">Incluido con tu pedido</h2>
  <ul role="list" class="mt-2 list-['✓_'] space-y-1 pl-5 marker:font-bold marker:text-green-600">
    <li>Garantía de tres años, ampliable a cinco con el plan empresa y sin coste de envío</li>
    <li>Envío en 48 horas</li>
    <li>Soporte técnico en toda España</li>
  </ul>
  <h2 class="mt-4 font-bold">Pasos de la compra</h2>
  <ol class="mt-2 list-decimal space-y-1 pl-6 marker:font-bold marker:text-indigo-600">
    <li>Elige tu equipo</li>
    <li>Confirma el pedido</li>
    <li>Recíbelo en casa</li>
  </ol>
</div>

El marcador va fuera de la línea (list-outside, valor por defecto) y por eso la segunda línea se alinea con el texto. _ equivale a un espacio dentro del corchete. role="list" mantiene la lista para VoiceOver. Compila igual en Tailwind 3.4.

Ejercicio 10: Entrada del blog

Maqueta una noticia de la tienda con prose, en gris pizarra, a todo el ancho de su contenedor y con modo oscuro. Incluye tres tarjetas de producto que no hereden los estilos del artículo.

Ver solución
<article class="prose prose-slate max-w-none rounded-xl bg-white p-8
                prose-headings:tracking-tight prose-a:text-indigo-600
                dark:bg-gray-900 dark:prose-invert dark:prose-a:text-indigo-300">
  <h1>Novedades de otoño en la tienda EMPRESA</h1>
  <p class="lead">Renovamos el catálogo de portátiles y ampliamos la garantía.</p>
  <p>Toda la gama está en <a href="#">el catálogo</a>. Estos son los tres modelos más vendidos:</p>

  <div class="not-prose my-6 grid grid-cols-3 gap-3 text-center">
    <div class="rounded-lg bg-gray-100 p-3 dark:bg-gray-800">
      <p class="text-sm font-medium dark:text-gray-100">Básico 15</p>
      <p class="text-lg font-bold tabular-nums dark:text-white">499 €</p>
    </div>
    <div class="rounded-lg bg-gray-100 p-3 dark:bg-gray-800">
      <p class="text-sm font-medium dark:text-gray-100">UltraBook 14</p>
      <p class="text-lg font-bold tabular-nums dark:text-white">899 €</p>
    </div>
    <div class="rounded-lg bg-gray-100 p-3 dark:bg-gray-800">
      <p class="text-sm font-medium dark:text-gray-100">Gaming 16</p>
      <p class="text-lg font-bold tabular-nums dark:text-white">1499 €</p>
    </div>
  </div>

  <h2>Garantía ampliada</h2>
  <p>Desde este mes todos los portátiles incluyen <strong>tres años</strong> de garantía.</p>
</article>

Artículo en modo oscuro con tres tarjetas de producto dentro

Figura 33. Solución del ejercicio 10 con el sistema en modo oscuro.

max-w-none anula el límite de 65ch. Las tarjetas llevan dark:text-* porque not-prose no impide heredar el color del artículo. Fíjate en dark:prose-a:text-indigo-300: un índigo oscuro sobre fondo oscuro no se leería, así que el enlace necesita su propio valor.

Ejercicio 11 (propuesto): Tu escala

Define en el tema dos tamaños, text-cuerpo y text-rotulo, con altura y espaciado propios, y una familia font-rotulo. Rehaz con ellos la tabla de la sección 18 y comprueba en las DevTools que el valor de line-height es el que esperas.

Ejercicio 12 (propuesto): Bloque de código

Crea una tarjeta que muestre una respuesta JSON de GET /api/productos/1 en un <pre> con fuente monoespaciada, tabuladores de 2 (tab-2) y whitespace-pre-wrap para que las líneas largas no desborden. Añade el nombre del fichero encima, recortado con truncate. Da la alternativa para Tailwind 3.4.


Resumen

Concepto API Idea clave
Preflight y texto h1 a h6, ul, a sin estilo Separa significado y aspecto
Familias font-sans, font-serif, font-mono Leen variables del tema
Fuente propia @font-face y --font-* en @theme theme.extend.fontFamily en v3
Fuente por defecto --font-sans, --default-font-family Sin clase en <body>
Peso font-normal a font-black, font-[650] Carga los pesos que uses
Anchura font-stretch-* 🆕 v4.0 Requiere el eje en la fuente
Tamaño text-xs a text-9xl También fija la altura de línea
Altura con barra text-sm/6, text-lg/loose Tamaño y altura en una clase
Altura de línea leading-* Ponla junto al tamaño, no en el padre
Sin unidades calc(1.75 / 1.125) 🔄 v4.0 Se hereda como proporción
Letras tracking-* En em; apretar lo grande, abrir las mayúsculas
Mayúsculas uppercase, normal-case capitalize no sirve para el español
Alineación text-left, text-start, text-end Las lógicas respetan RTL
Subrayado underline, decoration-*, underline-offset-* Modificadores independientes
Recorte de línea truncate Tres propiedades juntas
Recorte en líneas line-clamp-* Choca con flex y grid
Flex y recorte min-w-0 En el hijo directo del contenedor
Espacios whitespace-* pre-line para texto de <textarea>
Cortar palabras wrap-* 🆕 v4.1, break-* anywhere reduce el ancho mínimo
Guiones hyphens-auto Necesita lang
Reparto text-balance, text-pretty Títulos y párrafos, respectivamente
Sombra text-shadow-* 🆕 v4.1 /N sustituye la opacidad
Cifras tabular-nums Se hereda; imprescindible en tablas
Rasgos font-features-* 🆕 v4.2 Sustituye; la fuente debe tenerlos
Tabuladores tab-* 🆕 v4.3 Preflight ya pone 4
Listas list-disc pl-5, marker: Hay que devolver la sangría
Artículos prose, dark:prose-invert, not-prose Una clase para contenido sin clases