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-serifyfont-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óntext-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
truncateyline-clamp-*, y arreglar el fallo típico dentro de Flexbox - Decidir cómo se parten las líneas:
whitespace-*,wrap-*,break-*,hyphens-*ytext-wrap - Añadir sombras al texto con
text-shadow-*y entender su modificador de opacidad - Alinear cifras con
tabular-numsy activar rasgos OpenType confont-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:

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:
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í quetext-lg text-gray-500 text-centerno entra en conflicto. El prefijofont-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>

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-sansempieza porui-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:
- Cargarla con
@font-face. Esto es CSS normal y le dice al navegador dónde está el fichero. - 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 900indica un rango. Es una fuente variable: un solo fichero contiene todos los pesos, y por eso no hace falta un@font-facepara cada uno.font-display: swappide 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 detheme.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 familiasansdel tema. Si usas la LTS: definetheme.extend.fontFamily.sansentailwind.config.jsy 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 atributocrossorigines 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 |

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íneaitalicde la figura 4 usa una cursiva inclinada artificialmente, porque no he cargado el ficheroInterVariable-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-boldsolo generafont-weight: 700. Si usas la LTS: el resultado visual es el mismo; solo cambia que no puedes leer--font-weight-bolddesde tu CSS, y debes usartheme('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.

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-*ytext-sm/*solo admiten los números 3 a 10;leading-11otext-sm/11no generan CSS. Si usas la LTS: usa un valor arbitrario, comoleading-[2.75rem]otext-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 entheme.extend.fontSizedetailwind.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 |

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:
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-xsatext-4xlson longitudes enrem; 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 ejemplotext-[32px]/tightotext-[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>

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: definetheme.extend.lineHeightytheme.extend.letterSpacingentailwind.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.

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-tightlo corrige. - Las mayúsculas pequeñas piden abrir. Las mayúsculas se diseñan para ir al principio de palabra, no juntas.
uppercase tracking-wideres 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
uppercaselo transforme. Quien copie el texto o lo use en otro diseño seguirá teniendo la versión original. En el HTML de la figura ponePortátiles, y la tilde se conserva al transformar:PORTÁTILES.
⚠️ Importante:
capitalizepone en mayúscula cada palabra, como en los títulos ingleses. Por eso la figura 9 muestraRatón Óptico Inalámbrico. En español solo lleva mayúscula la primera palabra, de modo que casi nunca te servirá. Para eso existefirst-letter:uppercase, que verás en la sección 16.
8. Alineación
<p class="text-center">Centrado</p>

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

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-sliceyoverflow-ellipsispertenecen a la v3, y la guía oficial los da por eliminados. En 4.3.3 aún generan CSS, pero son nombres antiguos. Usabox-decoration-sliceytext-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;
}

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-*necesitadisplay: -webkit-box, y por tanto choca conflexogriden el mismo elemento. Ponlo siempre en el párrafo, no en su contenedor. Además,line-clamp-nonerestablecedisplay: 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-1aline-clamp-6yline-clamp-none. Si usas la LTS: escribe un valor arbitrario, comoline-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>

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-*yhyphens-*. - Cómo repartir las palabras entre las líneas:
text-wrap,text-balanceytext-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 |

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:

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-wordusabreak-words, que genera el mismo CSS, y en lugar dewrap-anywhereusa 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:

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 ­ |
hyphens-manual | Parte solo donde tú has puesto un guion opcional, ­. Es el valor por defecto |
hyphens-auto | El navegador parte las palabras con su diccionario |

Figura 17. Con hyphens-none la palabra rebasa el relleno. Con hyphens-auto el navegador encuentra por sí solo dónde partir.
⚠️ Importante:
hyphens-autodepende 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 atributolang. 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 |

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 conmatchUtilitiesen un plugin. Pierdes el colortext-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 |

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-500solo escribe la variable--tw-text-shadow-color, y el tamaño es quien la lee. Necesitas las dos clases. /30no significa “el 30 % de la sombra”. Sustituye la opacidad, no la reduce.text-shadow-lgtiene opacidad 0.1, ytext-shadow-lg/30la 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>

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:

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

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), yzeroyss02no hacían nada, sin ningún aviso. ConInterVariable.woff2completa (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-numsafont-features-['tnum']. La propiedadfont-variant-numerices 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>

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:

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>conlist-style: none, como se avisó en el módulo 02. Si es una lista de contenido, añaderole="list". Hazlo también si usas un marcador de texto en una lista importante, como en el ejercicio 9.
16. Otros recursos de texto

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')]atailwind.config.js; las clasesprosefuncionan 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>

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

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 |

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>

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-nonelas elimina. not-prose. Excluye un bloque: dentro de él no se aplica ninguna regla deprose. Sirve para insertar componentes con utilidades.- La herencia sigue actuando.
not-prosequita 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 deprose-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)]">

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
prosepara el contenido que no controlas y utilidades para la interfaz. Una tarjeta o un formulario no deben ir dentro deprose, 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-mediumoscuro junto a untext-sm text-gray-600separa 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-proselimita a65chsin usar el plugin. - Evita el texto muy pequeño.
text-xses para detalles, no para información importante. - No uses el peso
font-thinen 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>

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>

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>

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 |