Saltar al contenidoEasyByte

El margen no va en @page: va en el contenido

· · 7 min

Un margen que arreglamos tres veces seguidas

En julio de 2026 arreglé el mismo margen de impresión tres veces. La primera vez el margen estaba puesto y aun así el contenido salía pegado al borde del folio. La segunda vez lo corregí, verifiqué que estaba bien, y al imprimir seguía sin margen. La tercera vez funcionó en la página uno y se rompió en la dos. Tres rondas para algo que suena a una línea de CSS. La lección que saqué no es sobre márgenes: es sobre dónde pones las cosas que no quieres que se rompan cuando el software sale de tu pantalla y llega a la de otra persona. Y la regla, ya te la adelanto, es que el margen no va en @page. Va en el contenido.

Contexto: una web que imprime para quien no sabe qué es un margen

Estamos construyendo Fichea, un compositor de fichas de examen para profes de primaria. La idea es sencilla: el profe elige bloques vacíos (operaciones en columna, comprensión lectora, verdadero/falso, rellenar huecos), los rellena con su contenido, y saca un PDF listo para fotocopiar. El artefacto final es un folio A4 impreso. No un fichero que se abre en otra app: papel, que se reparte en clase.

Eso cambia quién aprieta el botón de imprimir. No es un desarrollador con la consola abierta. Es un profesor un domingo por la tarde que pulsa Ctrl+P, ve el diálogo de su navegador, y espera que salga bien a la primera. Si sale mal, no abre un issue: cierra la pestaña y vuelve a Word. Ese usuario es exactamente el que teníamos en mente cuando escribimos por qué una app que va fina en la demo se rompe en producción: la demo la manejas tú, en tu entorno; producción es el entorno de otro, que no controlas.

Todo el compositor lo escribió un bucle automático en cuatro rondas, con tests debajo, como conté en el artículo del bucle que escribió el código. El bucle hizo bien su trabajo: 271 pruebas en verde, siete tipos de bloque. Pero los tests corren en un navegador headless, sin diálogo de impresión. El margen fue justo lo que ningún test podía ver, porque vivía en el único sitio donde el usuario tiene la última palabra.

Ronda uno: el margen estaba puesto y salía pegado al borde

El primer aviso fue directo: "el PDF no tiene márgenes". Miré el CSS y ahí estaba, @page { margin: 7mm }. Estaba aplicándose. El problema es que esos 7 milímetros venían heredados de un generador anterior, uno que pintaba una cuadrícula de operaciones y para eso 7mm sobraban. Para un examen completo, con texto que llega hasta abajo, 7mm dejan el contenido rozando el borde: técnicamente hay margen, visualmente parece que no.

Lo subí a 16mm, que sobre un A4 deja 178mm de ancho útil, un margen de documento de verdad. Y aquí viene el primer hábito que sí funcionó: no me fié de mirarlo en pantalla. Rastericé el PDF real, el que sale por la impresora, y comprobé los 16mm sobre el papel. Verificado. Cerrado. Eso creía.

Ronda dos: el botón que rompe tu diseño lo pulsa el usuario

Al día siguiente, otro aviso, ahora imprimiendo de verdad con Ctrl+P: el PDF salía sin márgenes. Otra vez. Y esto es lo que hace perder la cabeza, porque el arreglo de la ronda uno estaba bien. Lo había verificado sobre papel. Es el mismo patrón que ya nos había mordido en dos incidentes de una misma semana, que conté en cuando el arreglo correcto parece no funcionar: el cambio es correcto, pero una capa por encima se lo come en silencio y el síntoma sigue apuntando al culpable equivocado.

La capa, esta vez, era el propio diálogo de impresión. El navegador ofrece un desplegable de márgenes con la opción "Ninguno". Si el usuario la elige, o si viene elegida de una impresión anterior, ese "Ninguno" anula tu @page { margin }. No lo respeta, no lo negocia: lo pisa. Tú puedes poner 16mm, 50mm o 200mm, que da igual: el navegador le da al usuario un interruptor que apaga tu regla, y ese interruptor está a un clic de distancia en una pantalla que tú no ves.

Ahí está el fallo de método que cometí en la ronda uno: puse el margen en @page, que es precisamente el sitio que el diálogo de impresión gobierna. Estaba dejando la decisión más importante del diseño en manos del último menú desplegable que el usuario tocara.

La solución fue sacar el margen de @page y meterlo en el contenido. El folio se dibuja con un padding propio de 16mm, y @page { margin: 0 }. El padding es contenido renderizado: forma parte de la hoja, no de la configuración de la impresora, así que ningún ajuste del diálogo lo toca. Lo verifiqué de la forma más honesta que se me ocurrió: generé el PDF con @page: 0, que es exactamente lo que hace el usuario cuando elige "Ninguno". Los 16mm seguían ahí.

Dónde vive el margenQuién manda¿Sobrevive a "Márgenes: Ninguno"?
@page { margin: 16mm }El diálogo del navegadorNo: el usuario lo anula con un clic
padding del contenido + @page { margin: 0 }Tu códigoSí: es parte de la hoja, no de la impresora

Ronda tres: y entonces llegó la segunda página

Canté victoria pronto. El siguiente aviso llegó al imprimir un examen de dos folios: la primera página perfecta, la segunda arrancando pegada al borde de arriba. Lógico, en cuanto lo piensas: el padding-top de la hoja aplica una vez, al principio del contenido. Cuando el contenido se parte en dos páginas, la segunda empieza donde se cortó la primera, sin ningún colchón arriba. El margen lateral aguantaba, porque el padding izquierdo y derecho acompaña a cada línea. El de arriba, no.

El truco que lo resolvió es viejo y poco glamuroso, de la época en que se maquetaba con tablas: envolver la ficha en una <table> con un <thead> y un <tfoot> vacíos, de 16mm de alto. El navegador, al imprimir una tabla que se parte entre páginas, repite la cabecera y el pie en cada folio. Ese comportamiento, pensado para que se repitan los encabezados de columna de una tabla larga, nos da gratis una banda de margen arriba y abajo en todas las páginas. Y como es contenido, no configuración, es inmune al diálogo igual que el padding.

<table class="hoja">
  <thead><tr><td class="banda-margen"></td></tr></thead>  <!-- 16mm, se repite arriba -->
  <tbody><tr><td class="cuerpo">... la ficha ...</td></tr></tbody>
  <tfoot><tr><td class="banda-margen"></td></tr></tfoot>   <!-- 16mm, se repite abajo -->
</table>

Lo verifiqué con un PDF real de dos páginas, otra vez con @page: 0: la segunda hoja arranca con sus 16mm arriba, la primera intacta. Margen lateral en el padding del td, margen vertical en las bandas repetidas. Ronda tres cerrada, y esta vez de verdad.

Lo que aprendí: pon los invariantes donde el usuario no llega

La regla que me llevo, y que ya aplico en todo lo que imprime, es esta: en una web que imprime para gente no técnica, cada cosa que no puede fallar tiene que vivir donde el entorno del usuario no la pueda anular. @page es cómodo y es lo que dice cualquier tutorial, pero es terreno del navegador y del usuario, no tuyo. El padding y la estructura del documento son terreno tuyo. La diferencia no se ve en tu pantalla: se ve en la de un profesor que, sin saberlo, tenía "Márgenes: Ninguno" seleccionado de la última vez.

Es la misma idea de siempre, la que sostiene todo lo que escribimos aquí: construir la ficha ya no era el problema, un bucle la montó en una noche. Construirla de forma que sobreviva al domingo por la tarde de alguien que no es programador, eso sí costó, y costó tres rondas. Lo honesto es decir que Fichea todavía es un experimento: está en validación, no es un producto de pago, y este margen es una de las cien cosas pequeñas que separan "funciona en mi máquina" de "funciona en la de cualquiera". Pero es justo en esas cien cosas donde se decide si alguien vuelve a usarlo o se va a Word.

Si estás a punto de montar algo que la gente va a imprimir, o exportar, o abrir en un sitio que no controlas, mira antes dónde has dejado los invariantes: si están en una opción que el usuario puede cambiar, no son invariantes. Si te estás peleando con esto y quieres que te cuente qué miraría, escríbeme a contact@easybyte.es.

— Javier

EasyByte Hub S. Coop. Mad. — Madrid, julio de 2026

Todas las guías