padding es el espacio interior entre el contenido de un elemento y su borde. margin es el espacio exterior, entre el borde del elemento y los elementos vecinos. Un truco para recordarlo: el padding "empuja" el contenido hacia dentro; el margin "empuja" el elemento hacia fuera de los demás.
Un ejemplo con un botón ayuda a verlo claro:
.boton {
padding: 10px 20px;
margin: 15px;
background: #1a1a1a;
color: #fff;
}Aquí padding: 10px 20px; aumenta el área clicable del botón y separa el texto de su propio borde (10px arriba/abajo, 20px a los lados), sin cambiar la posición del botón respecto a lo que le rodea. margin: 15px; hace justo lo contrario: separa el botón de otros elementos vecinos, sin tocar el espacio entre el texto y el borde del botón.
La notación abreviada sigue el mismo orden en ambas propiedades, empezando arriba y girando en el sentido del reloj: arriba derecha abajo izquierda. Así, padding: 10px 20px 10px 20px; equivale a padding: 10px 20px; (cuando arriba/abajo y derecha/izquierda coinciden, se pueden agrupar en solo dos valores).
Dos comportamientos que conviene tener en cuenta:
- El padding aumenta el tamaño total del elemento (a menos que se use
box-sizing: border-box), mientras que el margin nunca forma parte del tamaño del propio elemento: solo afecta al espacio que hay alrededor. - Los márgenes verticales se pueden "colapsar": si dos elementos seguidos tienen
margin-bottom: 20pxymargin-top: 20pxrespectivamente, el espacio real entre ambos no es de 40px, sino de 20px (se queda el mayor de los dos, no la suma). El padding nunca hace esto.
Un caso práctico donde la diferencia importa de verdad: si quieres que el fondo de color de una tarjeta llegue hasta el borde del elemento pero el texto quede separado de ese borde, usa padding. Si lo que quieres es separar una tarjeta de la siguiente, sin que el fondo de color se vea afectado, usa margin:
.tarjeta {
background: #f7f7f7;
padding: 20px; /* espacio entre el fondo gris y el texto */
margin-bottom: 24px; /* espacio entre esta tarjeta y la siguiente */
}Flexbox es un sistema de maquetación pensado para distribuir elementos en una sola dirección (fila o columna), alineándolos y repartiendo el espacio sobrante entre ellos con facilidad. Se activa con display: flex en el contenedor. Conviene usarlo para barras de navegación, tarjetas en fila o cualquier grupo de elementos que necesite alinearse o repartirse de forma flexible; para maquetaciones en dos dimensiones (filas y columnas a la vez) suele ser mejor CSS Grid.
Un ejemplo típico, una barra de navegación con el logo a la izquierda y los enlaces a la derecha:
.navegacion {
display: flex;
justify-content: space-between;
align-items: center;
}Aquí justify-content: space-between; empuja el primer elemento hijo hacia el extremo izquierdo y el último hacia el extremo derecho, repartiendo el espacio sobrante entre ellos. align-items: center; los alinea verticalmente entre sí, aunque tengan alturas distintas (por ejemplo, un logo más alto que el texto de un enlace). Sin necesidad de floats, ni de calcular anchos a mano.
Los dos ejes de Flexbox conviene tenerlos claros, porque casi todas las propiedades dependen de ellos:
- Eje principal (el que marca
flex-direction; por defecto, horizontal): se controla conjustify-content(flex-start,center,space-between,space-around...). - Eje transversal (perpendicular al principal): se controla con
align-items, que alinea los elementos en esa dirección.
Un segundo ejemplo, unas tarjetas en fila que se reparten el espacio disponible a partes iguales:
.tarjetas {
display: flex;
gap: 20px;
}
.tarjeta {
flex: 1;
}gap: 20px; añade separación entre las tarjetas sin tener que jugar con márgenes que luego hay que compensar en los extremos. flex: 1; en cada tarjeta hace
Es el criterio que usa el navegador para decidir qué regla aplica cuando varias afectan al mismo elemento. Cuanto más específico sea un selector, más "peso" tiene: un id pesa más que una clase, y una clase pesa más que una etiqueta HTML. Por ejemplo, #menu a gana a .navegacion a, y ambos ganan a un simple a. Cuando dos reglas tienen la misma especificidad, gana la que aparece después en el CSS.
El navegador calcula la especificidad como una puntuación con tres niveles, de más a menos peso:
- IDs (
#menu): el nivel con más peso. - Clases, atributos y pseudo-clases (
.navegacion,[type="text"],:hover): peso intermedio. - Etiquetas y pseudo-elementos (
a,div,::before): el nivel con menos peso.
Un selector suma puntos de cada nivel que use. Así, #menu a tiene 1 ID + 1 etiqueta, y .navegacion a tiene 1 clase + 1 etiqueta: como un ID pesa más que cualquier cantidad de clases, el primero gana siempre, sin importar cuántas clases se añadan a la segunda regla.
Un ejemplo con un botón que recibe estilos de dos sitios distintos:
.boton {
background: gray;
}
#cabecera .boton {
background: black;
}Aunque .boton aparezca después en el archivo, #cabecera .boton tiene más especificidad (1 ID + 1 clase, frente a 1 clase sola), así que el fondo final es negro. El orden en el CSS solo decide el empate cuando la especificidad es exactamente igual.
Este es justo el motivo por el que aparecen estilos que "no se aplican" aunque estén bien escritos: no es un error de sintaxis, es que otra regla, en otro punto del CSS, tiene más peso. Herramientas como el inspector del navegador muestran las reglas tachadas cuando pierden frente a otra más específica, lo cual es la forma más rápida de diagnosticar el problema.
Una recomendación práctica: cuantas menos capas de especificidad uses (evitar IDs para estilos y anidar selectores lo mínimo posible), más fácil es sobrescribir algo más adelante sin tener que aumentar la especificidad cada vez más para ganarle. Es el mismo motivo por el que conviene evitar !important salvo como último recurso: al ganar por encima de cualquier especificidad, si se abusa de él, deja de haber margen para forzar un estilo cuando de verdad hace falta.
relative desplaza el elemento respecto a su posición original en el flujo normal, sin sacarlo de ese flujo (el espacio que ocupaba sigue reservado). absolute saca el elemento del flujo por completo y lo posiciona respecto a su ancestro posicionado más cercano (el primero que tenga position distinto de static); si no hay ninguno, se posiciona respecto al documento entero. Por eso es habitual poner position: relative en el contenedor padre cuando se quiere controlar dónde "cuelga" un hijo con position: absolute.
Un ejemplo con relative a solas, para ver que el espacio original queda reservado:
.caja {
position: relative;
top: 10px;
left: 20px;
}La caja se dibuja visualmente 10px más abajo y 20px más a la derecha de donde estaría por defecto, pero el resto de elementos de la página siguen comportándose como si la caja siguiera en su sitio original: no se reajustan para "llenar" ese hueco. Es un desplazamiento puramente visual.
El caso más habitual, sin embargo, no es usar relative para desplazar algo, sino para convertir un contenedor en el "punto de referencia" de un hijo con absolute. Un ejemplo típico: una insignia sobre una tarjeta.
.tarjeta {
position: relative;
}
.insignia {
position: absolute;
top: 10px;
right: 10px;
}Aquí .tarjeta no se mueve de sitio (el position: relative sin top/left no desplaza nada), pero al tener un position distinto de static, se convierte en el ancestro posicionado más cercano. Por eso .insignia se coloca a 10px del borde superior y 10px del borde derecho de la tarjeta, no de toda la página. Sin ese relative en el padre, la insignia buscaría el siguiente ancestro posicionado hacia arriba en el árbol, y si no encontrara ninguno, acabaría posicionándose respecto al <html> entero, probablemente en un sitio muy distinto al que se buscaba.
Esta es una fuente muy habitual de bugs de maquetación: un elemento con position: absolute que "se escapa" a una esquina inesperada de la página. La primera pregunta a hacerse siempre es: ¿tiene este elemento algún ancestro con position: relative (o absolute, o fixed) cerca? Si no lo tiene, ahí está el problema.
Dos matices más que conviene tener presentes:
- fixed se comporta como
absolute, pero siempre respecto a la ventana del navegador, no a ningún ancestro; por eso se usa para cabeceras o botones que deben quedarse fijos al hacer scroll. - sticky combina ambos: se comporta como
relativehasta que, al hacer scroll, llega a un límite definido (por ejemplo,top: 0), momento en el que se "pega" ahí como si fuerafixed, dentro de los límites de su contenedor.
Es el modelo con el que el navegador calcula el tamaño de cada elemento: contenido, más padding, más border, más margin, de dentro hacia fuera. Por defecto, el ancho que se define con width es solo el del contenido; con box-sizing: border-box ese ancho pasa a incluir también el padding y el borde, lo cual suele hacer los cálculos más predecibles.
Un ejemplo numérico ayuda a ver por qué da tantos quebraderos de cabeza. Con el comportamiento por defecto (box-sizing: content-box):
.caja {
width: 200px;
padding: 20px;
border: 5px solid black;
}El ancho total que ocupa realmente esta caja en la página no son 200px, sino 200 + 20 + 20 + 5 + 5 = 250px (padding a ambos lados más borde a ambos lados). El margin ni siquiera entra en esta cuenta: se suma aparte, como espacio adicional fuera de esos 250px.
Con box-sizing: border-box, en cambio:
.caja {
box-sizing: border-box;
width: 200px;
padding: 20px;
border: 5px solid black;
}Ahora los 200px ya incluyen el padding y el borde: es el navegador quien reduce el espacio disponible para el contenido (en este caso, el contenido se queda en 200 - 20 - 20 - 5 - 5 = 150px), pero la caja completa sigue midiendo exactamente 200px de ancho, sin sorpresas.
Esto se nota especialmente al maquetar varias columnas con porcentajes. Con content-box, tres columnas al 33.33% con algo de padding se desbordan y rompen la fila, porque el navegador añade el padding por encima de ese 33.33%. Con border-box, esas mismas tres columnas siguen ocupando exactamente el 33.33% cada una, padding incluido, y encajan sin desbordarse.
Por eso es tan habitual encontrarse esta regla casi al principio de cualquier hoja de estilos:
*, *::before, *::after {
box-sizing: border-box;
}Aplicarla a todos los elementos (incluidos los pseudo-elementos) desde el principio evita tener que acordarse de añadirla caja por caja, y es la convención que siguen la mayoría de frameworks y resets CSS modernos.
Por último, conviene recordar que el margin queda siempre fuera del cálculo del tamaño de la caja, sea content-box o border-box: nunca forma parte del "tamaño" del elemento, solo del espacio que reserva alrededor de él frente a sus vecinos (como se explicaba en la FAQ de margin y padding).
display: none elimina el elemento del flujo del documento: no ocupa espacio, como si no existiera. visibility: hidden lo oculta visualmente, pero sigue ocupando su espacio, dejando un hueco en la página. Para el acordeón de preguntas del plugin de FAQs, por ejemplo, uso display: none en las respuestas cerradas.
La diferencia se ve enseguida con tres cajas en fila, donde la del medio se oculta de una forma u otra:
.oculto-display { display: none; }
.oculto-visibility { visibility: hidden; }Con .oculto-display, las cajas primera y tercera se juntan, como si la del medio nunca hubiera existido: el navegador no reserva ningún espacio para ella. Con .oculto-visibility, en cambio, queda un hueco en blanco del tamaño exacto que ocupaba la caja del medio, y las otras dos no se mueven ni un píxel.
Esto marca cuál usar según el efecto que se busque:
- display: none — para elementos que de verdad no deben afectar a la maquetación mientras estén ocultos: pestañas no seleccionadas, respuestas de un acordeón cerradas, un menú desplegable antes de abrirse.
- visibility: hidden — para casos donde interesa reservar el espacio aunque el contenido no se vea todavía, por ejemplo un icono de "cargando" que aparecerá ahí mismo sin que el resto de la interfaz salte de sitio mientras tanto.
Un matiz importante con visibility: se hereda y se puede "recuperar" en los hijos. Si un contenedor tiene visibility: hidden, se puede volver a mostrar un elemento concreto dentro de él con visibility: visible, y ese hijo sí se verá aunque su padre siga oculto. Con display: none esto no es posible: si el padre no está en el flujo, ningún hijo puede estarlo, sin importar qué display se le ponga.
Aplicado al acordeón de preguntas del plugin de FAQs, el JavaScript alterna la clase o el estilo así:
document.querySelectorAll('.indaga-faq-pregunta').forEach(function (btn) {
btn.addEventListener('click', function () {
var respuesta = btn.nextElementSibling;
var abierta = btn.getAttribute('aria-expanded') === 'true';
respuesta.style.display = abierta ? 'none' : 'block';
});
});Se usa display: none a propósito, y no visibility: hidden: si se usara visibility, todas las respuestas cerradas seguirían ocupando su espacio en blanco, y la página se vería llena de huecos vacíos antes de desplegar nada, en vez de mostrar solo la lista compacta de preguntas.
Una tercera opción relacionada, aunque distinta de las dos anteriores, es opacity: 0: hace el elemento invisible pero, igual que visibility: hidden, sigue ocupando espacio y además sigue siendo clicable y accesible por teclado, a menos que se combine explícitamente con algo como pointer-events: none. Se usa sobre todo para animar apariciones y desapariciones con transition, algo que ni display ni visibility permiten animar directamente.
Son reglas condicionales que aplican estilos distintos según las características de la pantalla, sobre todo su ancho. Por ejemplo, @media (max-width: 600px) { ... } aplica esos estilos solo en pantallas de 600 píxeles o menos. Son la base del diseño responsive, para adaptar un sitio a móvil, tablet y escritorio.
Un ejemplo típico: una barra de navegación que pasa de horizontal a vertical en pantallas pequeñas.
.navegacion {
display: flex;
flex-direction: row;
}
@media (max-width: 600px) {
.navegacion {
flex-direction: column;
}
}Por defecto, los enlaces del menú se muestran en fila. En cuanto el ancho de la ventana baja de 600px, la media query sobrescribe esa propiedad y los apila en columna, sin necesidad de duplicar el HTML ni usar JavaScript.
Además de max-width, existe min-width, que aplica los estilos a partir de un ancho mínimo hacia arriba. Combinando ambos se pueden crear rangos concretos:
@media (min-width: 601px) and (max-width: 1024px) {
/* estilos solo para tablet, aproximadamente */
}Esto es lo que se conoce como breakpoints: los puntos de ancho donde el diseño cambia de comportamiento. No hay unos valores "oficiales" universales, pero es habitual definir dos o tres: uno para móvil, otro para tablet y otro para escritorio, ajustados al contenido real del sitio más que a modelos de dispositivo concretos, que cambian constantemente.
Un enfoque muy recomendable es escribir primero los estilos para móvil sin ninguna media query, y usar min-width para ir añadiendo complejidad a medida que crece la pantalla (lo que se conoce como diseño mobile-first):
.tarjetas {
display: block; /* una columna, por defecto, para móvil */
}
@media (min-width: 768px) {
.tarjetas {
display: flex; /* varias columnas, a partir de tablet */
}
}Este orden suele dar hojas de estilo más simples que el enfoque contrario (diseñar primero para escritorio y luego ir "deshaciendo" cosas para pantallas pequeñas con max-width), porque parte de la versión más sencilla y añade complejidad progresivamente, en lugar de ir quitándola.
Las media queries no se limitan al ancho: también se pueden usar para adaptar estilos a la orientación del dispositivo (orientation: portrait), a si se va a imprimir la página (@media print), o para respetar las preferencias del sistema del usuario, como el modo oscuro con prefers-color-scheme: dark.
Es un sistema de maquetación en dos dimensiones (filas y columnas a la vez), pensado para estructuras completas de página, a diferencia de Flexbox, que trabaja mejor en una sola dirección. Se activa con display: grid en el contenedor, y columnas y filas se definen con propiedades como grid-template-columns.
Un ejemplo clásico: una galería de tarjetas repartida en tres columnas iguales.
.galeria {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
}La unidad fr ("fracción") reparte el espacio disponible en partes proporcionales: 1fr 1fr 1fr crea tres columnas exactamente iguales, y gap: 20px; añade separación entre ellas y entre filas, sin necesidad de márgenes que luego haya que compensar en los bordes, igual que en Flexbox.
Donde Grid se nota de verdad, frente a Flexbox, es al definir columnas de anchos distintos y coordinados entre sí. Por ejemplo, una plantilla típica de página con barra lateral:
.pagina {
display: grid;
grid-template-columns: 250px 1fr;
gap: 30px;
}Esto crea dos columnas: una fija de 250px para la barra lateral, y otra que ocupa todo el espacio restante para el contenido principal. Si además hubiera varias filas (cabecera, contenido, pie), Grid mantiene esas mismas columnas alineadas verticalmente en todas las filas a la vez, algo que con Flexbox requeriría anidar varios contenedores flex por separado.
Una función muy útil es repeat(), para no repetir el mismo valor a mano:
.galeria {
display: grid;
grid-template-columns: repeat(3, 1fr);
}Y combinada con auto-fit y minmax(), se puede conseguir una cuadrícula responsive sin escribir ni una sola media query:
.galeria {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}Esto le dice al navegador: "encaja tantas columnas de al menos 200px como quepan, y reparte el espacio sobrante entre ellas". En una pantalla ancha caben cinco o seis columnas; en un móvil, puede que solo quepa una, y el propio navegador recalcula cuántas entran en cada momento, sin que haya que definir puntos de ruptura manualmente.
Grid también permite nombrar zonas de la plantilla, lo cual hace que el CSS sea casi autoexplicativo para maquetaciones de página completa:
.pagina {
display: grid;
grid-template-areas:
"cabecera cabecera"
"lateral contenido"
"pie pie";
grid-template-columns: 250px 1fr;
}
.cabecera { grid-area: cabecera; }
.lateral { grid-area: lateral; }
.contenido { grid-area: contenido; }
.pie { grid-area: pie; }
Con este planteamiento, la propia declaración de grid-template-areas dibuja, literalmente, el esquema visual de la página, y cada bloque solo necesita indicar a qué zona pertenece, sin calcular posiciones ni tamaños a mano.
En resumen: si el contenido fluye en una sola dirección (una barra de botones, una fila de tarjetas que se envuelve), Flexbox suele bastar y es más simple. En cuanto se necesita coordinar filas y columnas a la vez (la estructura general de una página, una galería con huecos irregulares), Grid da mucho más control con menos código. No son excluyentes: es habitual usar Grid para la estructura general de la página, y Flexbox dentro de cada bloque para alinear sus elementos internos.
em es relativo al tamaño de fuente del elemento padre más cercano, lo que puede hacer que los tamaños se acumulen si hay elementos anidados. rem es siempre relativo al tamaño de fuente del elemento raíz (html), así que resulta más predecible al no depender del contexto donde se use.
El problema de acumulación con em se ve bien con listas anidadas, un caso clásico donde sorprende a cualquiera que lo prueba por primera vez:
li {
font-size: 1.2em;
}Si una lista tiene sublistas dentro, cada nivel de anidamiento multiplica el tamaño respecto a su propio padre, no respecto a la página: el primer nivel queda a 1.2 veces el tamaño base, el segundo nivel a 1.2 veces el tamaño del primero (es decir, 1.44 veces el original), el tercero a 1.2 veces el segundo (1.728 veces), y así sucesivamente. El texto crece en cascada sin que nadie lo haya pedido explícitamente.
Con rem, el mismo caso no tiene ese problema:
li {
font-size: 1.2rem;
}Da igual cuántos niveles de anidamiento haya: todos los <li> miden 1.2 veces el tamaño de fuente del <html>, ni más ni menos. Por eso rem se ha convertido en la unidad por defecto recomendada para tamaños de fuente en la mayoría de proyectos, precisamente para evitar sorpresas como la anterior.
em sigue teniendo sentido, sin embargo, cuando el objetivo es precisamente que algo escale junto con su elemento padre. Un botón es un buen ejemplo:
.boton {
font-size: 1rem;
padding: 0.75em 1.5em;
}Aquí el padding se define en em a propósito, para que sea relativo al tamaño de fuente del propio botón: si en algún sitio ese botón necesita ser más grande (.boton--grande { font-size: 1.5rem; }), el padding crece proporcionalmente sin tener que recalcularlo a mano, porque escala junto con el texto que contiene.
Una diferencia práctica adicional, importante para accesibilidad: ambas unidades responden si el usuario cambia el tamaño de fuente por defecto del navegador (algo que hacen, por ejemplo, personas con dificultades de visión), a diferencia de px, que se queda fijo pase lo que pase. Por eso conviene evitar tamaños de fuente en píxeles y usar rem como base, reservando em para esos casos concretos donde interesa que algo escale junto con su contexto inmediato, como el padding de un botón o el espaciado interno de un icono.
Son palabras clave que se añaden a un selector para aplicar estilos según un estado o una posición concreta del elemento, sin necesidad de añadir clases extra en el HTML. Por ejemplo, a:hover aplica un estilo cuando el ratón pasa por encima de un enlace, y li:first-child selecciona solo el primer elemento de una lista.
Se pueden agrupar en dos grandes familias, según lo que comprueben:
- Estado del elemento:
:hover(ratón encima),:focus(foco de teclado, como al tabular hasta un campo),:active(mientras se hace clic),:checked(una casilla marcada),:disabled(un campo deshabilitado). - Posición o estructura:
:first-child,:last-child,:nth-child(n),:only-child.
Un ejemplo de las de estado, sobre un botón:
.boton {
background: #1a1a1a;
color: #fff;
}
.boton:hover {
background: #333;
}
.boton:focus {
outline: 2px solid #4a90d9;
}:hover da una pista visual de que el botón es interactivo al pasar el ratón por encima. :focus es igual de importante, aunque se olvida más a menudo: marca visualmente qué elemento tiene el foco cuando alguien navega con el teclado (tabulando con la tecla Tab), algo imprescindible para accesibilidad. Quitar el outline por defecto sin sustituirlo por algo equivalente es un error habitual que deja a esas personas sin saber dónde están dentro del formulario.
Las de posición son muy útiles para dar estilo a listas o tablas sin añadir clases a cada elemento:
tr:nth-child(even) {
background: #f7f7f7;
}
.indaga-faq-item:last-child {
border-bottom: none;
}tr:nth-child(even) colorea de gris claro las filas pares de una tabla, creando el típico efecto de "cebra" que facilita seguir cada fila con la vista, sin marcar nada a mano en el HTML. En el propio plugin de FAQs, :last-child sobre .indaga-faq-item permite quitar el borde inferior solo a la última pregunta de la lista, para que no quede una línea suelta después de la última respuesta.
:nth-child() admite fórmulas más flexibles que even u odd, como :nth-child(3n) para seleccionar uno de cada tres elementos (el 3º, 6º, 9º...), útil por ejemplo para añadir un margen extra cada tres tarjetas en una cuadrícula y así marcar el final visual de cada fila.
Una distinción que conviene tener clara: las pseudo-clases (un solo dos puntos, :hover) seleccionan un elemento existente según su estado o posición; los pseudo-elementos (dos dos puntos, ::before) crean o apuntan a una parte del elemento que no es un nodo real del DOM, como ya se vio en la ampliación de ::before y ::after. Son mecanismos distintos, aunque comparten la misma sintaxis con dos puntos.
Controla el orden de apilamiento de elementos que se superponen: un elemento con un z-index mayor se muestra por encima de otro con un valor menor. Solo funciona en elementos con un position distinto de static (relative, absolute, fixed o sticky).
Un ejemplo directo, con dos cajas que se solapan a propósito:
.caja-fondo {
position: absolute;
top: 20px;
left: 20px;
z-index: 1;
}
.caja-frente {
position: absolute;
top: 40px;
left: 40px;
z-index: 2;
}Aunque .caja-fondo aparezca después en el HTML (lo que normalmente la colocaría "encima" por orden de aparición), .caja-frente se dibuja por delante, porque su z-index es mayor. El valor por sí solo no significa nada en términos absolutos: lo único que importa es la comparación entre los elementos que compiten por el mismo espacio.
El requisito del position distinto de static es la causa más habitual de que z-index "no funcione" a pesar de estar bien escrito:
.menu {
z-index: 999; /* no tiene ningún efecto */
}Sin una propiedad position (relative, absolute, fixed o sticky) en ese mismo selector, el navegador ignora por completo el z-index, sea cual sea su valor. Basta con añadir position: relative;, aunque no se desplace el elemento con top ni left, para que el z-index empiece a tener efecto.
Un caso práctico habitual: un menú desplegable que queda oculto detrás del contenido de la página.
.menu-desplegable {
position: absolute;
z-index: 100;
}
.banner-destacado {
position: relative;
z-index: 50;
}Con estos valores, el menú (100) se muestra siempre por encima del banner (50), independientemente
Permite que un cambio de valor en una propiedad (color, tamaño, posición...) se anime suavemente en lugar de aplicarse de golpe. Por ejemplo, transition: transform .2s ease; hace que cualquier cambio en transform tarde dos décimas de segundo en completarse, como el icono que gira al abrir una pregunta en el plugin de FAQs.
La sintaxis completa admite cuatro valores, en este orden: propiedad, duración, curva de aceleración y retraso.
.boton {
background: #1a1a1a;
transition: background 0.3s ease-in-out 0s;
}
.boton:hover {
background: #333;
}Sin la línea transition, el cambio de color al pasar el ratón por encima del botón sería instantáneo, de un fotograma a otro. Con ella, el navegador interpola automáticamente todos los colores intermedios entre el original y el de :hover a lo largo de 0.3 segundos, sin que haya que calcular ni dibujar esos pasos intermedios a mano.
La curva de aceleración (el tercer valor) cambia bastante la sensación del movimiento, aunque la duración sea la misma:
- linear: velocidad constante de principio a fin, se percibe algo mecánico.
- ease (el valor por defecto): empieza rápido, se ralentiza al final; suele ser el más natural para la mayoría de casos.
- ease-in: empieza lento y acelera; útil para elementos que "salen" de la pantalla.
- ease-out: empieza rápido y frena; útil para elementos que "llegan" y se asientan.
Se pueden animar varias propiedades a la vez, cada una con su propia configuración, separándolas por comas:
.indaga-faq-icono {
transition: transform 0.2s ease;
}
.indaga-faq-pregunta[aria-expanded="true"] .indaga-faq-icono {
transform: rotate(45deg);
}Este es justo el mecanismo detrás del icono "+" del acordeón de preguntas del plugin de FAQs: en reposo no tiene ninguna rotación, y al abrir la pregunta (cuando JavaScript cambia el atributo aria-expanded a "true") pasa a rotar 45 grados, convirtiéndose visualmente en una "x". Gracias a la transition, ese giro se ve como un movimiento fluido en vez de un salto brusco entre los dos estados.
Un matiz importante: no todas las propiedades CSS se pueden animar. Propiedades numéricas o de color (width, opacity, background-color, transform) sí admiten una transición suave, porque el navegador puede calcular valores intermedios. Propiedades como display no se pueden animar de esta forma, porque solo tienen dos estados posibles (mostrado u oculto) sin ningún punto intermedio con sentido; por eso, para animar la aparición o desaparición de un elemento, se suele recurrir a animar opacity o max-height en su lugar, y dejar display: none solo para el estado final ya oculto.
Por rendimiento, conviene priorizar animar transform y opacity frente a propiedades como width, top o margin: las dos primeras las puede gestionar el navegador aprovechando la aceleración por hardware de la tarjeta gráfica, mientras que animar propiedades que afectan a la maquetación obliga al navegador a recalcular la posición de otros elementos en cada fotograma, lo cual puede notarse como una animación menos fluida, sobre todo en páginas con muchos elementos.
px es una medida fija en píxeles, independiente del contexto. % es relativo al tamaño del elemento contenedor. vw y vh son relativos al ancho y alto de la ventana del navegador (1vw es el 1% del ancho visible), lo que resulta útil para que un elemento ocupe siempre una proporción de la pantalla, sea cual sea su tamaño.
La diferencia entre % y vw/vh es la que más confunde al principio, porque ambas parecen "lo mismo" a primera vista. La clave está en respecto a qué se calculan:
.contenedor {
width: 600px;
}
.hijo-porcentaje {
width: 50%; /* 300px: la mitad de su padre, .contenedor */
}
.hijo-viewport {
width: 50vw; /* la mitad del ancho de la VENTANA, no del padre */
}Si la ventana del navegador mide 1200px de ancho, .hijo-porcentaje mide 300px (el 50% de los 600px de su padre), mientras que .hijo-viewport mide 600px (el 50% de los 1200px de la ventana completa), sin importar el tamaño de .contenedor. Son cálculos completamente independientes: uno mira al padre, el otro mira a la ventana entera.
Un uso muy habitual de vh es hacer que una sección ocupe toda la altura visible de la pantalla al cargar la página, típico en la cabecera de una landing page:
.cabecera-principal {
height: 100vh;
}Esto es algo que % no puede conseguir directamente para la altura: un height: 100% solo funciona si todos los elementos padre, hasta llegar al <html>, tienen también una altura definida explícitamente; si algún antepasado no la tiene, el porcentaje no tiene ninguna referencia de la que partir y no hace nada. vh evita ese problema porque siempre mira directamente a la ventana, saltándose toda la cadena de padres.
En cuanto a px, su ventaja es la previsibilidad total: un borde de 1px siempre mide 1 píxel, sin depender de nada más. Por eso sigue siendo habitual para detalles pequeños y fijos, como bordes, sombras o iconos de tamaño exacto, donde no tendría sentido que escalaran con el contenedor o la pantalla. Su inconveniente es justo el contrario: no se adapta a nada, así que anchos y tamaños de fuente pensados para responsive rara vez se definen en px hoy en día.
Un problema práctico de usar vw para tipografía a pantalla completa: en móviles muy estrechos, un texto en 5vw puede quedarse demasiado pequeño para leerse cómodamente, y en pantallas muy anchas puede crecer más de lo razonable. Por eso vw se combina a menudo con clamp(), fijando un mínimo y un máximo mientras se deja que escale con la pantalla entre esos dos límites, como se vio en la
Son valores reutilizables que se definen una vez y se usan en distintas reglas, como --color-principal: #1a1a1a; definido en :root, y luego usado con color: var(--color-principal);. A diferencia de los preprocesadores como Sass, son nativas del navegador y se pueden incluso cambiar dinámicamente con JavaScript.
Lo más habitual es definirlas en :root, un selector que apunta al elemento raíz del documento (<html>), para que estén disponibles en toda la página:
:root {
--color-principal: #1a1a1a;
--color-hover: #333;
--espaciado-base: 1em;
}
.boton {
background: var(--color-principal);
padding: var(--espaciado-base);
}
.boton:hover {
background: var(--color-hover);
}La ventaja frente a repetir #1a1a1a en cada regla es evidente al cambiar de idea más adelante: basta con modificar el valor una vez en :root, y todos los sitios que usan var(--color-principal) se actualizan solos, sin buscar y reemplazar por todo el CSS.
Una diferencia clave frente a las variables de Sass es que las de CSS respetan la cascada y se pueden redefinir dentro de un selector concreto, afectando solo a ese ámbito:
.indaga-faq-selector {
--color-principal: #4a90d9;
}Dentro de .indaga-faq-selector y de todos sus hijos, var(--color-principal) pasa a valer #4a90d9, sin tocar el valor definido en :root para el resto de la página. Con Sass esto no es posible: sus variables se resuelven al compilar el CSS, antes de que el navegador sepa nada de selectores o de la cascada, así que un mismo nombre de variable no puede tener valores distintos según el contexto en el que se use.
var() admite un segundo argumento como valor de reserva, por si la variable no está definida:
.elemento {
color: var(--color-que-no-existe, black);
}En ese caso, se usaría black como valor por defecto, lo cual es útil para escribir componentes que funcionen razonablemente bien aunque el sitio donde se inserten no defina esa variable concreta.
Donde las variables CSS destacan de verdad frente a Sass es que, al ser nativas del navegador, JavaScript puede leerlas y modificarlas en tiempo real, sin recompilar nada:
document.documentElement.style.setProperty('--color-principal', '#c0392b');Esta línea cambia el color en toda la página al instante, en cualquier sitio que use var(--color-principal). Es la base técnica de muchos selectores de tema (claro/oscuro, o de color de acento) que cambian el aspecto de un sitio sin recargar la página: en vez de alternar entre dos hojas de estilo completas, simplemente se reasignan unas pocas variables.
Un uso combinado con prefers-color-scheme, para adaptar automáticamente un tema completo:
:root {
--fondo: #ffffff;
--texto: #1a1a1a;
}
@media (prefers-color-scheme: dark) {
:root {
--fondo: #1a1a1a;
--texto: #f0f0f0;
}
}
body {
background: var(--fondo);
color: var(--texto);
}El resto del CSS del sitio no necesita saber nada sobre modo claro u oscuro: solo usa var(--fondo) y var(--texto), y son las variables las que cambian de valor según la preferencia del sistema detectada por la media query.
Es un lenguaje que añade funcionalidades al CSS normal (variables, anidamiento de selectores, funciones, archivos parciales que se combinan en uno) y que luego se "compila" a CSS estándar para que el navegador lo entienda. Ayuda a organizar hojas de estilo grandes, aunque en proyectos pequeños las variables y el anidamiento nativos de CSS moderno cubren buena parte de esa necesidad sin herramientas adicionales.
El anidamiento es la característica que más se nota al principio. En CSS normal, para dar estilo a los enlaces dentro de un menú hay que repetir el selector del padre en cada regla:
.menu { display: flex; }
.menu a { color: white; }
.menu a:hover { color: gray; }Con Sass, esas mismas reglas se pueden anidar dentro del selector padre, reflejando visualmente la jerarquía del HTML:
.menu {
display: flex;
a {
color: white;
&:hover {
color: gray;
}
}
}El símbolo & hace referencia al selector padre en ese punto, así que &:hover se compila como .menu a:hover. Este archivo .scss no lo entiende el navegador directamente: hace falta un paso de compilación (con una herramienta como Dart Sass, o integrada en el flujo de un framework) que lo convierte en el CSS plano de arriba antes de subirlo al servidor.
Otra ventaja clásica de Sass son los archivos parciales, que permiten dividir una hoja de estilos grande en piezas manejables:
// _variables.scss
$color-principal: #1a1a1a;
// _botones.scss
.boton {
background: $color-principal;
}
// estilos.scss
@import 'variables';
@import 'botones';Al compilar estilos.scss, Sass junta todos esos archivos en un único .css final, así que en producción se sigue sirviendo un solo archivo, sin peticiones adicionales por cada parcial, aunque durante el desarrollo el código quede organizado en piezas pequeñas y con nombre propio.
Las funciones y mixins son la parte que menos tiene equivalente directo en CSS nativo: permiten definir un bloque de estilos reutilizable, con parámetros, que se puede "insertar" en distintas reglas:
@mixin boton-base($color) {
padding: 10px 20px;
border-radius: 4px;
background: $color;
}
.boton-primario {
@include boton-base(#1a1a1a);
}
.boton-peligro {
@include boton-base(#b32d2e);
}Esto evita repetir las mismas tres propiedades en cada variante de botón, cambiando solo el color que recibe el mixin como parámetro; en CSS puro, lo más parecido es combinar una clase base con variables CSS, pero sin la posibilidad de recibir varios parámetros a la vez ni de incluir lógica como bucles.
Dicho esto, el hueco entre Sass y el CSS nativo se ha estrechado bastante en los últimos años: las variables CSS (var(--nombre)) cubren buena parte de lo que antes solo ofrecían las variables de Sass, y el anidamiento nativo (& incluido) ya funciona directamente en los navegadores modernos sin necesidad de compilar nada. Para un proyecto pequeño, como los child themes y plugins que sueles montar sin dependencias externas, evitar el paso de compilación de Sass y quedarse con CSS moderno directamente suele encajar mejor con esa misma filosofía; Sass sigue aportando más ventaja en proyectos grandes, con muchos componentes y varias personas tocando la misma hoja de estilos a la vez.
Es la práctica de escribir primero los estilos pensados para pantallas pequeñas, y luego ampliar o adaptar el diseño para pantallas más grandes usando media queries con min-width. Suele dar hojas de estilo más simples que el enfoque contrario (diseñar para escritorio y luego "encoger" para móvil), porque parte de la versión más restringida y va añadiendo complejidad progresivamente.
El contraste se ve mejor comparando los dos enfoques sobre el mismo elemento: una cuadrícula de tarjetas.
Enfoque "desktop-first" (el contrario), que parte de la versión grande y usa max-width para ir recortando:
.tarjetas {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
@media (max-width: 900px) {
.tarjetas {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 600px) {
.tarjetas {
grid-template-columns: 1fr;
}
}El problema de este orden es que el estilo "por defecto" (sin ninguna media query) es el más complejo: tres columnas, pensado para escritorio. Un móvil tiene que descargar y procesar esa regla completa, aunque ninguna de las dos media queries siguientes le afecte hasta que se aplique la que le corresponde.
El mismo caso en mobile-first, con min-width:
.tarjetas {
display: grid;
grid-template-columns: 1fr;
}
@media (min-width: 601px) {
.tarjetas {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 901px) {
.tarjetas {
grid-template-columns: repeat(3, 1fr);
}
}Ahora el estilo por defecto (sin media query) es el más simple: una sola columna. Cada media query añade una capa más de complejidad a medida que hay más espacio disponible, en lugar de partir de la versión más compleja y ir quitándole cosas. Es el mismo resultado final, pero el camino para llegar hasta ahí es más ordenado de leer y de mantener.
Esto no es solo una cuestión de estilo de escritura: tiene una razón práctica de fondo. En la mayoría de sitios, el tráfico móvil es igual o mayor que el de escritorio, así que tiene sentido que la versión "base" (la que se aplica sin condiciones) sea la pensada para ese caso más común, y que el trabajo extra de las media queries se reserve para las pantallas más grandes, que son la excepción a partir de cierto punto.
Un ejemplo típico de esto aplicado a tipografía y espaciado, no solo a columnas:
body {
font-size: 16px;
padding: 16px;
}
@media (min-width: 768px) {
body {
font-size: 18px;
padding: 32px;
}
}En móvil, texto y márgenes compactos, pensados para aprovechar una pantalla pequeña. A partir de 768px (tablet en adelante), tanto la tipografía como el espaciado crecen, porque hay margen de sobra para hacerlo sin que el contenido se sienta apretado.
Un matiz importante: mobile-first no significa que el sitio en escritorio deba verse "igual que el móvil pero más grande". Significa simplemente que la base de la que se parte, sin condiciones, es la pensada para pantallas pequeñas — el diseño de escritorio puede seguir siendo tan distinto como haga falta, añadido mediante min-width, igual que en el ejemplo de la barra lateral con grid-template-columns que aparecía en la ampliación de CSS Grid.
Son símbolos que se colocan entre dos selectores para indicar una relación concreta entre los elementos que representan, en lugar de aplicar el estilo a cualquier descendiente sin más.
A > B selecciona los elementos B que son hijos directos de A, sin importar cuántos nietos, bisnietos, etc. tenga A más abajo:
.menu > li {
display: inline-block;
}Esto da estilo solo a los <li> que cuelgan directamente de .menu, pero no a los que estén dentro de una lista anidada varios niveles más abajo. Con un selector normal, .menu li (sin el >), el estilo se aplicaría a cualquier <li> descendiente, sin importar cuántos niveles de anidamiento haya de por medio — un submenú desplegable, por ejemplo, también se vería afectado, aunque no fuera esa la intención.
A + B selecciona el elemento B que va inmediatamente después de A, al mismo nivel (el mismo padre):
h2 + p {
margin-top: 0;
}Esto quita el margen superior solo al párrafo que viene justo después de un <h2>, para que el texto quede pegado al título, sin afectar al resto de párrafos del artículo, que sí conservan su espaciado normal entre sí.
A ~ B es parecido a +, pero menos estricto: selecciona todos los elementos B que van después de A al mismo nivel, no solo el inmediato siguiente:
h2 ~ p {
color: #444;
}Aquí, todos los párrafos que aparezcan después de ese <h2> (no solo el primero) reciben ese color, mientras que los párrafos que estén antes del título no se ven afectados. La diferencia con + es justo esa: uno coge solo al vecino inmediato, el otro coge a todos los vecinos posteriores.
Un uso real de + muy habitual: separar visualmente elementos consecutivos sin necesidad de añadir un margen a todos y luego quitárselo al primero:
.indaga-faq-item + .indaga-faq-item {
border-top: 1px solid #e0e0e0;
}Esta regla añade un borde superior a cada pregunta del plugin de FAQs, excepto a la primera de la lista, porque la primera no tiene ningún .indaga-faq-item justo antes de ella al mismo nivel. Es una alternativa más limpia que poner el borde a todas y luego usar :first-child para quitárselo a la primera: con + se consigue el mismo resultado en una sola regla.
En resumen, de más a menos restrictivo: un espacio en blanco selecciona cualquier descendiente a cualquier profundidad; > solo hijos directos; + solo el hermano inmediatamente siguiente; ~ cualquier hermano posterior. Elegir el combinador correcto suele evitar tener que añadir clases extra solo para "delimitar" hasta dónde debe llegar un estilo.
Controla qué ocurre cuando el contenido de un elemento no cabe dentro de su tamaño. overflow: hidden recorta lo que sobra sin mostrarlo. overflow: scroll añade barras de desplazamiento siempre. overflow: auto las añade solo si hace falta, que suele ser la opción más práctica para contenedores con contenido de longitud variable.
Un ejemplo directo, con un contenedor de altura fija y más texto del que cabe:
.caja {
height: 150px;
overflow: auto;
}Mientras el contenido quepa en esos 150px, la caja no muestra ninguna barra de desplazamiento y se ve exactamente igual que si no tuviera esta propiedad. En cuanto el contenido crece más allá de esa altura, aparece automáticamente una barra de scroll vertical, sin necesidad de calcular de antemano si hará falta o no. Es justo esa capacidad de "decidir sola" lo que hace de auto la opción más usada en la práctica, frente a scroll, que mostraría la barra siempre, incluso cuando no hace ninguna falta, dejando un hueco gris vacío al lado del contenido.
overflow: hidden no añade ninguna barra: simplemente recorta lo que se sale del contenedor, sin forma de acceder a ese contenido recortado desde la interfaz (salvo que se controle por JavaScript). Es habitual usarlo para dos cosas muy distintas entre sí:
.avatar {
width: 60px;
height: 60px;
border-radius: 50%;
overflow: hidden;
}Aquí sirve para recortar una imagen más grande que el círculo del avatar, dejando visible solo la parte que cabe dentro de esos 60x60 píxeles redondeados: la imagen original no cambia de tamaño, simplemente se recorta lo que sobresale del círculo.
.contenedor-padre {
overflow: hidden;
}
.hijo-flotante {
float: left;
}Este segundo uso es más una consecuencia histórica que estética: cuando un elemento interior lleva float, el contenedor padre "no se entera" de su altura real, y puede acabar colapsando a una altura de 0 como si estuviera vacío. Añadir overflow: hidden al padre obliga al navegador a tener en cuenta la altura del contenido flotante al calcular el tamaño del contenedor, arreglando ese colapso — es lo que se conoce como un "clearfix" simplificado. Hoy en día, con Flexbox y Grid disponibles, este problema concreto de los floats aparece mucho menos que hace unos años.
Un efecto secundario de overflow: hidden que conviene tener presente: además de recortar el contenido que se sale por los lados, también recorta cualquier elemento hijo posicionado con absolute que intente "escaparse" del contenedor, e incluso puede cortar sombras (box-shadow) o contenido que sobresalga a propósito, como una insignia colocada parcialmente fuera del borde de una tarjeta. Si algo desaparece de forma inesperada al pasar cerca del borde de un contenedor, overflow: hidden en algún ancestro es de los primeros sospechosos a revisar.
Por último, overflow se puede aplicar por separado a cada eje con overflow-x y overflow-y, por ejemplo para permitir scroll horizontal en una tabla ancha (overflow-x: auto;) sin afectar en absoluto al comportamiento vertical del resto de la página.
Es una transición suave entre dos o más colores, generada directamente por CSS sin necesidad de una imagen. Se usa normalmente como fondo, por ejemplo background: linear-gradient(to right, #1a1a1a, #444);, que crea un degradado horizontal de oscuro a gris. También existen gradientes radiales con radial-gradient().
El primer argumento de linear-gradient() define la dirección del degradado. Se puede indicar con palabras clave:
.fondo-1 {
background: linear-gradient(to right, #1a1a1a, #444);
}
.fondo-2 {
background: linear-gradient(to bottom, #1a1a1a, #444);
}
.fondo-3 {
background: linear-gradient(45deg, #1a1a1a, #444);
}to right va de izquierda a derecha, to bottom de arriba a abajo, y con un ángulo en grados (45deg) se consigue cualquier dirección intermedia, medida en el sentido de las agujas del reloj empezando desde arriba. Si no se indica ninguna dirección, el valor por defecto es to bottom.
Se pueden encadenar más de dos colores, y controlar en qué punto exacto empieza cada uno con un porcentaje:
.fondo-tres-colores {
background: linear-gradient(to right, #1a1a1a 0%, #4a90d9 50%, #f0f0f0 100%);
}Sin esos porcentajes, CSS reparte los colores a distancias iguales automáticamente; indicándolos a mano se puede, por ejemplo, hacer que un color ocupe la mitad del degradado y los otros dos se repartan la otra mitad, en vez de un tercio cada uno.
Un uso muy práctico, más allá de lo puramente decorativo: superponer un degradado semitransparente sobre una imagen de fondo para que un texto encima siga siendo legible, sin oscurecer la imagen entera de forma plana:
.cabecera-con-imagen {
background-image:
linear-gradient(to bottom, rgba(0,0,0,0.6), rgba(0,0,0,0)),
url('foto-cabecera.jpg');
background-size: cover;
}Aquí se combinan dos fondos superpuestos: un degradado de negro semitransparente a transparente en la parte de arriba, y debajo la fotografía real. El resultado es que la zona superior de la imagen se oscurece lo suficiente para que un título en blanco resulte legible, mientras que la parte inferior de la foto se mantiene con su color original, sin necesidad de editar la imagen en un programa aparte.
radial-gradient() sigue la misma lógica pero expandiéndose desde un punto central hacia fuera, en círculo o elipse, en vez de en línea recta:
.foco {
background: radial-gradient(circle, #4a90d9, #1a1a1a);
}Esto crea un efecto de "foco de luz", con el color más claro concentrado en el centro y oscureciéndose hacia los bordes, muy usado para botones destacados o fondos con algo de profundidad sin necesidad de una imagen.
Una ventaja práctica de los gradientes CSS frente a usar una imagen de fondo: se recalculan automáticamente si el elemento cambia de tamaño (por ejemplo, en una pantalla más ancha o más estrecha), sin pixelarse ni tener que preparar varias versiones de la misma imagen para distintas resoluciones, y además pesan prácticamente nada en comparación con un archivo de imagen.
Controla cómo se ajusta una imagen o un vídeo dentro de un contenedor de tamaño fijo, cuando sus proporciones no coinciden. object-fit: cover rellena todo el contenedor recortando lo que sobre, manteniendo la proporción. object-fit: contain muestra la imagen completa, dejando espacio vacío si hace falta. Es muy útil para galerías donde las imágenes originales tienen tamaños distintos.
El problema que resuelve se ve claro sin esta propiedad: si se fuerza un ancho y un alto fijos a una imagen con proporciones distintas a las del contenedor, la imagen se deforma, estirándose o comprimiéndose para encajar:
.miniatura {
width: 300px;
height: 200px;
}Sin object-fit, una foto vertical dentro de esta caja horizontal se vería aplastada, con las personas u objetos de la imagen deformados. Añadiendo la propiedad, el navegador respeta las proporciones originales y decide qué hacer con el sobrante según el valor elegido:
.miniatura {
width: 300px;
height: 200px;
object-fit: cover;
}Con cover, la imagen se escala para cubrir por completo los 300x200px, recortando lo que sobre por los lados (o por arriba y abajo, según el caso) para que no quede ningún hueco vacío. Es el comportamiento más habitual para galerías de miniaturas, tarjetas de producto o avatares, donde interesa que todas las imágenes ocupen exactamente el mismo espacio visual, sin importar sus proporciones originales.
.miniatura-completa {
width: 300px;
height: 200px;
object-fit: contain;
}Con contain, en cambio, la imagen se escala para que quepa entera dentro de la caja, sin recortar nada; si sus proporciones no coinciden con las del contenedor, quedan franjas vacías a los lados o arriba y abajo (normalmente del color de fondo del contenedor). Es la opción adecuada cuando mostrar la imagen completa importa más que llenar todo el espacio, por ejemplo con logotipos o capturas de pantalla donde recortar cualquier parte sería un problema.
Existen dos valores adicionales, menos usados pero útiles en casos concretos:
- fill (el valor por defecto si no se especifica nada): estira la imagen para llenar el contenedor exactamente, deformándola si hace falta — es el comportamiento "sin object-fit" de toda la vida.
- none: ignora el tamaño del contenedor y muestra la imagen a su tamaño real, recortando lo que no quepa.
Se puede combinar con object-position para controlar qué parte de la imagen queda visible cuando se recorta con cover, por ejemplo object-position: top; para asegurarse de que, en una foto de una persona, se recorte por abajo y no por arriba, manteniendo siempre la cara visible:
.foto-perfil {
width: 150px;
height: 150px;
object-fit: cover;
object-position: top;
}Antes de que existiera object-fit, conseguir este mismo efecto de "recortar sin deformar" requería trucos como poner la imagen como background-image con background-size: cover en un <div> vacío, en lugar de usar una etiqueta <img> real — un enfoque que funciona visualmente, pero que pierde las ventajas semánticas y de accesibilidad de una imagen de verdad (como el atributo alt).
Es un modificador que se añade a una propiedad para que gane a cualquier otra regla, saltándose las reglas normales de especificidad. El problema es que, si se abusa de él, cuando de verdad hace falta forzar un estilo ya no queda ningún recurso por encima, y depurar por qué un estilo no se aplica se vuelve mucho más difícil. Es preferible resolver los conflictos ajustando la especificidad del selector antes que recurrir a !important.
La sintaxis es sencilla: se añade justo después del valor, antes del punto y coma.
.boton {
background: red !important;
}Con esto, ese fondo rojo gana a cualquier otra regla que afecte al mismo elemento, sin importar cuánta especificidad tenga (un id, varias clases combinadas...), y sin importar en qué orden aparezcan las reglas en el archivo. Es, literalmente, saltarse por completo el sistema de especificidad visto en la FAQ correspondiente.
El problema aparece en cuanto dos reglas distintas usan !important sobre la misma propiedad del mismo elemento: en ese caso, vuelve a decidir la especificidad normal entre ellas, y si empatan también en eso, gana la que aparezca después en el CSS. Es decir, !important no es un comodín absoluto e infalible: solo desplaza el problema un nivel más arriba, y si se usa en varios sitios, se acaba necesitando averiguar cuál de los !important "gana" a cuál, que es un problema bastante más difícil de rastrear que el de especificidad normal que se pretendía evitar.
.boton {
background: red !important;
}
.tema-oscuro .boton {
background: black !important; /* este gana, por especificidad, entre los dos !important */
}Un ejemplo típico de cómo se llega a esta situación sin querer: un desarrollador añade !important para forzar un estilo que "no se aplicaba", sin investigar por qué. Meses después, otro desarrollador (o el mismo, ya sin recordar el motivo) necesita cambiar ese mismo estilo en un caso concreto, descubre que no puede porque hay un !important de por medio, y para solucionarlo añade otro !important todavía más específico. Repetido varias veces, el CSS acaba con capas de !important compitiendo entre sí, y entender qué regla manda realmente requiere leer todo el archivo en vez de una sola línea.
La alternativa casi siempre pasa por subir un poco la especificidad del selector legítimo, en lugar de forzarlo con !important:
/* en vez de esto: */
.boton {
background: red !important;
}
/* mejor esto, aumentando la especificidad de forma controlada: */
.formulario .boton {
background: red;
}Hay, no obstante, un puñado de casos donde !important sí se considera un uso razonable, precisamente porque no hay otra forma limpia de resolverlos:
- Clases de utilidad muy concretas, como
.oculto { display: none !important; }, pensadas para forzar ese comportamiento sin importar qué otras reglas afecten al elemento, en cualquier contexto donde se aplique esa clase. - Sobrescribir estilos inline generados por JavaScript de terceros que Guillermo no controla directamente, donde no hay forma de aumentar la especificidad del selector porque el estilo inline ya tiene, de por sí, más peso que casi cualquier selector normal.
Como norma general: si !important aparece de forma puntual y documentada para un caso concreto como los anteriores, no es motivo de alarma. Si empieza a aparecer repartido por todo el CSS "porque si no, no funciona", suele ser señal de que la estructura de selectores necesita revisarse, no de que haga falta más fuerza bruta.
@font-face permite definir una fuente personalizada indicando su nombre y la ruta a los archivos de la tipografía, por ejemplo @font-face { font-family: "MiFuente"; src: url("mifuente.woff2"); }. Una vez definida, se usa como cualquier otra fuente con font-family: "MiFuente", sans-serif;, con una alternativa de reserva por si no llega a cargar.
La declaración completa suele incluir varios formatos del mismo archivo, para cubrir distintos navegadores:
@font-face {
font-family: "MiFuente";
src: url("mifuente.woff2") format("woff2"),
url("mifuente.woff") format("woff");
font-weight: 400;
font-style: normal;
font-display: swap;
}El navegador prueba los formatos en el orden en que aparecen y usa el primero que sea capaz de leer. woff2 es el formato más moderno y ligero, así que se coloca primero; woff queda como alternativa para navegadores algo más antiguos que no lo soporten. Hoy en día, con woff2 soportado prácticamente en todas partes, muchos proyectos nuevos ya prescinden del resto de formatos antiguos (como ttf o eot), que hacían falta hace años para cubrir navegadores ya obsoletos.
font-weight y font-style dentro de @font-face no son decorativos: le dicen al navegador a qué peso y estilo concretos corresponde ese archivo de fuente. Si una tipografía tiene versión normal y negrita como archivos separados, hacen falta dos bloques @font-face con el mismo font-family pero distinto font-weight:
@font-face {
font-family: "MiFuente";
src: url("mifuente-regular.woff2") format("woff2");
font-weight: 400;
}
@font-face {
font-family: "MiFuente";
src: url("mifuente-bold.woff2") format("woff2");
font-weight: 700;
}Con esto declarado, escribir font-weight: 700; en cualquier selector que use font-family: "MiFuente" carga automáticamente el archivo en negrita real, en lugar de que el navegador intente "inventar" una negrita aplicando un engrosamiento artificial sobre la versión normal, que suele verse peor y más borrosa que una negrita diseñada de verdad para ese peso.
font-display merece explicación aparte, porque afecta directamente a cómo se percibe la carga de la página. Controla qué hace el navegador mientras la fuente personalizada todavía se está descargando:
- swap: muestra el texto de inmediato con una fuente de reserva del sistema, y lo sustituye por la personalizada en cuanto termina de cargar. Evita el llamado "texto invisible" mientras se espera, a cambio de un pequeño salto visual cuando cambia la fuente.
- block (comportamiento por defecto en muchos navegadores si no se especifica nada): oculta el texto durante un breve periodo, a la espera de la fuente personalizada, antes de recurrir a una de reserva si tarda demasiado.
Para un sitio de contenido como Indaga.net, donde el texto debe ser legible cuanto antes, font-display: swap; suele ser la opción más razonable: prioriza que la persona pueda empezar a leer de inmediato, aunque la tipografía final tarde un instante más en aparecer.
Una alternativa cada vez más habitual a autoalojar los archivos de fuente con @font-face es usar un servicio como Google Fonts, que sirve las fuentes desde su propia red de servidores mediante una simple etiqueta <link> o una línea @import. La ventaja de @font-face con archivos propios es no depender de un servicio externo (ni de una petición adicional a otro dominio, con su correspondiente impacto en privacidad y en la propia gestión de caché de Autoptimize y WP Fastest Cache), a cambio de tener que gestionar y actualizar los archivos de fuente manualmente.
Cambia el icono del puntero del ratón al pasar sobre un elemento, lo cual ayuda a comunicar si algo es interactivo. cursor: pointer muestra la manita típica de los enlaces, útil en elementos como botones estilizados que por defecto no la llevarían. cursor: not-allowed indica que una acción no está disponible en ese momento.
El caso más habitual donde hace falta tocar esta propiedad a mano es precisamente el de elementos que se comportan como botones pero no son técnicamente un <a> ni un <button> nativo, o que llevan tanto estilo personalizado que han perdido su apariencia por defecto:
.indaga-faq-pregunta {
cursor: pointer;
}Un <button> de HTML ya muestra cursor: pointer por defecto en la mayoría de navegadores, así que en teoría no haría falta esta línea en el plugin de FAQs. Pero conviene declararlo explícitamente de todas formas, porque algunos resets de CSS o algunos temas sobrescriben ese comportamiento por defecto de los botones (dejando el cursor normal de flecha), y confiar en el valor por defecto del navegador es menos fiable que fijarlo a propósito cuando la interactividad es importante para el usuario.
El valor not-allowed tiene sentido sobre todo combinado con un estado deshabilitado, para reforzar visualmente que esa acción no está disponible en ese momento:
.boton[disabled] {
cursor: not-allowed;
opacity: 0.6;
}Aquí el cursor cambia a un icono de "prohibido" al pasar por encima de un botón deshabilitado, además de atenuarlo visualmente con opacity. Es una señal doble (visual y de comportamiento del puntero) de que hacer clic ahí mismo no va a tener ningún efecto, sin necesidad de que la persona lo intente para descubrirlo.
Existen bastantes más valores además de pointer y not-allowed, cada uno pensado para comunicar un tipo distinto de interacción:
- text: el cursor en forma de "I", el mismo que aparece por defecto sobre texto seleccionable; útil para marcar explícitamente zonas de texto editable dentro de elementos que normalmente no lo mostrarían.
- move: indica que el elemento se puede arrastrar para reordenarlo, habitual en listas con drag-and-drop.
- grab / grabbing: pensado para elementos que se pueden "coger" y desplazar, como un carrusel de imágenes;
grabmientras el ratón está encima sin pulsar, ygrabbingmientras se arrastra activamente. - wait: un icono de carga, para indicar que hay que esperar mientras algo se procesa en segundo plano.
- default: la flecha normal, útil para forzar explícitamente el cursor por defecto en un elemento donde, por herencia de algún ancestro, se hubiera quedado con otro valor sin querer.
Un matiz que conviene recordar: cursor es puramente visual, no cambia el comportamiento real del elemento. Poner cursor: pointer; en un <div> sin ningún addEventListener('click', ...) hace que parezca clicable, pero no hace absolutamente nada al pulsarlo — la propiedad comunica una promesa que el propio código tiene que cumplir después, no la sustituye.
BEM (Block, Element, Modifier) es una convención para nombrar clases CSS de forma predecible: bloque__elemento--modificador. Por ejemplo, faq__pregunta--activa indica que es el elemento "pregunta" dentro del bloque "faq", con el modificador "activa". Ayuda a evitar colisiones de nombres y a entender la relación entre clases solo con leer el nombre.
Los tres conceptos, por separado, son sencillos:
- Bloque: un componente independiente y reutilizable, con sentido por sí mismo, como
faq,menuotarjeta. - Elemento: una parte de ese bloque que no tiene sentido fuera de él, unida con doble guion bajo (
__), comofaq__preguntaofaq__respuesta. - Modificador: una variante de un bloque o elemento, unida con doble guion (
--), comofaq__pregunta--activaoboton--grande.
Aplicado a la estructura del plugin de FAQs, quedaría algo así:
<div class="faq">
<button class="faq__pregunta faq__pregunta--activa">
¿Qué es BEM?
</button>
<div class="faq__respuesta">
...
</div>
</div>.faq { }
.faq__pregunta { }
.faq__respuesta { }
.faq__pregunta--activa { }Con solo leer los nombres de las clases, sin mirar el HTML, ya se entiende la relación entre ellas: faq__pregunta pertenece a faq, y faq__pregunta--activa es una variante de ese mismo elemento. Esto contrasta con anidar selectores para conseguir el mismo resultado:
.faq .pregunta.activa { }Esta alternativa parece más corta, pero tiene dos problemas que BEM evita a propósito. Primero, la especificidad: al combinar varios selectores anidados, esta regla pesa más que una clase sola, lo que complica sobrescribirla más adelante sin añadir todavía más especificidad. Segundo, y más importante a largo plazo: el nombre .pregunta por sí solo, sin el prefijo del bloque, es genérico y fácil que choque con otra clase .pregunta definida en otro componente completamente distinto del sitio, aunque nadie lo haya hecho a propósito.
Una regla clave de BEM, que se nota sobre todo en proyectos grandes con varias personas tocando el mismo CSS: los elementos no se anidan entre sí en el nombre, aunque estén anidados en el HTML. No se escribe faq__pregunta__icono, sino que el icono, si necesita su propia clase, sigue perteneciendo directamente al bloque:
.faq__icono { } /* correcto */
.faq__pregunta__icono { } /* esto NO es la convención BEM */La razón es que un elemento puede moverse de sitio dentro del bloque con el tiempo (por ejemplo, si el icono pasa de estar dentro del botón de la pregunta a estar fuera de él), y si el nombre de la clase reflejara esa jerarquía exacta del HTML, habría que renombrarla cada vez que cambiara la estructura. Manteniendo todos los elementos "planos" bajo el mismo bloque, el nombre de la clase sigue siendo válido aunque cambie dónde está anidado en el HTML.
BEM no es la única convención de nombrado que existe (hay variantes como SUIT CSS o alternativas más ligeras), y tampoco hace falta seguirla al pie de la letra en un proyecto pequeño para beneficiarse de su idea central: prefijar las clases con el nombre del bloque al que pertenecen, en vez de usar nombres sueltos y genéricos como .item, .title o .active, que son los que con más facilidad acaban colisionando entre distintas partes de un sitio con el tiempo.
Controla si los elementos dentro de un contenedor flex deben mantenerse en una sola línea, aunque no quepan (nowrap, el valor por defecto), o pasar a la línea siguiente cuando falte espacio (wrap). Es habitual combinarla con flex-wrap: wrap; en selectores de categorías o listados de tarjetas, para que se reorganicen bien en pantallas estrechas.
El comportamiento por defecto, sin esta propiedad, se ve bien con una fila de botones en un contenedor estrecho:
.selector {
display: flex;
}Con flex-wrap en su valor por defecto (nowrap), si los botones no caben todos en el ancho disponible, el navegador los encoge para que quepan de todas formas, hasta el límite de su min-width. Si ese límite se alcanza y sigue sin caber, el contenido puede llegar a desbordarse fuera del contenedor, algo que casi nunca es el efecto que se busca en un diseño responsive.
.selector {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
}Con wrap, en cuanto un botón no cabe en la línea actual, pasa directamente a la línea siguiente, sin encogerse de forma forzada. Es justo lo que hace el selector de categorías del plugin de FAQs: en una pantalla ancha los botones de categoría caben todos en una fila; en un móvil estrecho, los últimos simplemente bajan a una segunda línea, manteniendo su tamaño legible en lugar de aplastarse.
Existe un tercer valor, menos usado pero con un efecto concreto:
.selector {
flex-wrap: wrap-reverse;
}wrap-reverse hace lo mismo que wrap, pero apilando las líneas en orden inverso: la primera línea que se desborda aparece visualmente abajo del todo, en lugar de arriba. Su uso es bastante específico (por ejemplo, para que un elemento recién añadido en una lista con wrap aparezca siempre en la fila superior visible), y en la práctica se recurre a él mucho menos que a los otros dos valores.
Una propiedad relacionada que conviene conocer junto a flex-wrap es el atajo flex-flow, que combina flex-direction y flex-wrap en una sola declaración:
.contenedor {
flex-flow: row wrap;
}Esto equivale a escribir flex-direction: row; y flex-wrap: wrap; por separado, simplemente en menos líneas. Es habitual encontrarlo así, en una sola propiedad, en hojas de estilo que priorizan la brevedad sobre la explicitud de cada valor por separado.
Un matiz importante cuando se activa wrap: justify-content y align-items siguen actuando dentro de cada línea, pero para controlar cómo se reparte el espacio entre las propias líneas (el espacio vertical sobrante cuando hay varias filas), hace falta una propiedad distinta, align-content, que no tiene ningún efecto si todos los elementos caben en una sola línea.
justify-content controla cómo se reparten los elementos a lo largo del eje principal del contenedor (normalmente horizontal), con valores como center o space-between. align-items controla cómo se alinean en el eje transversal (normalmente vertical), por ejemplo center para centrarlos verticalmente dentro de una fila.
La clave para no confundirlas está en recordar que cada una actúa sobre un eje distinto, y que esos ejes dependen de flex-direction. Con la dirección por defecto (row, en fila horizontal), el eje principal es el horizontal y el transversal el vertical — que es el caso más habitual y el que se ve en los ejemplos siguientes.
Un ejemplo con justify-content, sobre una barra con tres botones:
.barra {
display: flex;
justify-content: center;
}Con center, los tres botones se agrupan juntos en el centro horizontal del contenedor, dejando espacio libre repartido a ambos lados. Los valores más usados de justify-content son:
- flex-start (por defecto): todos los elementos juntos al principio del eje principal.
- center: todos juntos, centrados.
- flex-end: todos juntos al final.
- space-between: el primero pegado al principio, el último pegado al final, y el espacio sobrante repartido a partes iguales entre los elementos intermedios.
- space-around: cada elemento con el mismo espacio a su alrededor (lo que hace que el hueco entre dos elementos sea el doble que el hueco a los extremos, porque ahí se suman dos "medios espacios").
- space-evenly: todos los huecos exactamente iguales, incluidos los de los extremos, a diferencia de
space-around.
align-items, en cambio, entra en juego cuando los elementos tienen alturas distintas entre sí, algo muy habitual cuando unos llevan más texto que otros:
.barra {
display: flex;
align-items: center;
height: 80px;
}Aquí, aunque los elementos dentro de .barra tengan alturas distintas (un icono pequeño junto a un bloque de texto de dos líneas, por ejemplo), align-items: center; los alinea verticalmente por su punto medio, dentro de los 80px de alto del contenedor. Los valores equivalentes en este eje son flex-start (alineados arriba), center (centrados), flex-end (alineados abajo) y stretch (el valor por defecto: todos estirados para ocupar toda la altura disponible, si no tienen una altura propia fijada).
Combinando ambas propiedades se consigue, en una sola regla, uno de los patrones más repetidos en cualquier interfaz: un elemento a la izquierda, otro a la derecha, y ambos centrados verticalmente entre sí, como en una cabecera con logo y menú:
.cabecera {
display: flex;
justify-content: space-between;
align-items: center;
}space-between separa el logo (primer elemento) del menú (último elemento) llevándolos a los extremos, y align-items: center; asegura que, aunque el logo sea una imagen más alta que el texto de los enlaces del menú, ambos queden alineados por su centro vertical, sin que uno parezca "flotar" más arriba o más abajo que el otro.
Un error habitual al empezar con Flexbox es intentar centrar algo con align-items cuando en realidad hace falta justify-content, o al revés, sobre todo al cambiar la dirección del contenedor. Si se añade flex-direction: column;, los dos ejes se intercambian: justify-content pasa a controlar el reparto vertical, y align-items el horizontal — el nombre de cada propiedad no cambia, pero sí lo que controla, precisamente porque ambas actúan siempre sobre "el eje principal" y "el eje transversal", no sobre "horizontal" y "vertical" de forma fija.
Permiten insertar contenido generado por CSS antes o después del contenido real de un elemento, sin añadir una etiqueta extra en el HTML. Se usan junto con la propiedad content, por ejemplo .indaga-faq-icono::after { content: '+'; }, muy habituales para iconos decorativos o pequeños detalles visuales que no forman parte del contenido real.
Lo primero que conviene entender es que ::before y ::after no insertan contenido dentro del HTML que se pueda inspeccionar como un elemento normal: crean un "pseudo-elemento" que existe solo a nivel visual, dentro de la caja del elemento al que se aplican, justo antes o después de su contenido real. Sin la propiedad content, aunque sea vacía (content: '';), no aparecen en absoluto, por muchas otras propiedades que se les definan.
.indaga-faq-icono::after {
content: '+';
font-weight: bold;
}Este es justo el patrón que usa el acordeón de preguntas del plugin de FAQs: en vez de escribir el símbolo "+" directamente en el HTML de cada pregunta, se genera con CSS. La ventaja es doble: por un lado, ese símbolo es puramente decorativo y no debería formar parte del contenido real leído por un lector de pantalla (que ya anuncia el estado abierto/cerrado del botón por otros medios, como el atributo aria-expanded); por otro, cambiarlo más adelante (por ejemplo, a una flecha en vez de un "+") es tan simple como editar una línea de CSS, sin tocar el HTML de cada una de las preguntas.
Un uso muy habitual de ::before es añadir un elemento decorativo delante de un texto, como una comilla grande antes de una cita:
.cita::before {
content: '\201C'; /* comilla tipográfica de apertura */
font-size: 2em;
color: #ccc;
}El valor de content no tiene por qué ser solo texto literal entre comillas: también puede ser un código de carácter Unicode (como en este ejemplo, la comilla tipográfica), el valor de un atributo del propio elemento con attr(), o incluso una imagen con url().
Un caso práctico con attr(), para mostrar el valor de un atributo data-* directamente en el diseño, sin duplicarlo en el HTML visible:
<span data-etiqueta="Nuevo">Producto</span>span::after {
content: ' (' attr(data-etiqueta) ')';
color: #4a90d9;
}Esto añadiría automáticamente el texto " (Nuevo)" después de "Producto", tomando el valor directamente del atributo data-etiqueta, sin tener que escribirlo dos veces en el HTML.
Un uso ya visto en la ampliación del box model: combinar *, *::before, *::after { box-sizing: border-box; } para que también los pseudo-elementos generados hereden ese mismo modelo de caja, ya que, al no ser nodos reales del DOM, no se ven afectados automáticamente por reglas que apunten solo a etiquetas normales.
Dos matices importantes que conviene recordar:
- Los pseudo-elementos generados con
contentson puramente visuales y, aunque técnicamente forman parte del árbol de renderizado, no existen como nodos del DOM: no se pueden seleccionar con JavaScript mediantequerySelector(), ni el texto que contienen es seleccionable con el ratón en todos los navegadores por igual. Por eso nunca deben usarse para contenido con significado real, solo para detalles puramente decorativos. - Cada elemento solo puede tener un
::beforey un::after, no varios; si hace falta más de un elemento generado, hay que recurrir a HTML real.
Permite combinar unidades distintas en un único cálculo dentro de CSS, por ejemplo width: calc(100% - 40px);, que ocupa todo el ancho disponible menos 40 píxeles fijos. Evita tener que calcular ese valor a mano o depender de JavaScript para ajustes que combinan porcentajes con medidas fijas.
El caso de uso más habitual es justo ese: un elemento que debe ocupar "todo el espacio disponible, menos algo fijo". Por ejemplo, un buscador con un botón al lado, donde el campo de texto debe ocupar todo el ancho salvo el que ocupa el botón:
.buscador {
display: flex;
}
.buscador input {
width: calc(100% - 80px);
}
.buscador button {
width: 80px;
}Sin calc(), esto sería imposible de expresar en una sola propiedad: width: 100%; a secas haría que el campo de texto se saliera del contenedor al sumarle los 80px del botón, y calcular el porcentaje exacto a mano (que además cambiaría según el ancho de pantalla) no sería práctico. calc() resuelve esa combinación de unidades directamente en el navegador, recalculándola automáticamente si el contenedor cambia de tamaño.
Admite las cuatro operaciones aritméticas básicas, con una regla estricta de sintaxis que suele pillar a cualquiera la primera vez: debe llevar espacios alrededor de + y - (no así con * y /):
/* correcto */
width: calc(100% - 40px);
/* incorrecto: no funciona, sin espacio alrededor del signo */
width: calc(100%-40px);Esto es así porque, sin esos espacios, el navegador no puede distinguir con seguridad si el signo forma parte del cálculo o si es, por ejemplo, un valor negativo pegado a la unidad anterior. Con * y /, en cambio, esa ambigüedad no existe, así que los espacios ahí son opcionales.
Se pueden anidar varias operaciones y combinar más de dos valores, incluidas otras funciones CSS dentro del propio cálculo:
.columna {
width: calc((100% - 40px) / 3);
}Esto reparte en tres columnas iguales el ancho disponible, después de restarle 40px (por ejemplo, el espacio total ocupado por dos huecos de 20px entre las tres columnas), sin tener que calcular ese resultado final a mano ni depender de que cambie si el contenedor cambia de tamaño.
Otro uso frecuente es combinar calc() con variables CSS, para centralizar un valor base y derivar otros a partir de él:
:root {
--espaciado-base: 16px;
}
.seccion {
padding: calc(var(--espaciado-base) * 2);
}
.subseccion {
padding: var(--espaciado-base);
}Cambiando solo --espaciado-base en un sitio, tanto el padding de .seccion (el doble) como el de .subseccion (el valor base) se recalculan automáticamente, manteniendo siempre esa proporción entre ambos sin tener que actualizar cada valor por separado.
Antes de que clamp() existiera como función independiente, calc() se combinaba también con unidades de viewport (vw) para tipografía fluida, aunque con un cálculo bastante menos legible que el que permite clamp() hoy en día para ese mismo propósito, como se vio en su propia ampliación.
Es una media query que detecta si el usuario tiene activado el modo oscuro en su sistema operativo o navegador, permitiendo adaptar los estilos automáticamente: @media (prefers-color-scheme: dark) { ... }. Es la base para ofrecer un modo oscuro en una web sin necesidad de un interruptor manual, aunque también se puede combinar con uno para que el usuario elija.
Un ejemplo mínimo, adaptando el fondo y el color de texto de toda la página según esa preferencia:
body {
background: #ffffff;
color: #1a1a1a;
}
@media (prefers-color-scheme: dark) {
body {
background: #1a1a1a;
color: #f0f0f0;
}
}El bloque sin media query define el aspecto claro, que actúa como valor por defecto para cualquier navegador que no informe de ninguna preferencia (o que la tenga en claro). El bloque dentro de la media query solo se aplica cuando el sistema operativo del visitante tiene activado el modo oscuro, sobrescribiendo esos dos valores.
Combinarlo con variables CSS, como se veía en su propia ampliación, hace que el resto de la hoja de estilos no tenga que preocuparse en absoluto por el tema activo:
:root {
--fondo: #ffffff;
--texto: #1a1a1a;
--borde: #e0e0e0;
}
@media (prefers-color-scheme: dark) {
:root {
--fondo: #1a1a1a;
--texto: #f0f0f0;
--borde: #444;
}
}
.indaga-faq-item {
border-bottom: 1px solid var(--borde);
}
body {
background: var(--fondo);
color: var(--texto);
}Con este planteamiento, cada componente del sitio (la tarjeta de una FAQ, un botón, una cabecera) simplemente usa las variables --fondo, --texto o --borde, sin que su propio CSS sepa nada sobre si el modo oscuro está activo o no. Toda la lógica del cambio de tema vive en un único sitio: el bloque @media que redefine esas variables.
Un matiz importante: prefers-color-scheme refleja la preferencia del sistema operativo o navegador del visitante, no una elección hecha dentro del propio sitio web. Si además se quiere ofrecer un interruptor manual de claro/oscuro independiente de esa preferencia del sistema, hace falta combinar esta media query con JavaScript: normalmente, guardando la elección manual del usuario (por ejemplo, en localStorage) y aplicándola mediante un atributo en el <html>, como data-tema="dark", que tenga prioridad sobre la preferencia detectada automáticamente:
:root {
--fondo: #ffffff;
}
@media (prefers-color-scheme: dark) {
:root {
--fondo: #1a1a1a;
}
}
:root[data-tema="claro"] {
--fondo: #ffffff;
}
:root[data-tema="oscuro"] {
--fondo: #1a1a1a;
}Con esta estructura, si el usuario no ha elegido nada manualmente, manda la preferencia del sistema (la media query); si ha pulsado explícitamente un botón para forzar un tema, el atributo data-tema tiene más especificidad y gana por encima de la media query, respetando su elección aunque no coincida con la de su sistema operativo.
Existe también prefers-color-scheme: light, para casos donde interese aplicar estilos solo cuando el modo sea explícitamente claro, aunque en la práctica es mucho más habitual definir el tema claro como base (sin media query) y usar solo la variante dark para las diferencias, tal y como se ha hecho en los ejemplos anteriores.
Fija la proporción entre el ancho y el alto de un elemento, por ejemplo aspect-ratio: 16 / 9; para un contenedor de vídeo, de forma que el alto se calcule automáticamente a partir del ancho disponible. Sustituye al truco antiguo de usar padding-bottom en porcentaje para simular una proporción fija, como el que se usa en el wrapper responsive de los vídeos embebidos.
El uso más directo, sobre un contenedor de vídeo con ancho variable:
.video-wrapper {
width: 100%;
aspect-ratio: 16 / 9;
}
.video-wrapper iframe {
width: 100%;
height: 100%;
}Sea cual sea el ancho real que ocupe .video-wrapper en cada momento (a pantalla completa en escritorio, o mucho más estrecho en un móvil), el navegador calcula automáticamente la altura necesaria para mantener siempre la proporción 16:9, sin que haya que fijar un alto en píxeles que solo funcionaría bien para un ancho concreto.
El truco antiguo que sustituye, para quien lo haya usado alguna vez, se basaba en un comportamiento poco intuitivo de padding-bottom en porcentaje (que se calcula respecto al ancho del elemento, no a su alto):
/* la forma antigua de conseguir lo mismo, antes de aspect-ratio */
.video-wrapper-antiguo {
position: relative;
width: 100%;
padding-bottom: 56.25%; /* 9 / 16 = 0.5625 */
}
.video-wrapper-antiguo iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}Funcionaba, pero requería saber de antemano ese cálculo poco intuitivo (dividir alto entre ancho y convertirlo a porcentaje), además de sacar el contenido del flujo normal con position: absolute solo para conseguir el efecto. aspect-ratio consigue exactamente el mismo resultado en una sola línea, legible y sin necesidad de ese posicionamiento adicional.
Más allá de vídeos, es igual de útil para imágenes con proporciones desconocidas de antemano, evitando que la página "salte" mientras terminan de cargar:
.foto-tarjeta {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
}Al fijar la proporción del contenedor de antemano, el navegador ya sabe qué altura reservar para la imagen antes incluso de haberla descargado, evitando el molesto reajuste de la maquetación (lo que se conoce como layout shift) que ocurre cuando una imagen sin dimensiones definidas termina de cargar y "empuja" el resto del contenido hacia abajo.
También admite un único número, interpretado como una proporción directa entre ancho y alto:
.cuadrado {
aspect-ratio: 1;
}Esto fuerza al elemento a ser siempre cuadrado (ancho igual a alto), sea cual sea el ancho real que le corresponda según el resto de la maquetación, útil por ejemplo para avatares o iconos dentro de una cuadrícula donde todas las celdas deben tener la misma proporción.
Fija un valor que se mantiene entre un mínimo y un máximo, pero que puede crecer o encogerse de forma fluida entre esos límites según otra medida, normalmente el ancho de la ventana. Por ejemplo, font-size: clamp(1rem, 2vw, 1.5rem); hace que el tamaño de letra escale con la pantalla, sin bajar de 1rem ni superar 1.5rem.
clamp() recibe siempre tres valores, en este orden: mínimo, valor preferido y máximo.
font-size: clamp(1rem, 2vw, 1.5rem);- 1rem: el tamaño nunca baja de aquí, por muy estrecha que sea la pantalla.
- 2vw: el valor "preferido", el que se usaría si no existieran límites — en este caso, el 2% del ancho de la ventana, que crece y decrece junto con ella.
- 1.5rem: el tamaño nunca sube de aquí, por muy ancha que sea la pantalla.
En pantallas donde 2vw calcularía un valor menor que 1rem (pantallas estrechas), se queda fijo en 1rem. En pantallas donde 2vw superaría 1.5rem (pantallas muy anchas), se queda fijo en 1.5rem. Solo en el rango intermedio, el tamaño real escala de forma fluida junto con el ancho de la ventana, sin saltos bruscos.
Esto resuelve directamente el problema que se mencionaba en la ampliación de vw: usar solo font-size: 2vw; sin límites puede dar un texto demasiado pequeño para leer en un móvil muy estrecho, o desproporcionadamente grande en un monitor muy ancho. clamp() conserva la ventaja de que el texto escale con la pantalla, pero dentro de un rango razonable en ambos extremos.
La gran ventaja práctica frente al enfoque anterior con media queries es que sustituye varios saltos bruscos por una única transición continua:
/* el enfoque anterior, con saltos fijos en cada breakpoint */
h1 {
font-size: 1.5rem;
}
@media (min-width: 600px) {
h1 { font-size: 2rem; }
}
@media (min-width: 1000px) {
h1 { font-size: 2.5rem; }
}
/* lo mismo, con una única línea y una transición fluida */
h1 {
font-size: clamp(1.5rem, 4vw, 2.5rem);
}Con media queries, el tamaño de letra da "saltos" bruscos exactamente en los puntos de ruptura definidos, y se queda fijo entre uno y otro. Con clamp(), el tamaño cambia de forma gradual en cada píxel de ancho de ventana, sin ningún punto donde se note un salto repentino, y con muchas menos líneas de código.
No se limita a tipografía: funciona con cualquier propiedad que acepte una medida, como espaciados o anchos:
.contenedor {
width: clamp(300px, 90%, 1200px);
}Aquí el contenedor ocupa el 90% del ancho disponible como valor preferido, pero nunca baja de 300px (para no quedar demasiado estrecho en un móvil diminuto) ni supera los 1200px (para no estirarse en exceso en un monitor muy ancho), todo en una sola declaración.
Un matiz técnico útil: los tres valores no tienen por qué usar la misma unidad entre sí. Es habitual, como en los ejemplos de arriba, mezclar unidades fijas (rem, px) para los límites mínimo y máximo, con una unidad relativa (vw, %) para el valor preferido que marca la parte fluida — es precisamente esa combinación la que hace que clamp() resulte tan útil frente a usar una sola unidad de un único tipo.
Añade una sombra alrededor de un elemento, definida por un desplazamiento horizontal, uno vertical, un desenfoque, opcionalmente una expansión y un color, por ejemplo box-shadow: 0 2px 6px rgba(0,0,0,0.15);. Se usa mucho para dar sensación de profundidad a tarjetas o paneles, como el que envuelve un cuadro de instrucciones en el panel de administración.
La sintaxis completa admite hasta cinco valores, en este orden:
box-shadow: 0 2px 6px 0 rgba(0,0,0,0.15);
/* | | | | |
| | | | color de la sombra
| | | expansión (opcional)
| | desenfoque
| desplazamiento vertical
desplazamiento horizontal
*/- Desplazamiento horizontal: cuánto se mueve la sombra a la derecha (positivo) o izquierda (negativo) respecto al elemento.
- Desplazamiento vertical: cuánto se mueve hacia abajo (positivo) o arriba (negativo).
- Desenfoque: a mayor valor, más difuminados quedan los bordes de la sombra; en 0, la sombra tiene un borde nítido, sin difuminar.
- Expansión (opcional): agranda o encoge el tamaño de la sombra antes de aplicar el desenfoque, independientemente del tamaño del propio elemento.
- Color: normalmente en
rgba(), para poder controlar también la opacidad de la sombra.
Un ejemplo típico de tarjeta con una sombra suave, sutil, la más habitual en interfaces modernas:
.tarjeta {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}El uso de rgba() en vez de un color plano como black es importante aquí: una sombra con opacidad baja (0.1, en este caso) se mezcla de forma natural con cualquier color de fondo sobre el que se dibuje la tarjeta, mientras que un negro sólido al 100% se vería como una mancha dura y poco realista, sobre todo si el fondo no es blanco puro.
Se pueden apilar varias sombras a la vez separándolas por comas, lo cual permite construir efectos más elaborados que una sola sombra no conseguiría por sí sola:
.tarjeta-elevada {
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.08),
0 4px 12px rgba(0, 0, 0, 0.08);
}Aquí se combinan una sombra pequeña y muy pegada al elemento (que simula el contacto directo con la superficie) con otra más grande y difuminada (que simula una sombra proyectada más lejos), consiguiendo un efecto de profundidad más realista que una única sombra con valores intermedios.
La palabra clave inset, colocada al principio, invierte el efecto y proyecta la sombra hacia dentro del elemento en lugar de hacia fuera:
.campo-con-foco:focus {
box-shadow: inset 0 0 0 2px #4a90d9;
}Este patrón concreto (inset con desplazamientos y desenfoque en 0, solo expansión) es una alternativa habitual a outline o border para marcar el estado de foco de un campo de formulario: dibuja un borde de color hacia dentro del elemento sin alterar su tamaño real ni desplazar el contenido que le rodea, algo que sí podría ocurrir si se cambiara el grosor de un border normal al recibir el foco.
Una diferencia de rendimiento a tener en cuenta frente a filter: drop-shadow() (su alternativa más moderna, pensada sobre todo para formas no rectangulares o imágenes con transparencia): box-shadow siempre sigue la forma rectangular de la caja del elemento, aunque tenga border-radius aplicado la sombra sí se adapta a las esquinas redondeadas, pero nunca sigue el contorno real de una imagen con fondo transparente, como sí hace drop-shadow().
Es la técnica de combinar varias imágenes pequeñas (iconos, por ejemplo) en un único archivo de imagen, y mostrar solo el fragmento necesario en cada elemento usando background-position. Reduce el número de peticiones que el navegador tiene que hacer al servidor, aunque hoy en día muchas de esas necesidades se cubren con iconos de fuente o SVG en línea, más flexibles de colorear y escalar.
La idea de fondo, antes de ver el código: en vez de tener 10 archivos de icono independientes (icono-buscar.png, icono-cerrar.png, icono-menu.png...), se combinan todos en una única imagen grande, colocados uno al lado de otro en una cuadrícula conocida de antemano. Cada elemento que necesita mostrar uno de esos iconos usa esa misma imagen como fondo, pero recortada a la posición exacta donde está el icono que le corresponde.
.icono {
width: 32px;
height: 32px;
background-image: url('sprite-iconos.png');
}
.icono-buscar {
background-position: 0 0;
}
.icono-cerrar {
background-position: -32px 0;
}
.icono-menu {
background-position: -64px 0;
}Aquí sprite-iconos.png es una única imagen con los tres iconos colocados en fila, cada uno de 32x32 píxeles. background-position desplaza esa imagen dentro del recuadro de 32x32px del elemento: en 0 0 se ve el primer icono; en -32px 0, la imagen se desplaza 32px hacia la izquierda, dejando visible el segundo icono; y así sucesivamente. Es exactamente el mismo principio que mover una ventana sobre una imagen fija más grande que ella.
La razón histórica de esta técnica era puramente de rendimiento: hace años, cada archivo de imagen suponía una petición HTTP independiente al servidor, y demasiadas peticiones pequeñas podían ralentizar la carga de una página más que si se combinaban en pocos archivos grandes. Un sprite con 20 iconos suponía una sola petición, en lugar de 20.
Hoy en día, esa ventaja concreta ha perdido bastante peso por dos motivos: por un lado, HTTP/2 (ya extendido en la mayoría de servidores) permite muchas peticiones simultáneas sobre una misma conexión sin el coste que tenían antes; por otro, han aparecido alternativas más flexibles para el mismo problema:
- Iconos de fuente (como Dashicons, que ya usa WordPress en su propio panel de administración): se comportan como texto, así que su color y tamaño se controlan con
coloryfont-size, sin necesidad de generar una imagen distinta para cada color que se necesite. - SVG en línea: al ser vectorial, se ve nítido a cualquier tamaño y resolución de pantalla (incluidas las de alta densidad), y también se puede colorear directamente con CSS mediante la propiedad
fill, algo que un sprite en PNG no permite sin generar una versión distinta de la imagen por cada color necesario.
El sprite sigue teniendo sentido en casos muy concretos: cuando de verdad se necesitan muchas variantes de una misma ilustración con detalle fotográfico (no iconos simples de una sola forma), donde SVG no encaja bien, o al trabajar sobre un sistema heredado que ya usa este enfoque y donde migrar a SVG supondría rehacer una buena cantidad de iconos existentes.
Aplica transformaciones visuales a un elemento sin afectar al flujo del resto de la página: rotarlo (rotate()), escalarlo (scale()), moverlo (translate()) o inclinarlo (skew()). Se usa, por ejemplo, en el icono "+" del acordeón de FAQs, que gira 45 grados con transform: rotate(45deg); al abrir una pregunta.
Lo primero que conviene entender es esa idea de "sin afectar al flujo": a diferencia de cambiar width, margin o top, transformar un elemento con transform no reserva ni libera espacio a su alrededor. Un elemento rotado o desplazado puede visualmente solaparse con sus vecinos, porque estos siguen comportándose como si el elemento transformado ocupara su posición y tamaño originales.
Las cuatro funciones más habituales, con un ejemplo de cada una:
.rotar { transform: rotate(45deg); }
.escalar { transform: scale(1.2); }
.mover { transform: translate(20px, 10px); }
.inclinar { transform: skew(10deg); }- rotate(45deg): gira el elemento 45 grados en el sentido de las agujas del reloj, alrededor de su centro por defecto.
- scale(1.2): agranda el elemento un 20% respecto a su tamaño original (1.0 sería el tamaño normal, 0.5 la mitad).
- translate(20px, 10px): lo desplaza 20px a la derecha y 10px hacia abajo, de forma parecida a
position: relativecontop/left, pero sin sacarlo del flujo y, sobre todo, con mucho mejor rendimiento para animar. - skew(10deg): lo inclina 10 grados, como si se empujara desde un lado, un efecto menos habitual salvo en detalles muy concretos de diseño.
Se pueden combinar varias transformaciones en una sola declaración, separadas por espacios, y se aplican en el orden en que aparecen:
.tarjeta:hover {
transform: scale(1.05) rotate(-2deg);
}Combinado con transition (visto en su propia ampliación), esto crea un efecto muy habitual en tarjetas interactivas: al pasar el ratón por encima, la tarjeta crece ligeramente y se inclina un par de grados, dando una sensación de "elevación" física, y vuelve suavemente a su estado normal al retirar el ratón.
El caso concreto del icono del acordeón de FAQs:
.indaga-faq-icono {
transition: transform 0.2s ease;
}
.indaga-faq-pregunta[aria-expanded="true"] .indaga-faq-icono {
transform: rotate(45deg);
}En reposo, el símbolo "+" no tiene ninguna rotación. Al abrir la pregunta, JavaScript cambia el atributo aria-expanded a "true", lo que activa esta regla y gira el icono 45 grados, convirtiéndolo visualmente en una "x" — sin necesidad de dos iconos distintos (uno para abierto y otro para cerrado), ni de sustituir ningún contenido: es la misma "+" original, simplemente rotada.
Una razón de peso, ya mencionada en la ampliación de transition, para preferir transform frente a animar propiedades como top, left o width: el navegador puede procesar transform (junto con opacity) aprovechando la aceleración por hardware de la tarjeta gráfica, sin necesidad de recalcular la posición del resto de elementos de la página en cada fotograma de la animación. Por eso, para animaciones de movimiento, escala o rotación, transform suele dar un resultado visualmente más fluido que mover un elemento cambiando directamente sus propiedades de posición o tamaño.
La propiedad relacionada transform-origin permite cambiar el punto alrededor del cual giran o escalan estas transformaciones (por defecto, el centro del elemento). Por ejemplo, transform-origin: top left; haría que un rotate() gire el elemento desde su esquina superior izquierda en vez de desde el centro, útil para efectos donde el punto de anclaje visual debe quedar fijo mientras el resto del elemento se mueve a su alrededor.
<div> es un elemento de bloque: ocupa todo el ancho disponible y empieza en una línea nueva. <span> es un elemento en línea: solo ocupa el espacio de su contenido y no rompe el flujo del texto. Por eso <div> se usa para agrupar secciones grandes, y <span> para marcar una parte concreta dentro de un párrafo.
La diferencia se ve mejor con un ejemplo directo, colocando ambos uno junto a otro:
<div>Primer bloque</div>
<div>Segundo bloque</div>Aunque no se les aplique ningún CSS, estos dos <div> aparecen en líneas separadas, uno debajo del otro, porque por defecto ocupan todo el ancho disponible del contenedor. En cambio:
<p>Este texto tiene <span>una parte resaltada</span> en medio de la frase.</p>Aquí <span> no rompe el párrafo en ningún punto: el texto sigue fluyendo con normalidad antes y después de él, y solo ocupa el ancho exacto de "una parte resaltada". Es la diferencia clave entre ambos: <div> piensa en "secciones" de la página; <span> piensa en "fragmentos" dentro de un flujo de texto ya existente.
Un uso típico de <div>, para agrupar contenido relacionado que se quiere tratar como un bloque a la hora de aplicar estilos o JavaScript:
<div class="indaga-faq-item">
<button class="indaga-faq-pregunta">¿Qué es un hook?</button>
<div class="indaga-faq-respuesta">
<p>Un hook es un punto del código...</p>
</div>
</div>Este es justo el patrón que usa el plugin de FAQs: cada pregunta es un <div> que agrupa el botón de la pregunta y el bloque de la respuesta, formando una unidad completa a la que luego se le puede dar estilo o mostrar/ocultar entera con JavaScript, sin tener que tocar sus dos partes por separado.
Un uso típico de <span>, para aplicar un estilo puntual a una sola palabra o frase corta dentro de un texto más largo:
<p>El plugin usa <span style="color: #4a90d9;">consultas preparadas</span> para evitar inyección SQL.</p>Aquí no tendría sentido usar <div>: forzaría un salto de línea antes y después de "consultas preparadas", rompiendo la frase en tres líneas separadas en vez de mantenerla como un único párrafo fluido con una parte resaltada en el medio.
Ninguno de los dos tiene, por sí mismo, ningún significado semántico: son contenedores "neutros", pensados precisamente para los casos donde ninguna etiqueta semántica (<article>, <nav>, <strong>, <em>...) encaja con lo que se quiere agrupar o marcar. Por eso, antes de usar un <div> o un <span> por defecto, conviene comprobar si existe una etiqueta más específica que ya exprese ese significado: por ejemplo, <em> en vez de un <span> con estilo de cursiva, si lo que se busca es dar énfasis real al texto y no solo cambiarle el aspecto visual.
Un matiz técnico relevante para CSS: como <div> es de bloque y <span> es en línea, propiedades como width, height, margin-top o margin-bottom no tienen ningún efecto sobre un <span> tal cual, salvo que se le cambie el display (por ejemplo, a inline-block), como se explicaba en la diferencia entre elementos de bloque y en línea. Un <div>, al ser de bloque desde el principio, sí acepta esas propiedades sin ningún ajuste adicional.
Describe el contenido de la imagen en texto, para que lectores de pantalla puedan comunicárselo a usuarios con discapacidad visual, y para que el navegador muestre algo si la imagen no llega a cargar. Además, los buscadores lo usan para entender de qué trata la imagen, así que también ayuda al SEO.
Un ejemplo básico, con un texto alternativo descriptivo:
<img src="grafico-visitas.png" alt="Gráfico de barras con las visitas mensuales de Indaga.net en 2026">Un lector de pantalla, al llegar a esta imagen, leería en voz alta ese texto en lugar de la imagen (que no puede "ver"), dándole a la persona la misma información que transmite visualmente el gráfico. Si la imagen no llega a cargar por cualquier motivo (una URL rota, una conexión lenta que se corta), el navegador muestra ese mismo texto en el hueco donde debería estar la imagen, en vez de dejar un icono roto sin ninguna explicación.
Redactar un buen alt no es simplemente repetir el nombre del archivo o añadir palabras clave pensando solo en el buscador. Debe describir lo que la imagen aporta al contenido, tal y como lo haría alguien explicándosela en voz alta a otra persona:
<!-- Poco útil: no dice nada sobre el contenido real -->
<img src="img_2847.jpg" alt="imagen">
<!-- Mejor: describe lo que muestra la imagen -->
<img src="img_2847.jpg" alt="Panel de administración del plugin de FAQs, mostrando el listado con buscador y paginación">Un caso especial, y bastante habitual, es el de las imágenes puramente decorativas, que no aportan información relevante al contenido (un separador visual, un fondo repetido, un icono junto a un texto que ya dice lo mismo). Para esos casos, la recomendación no es omitir el atributo, sino dejarlo vacío a propósito:
<img src="separador-decorativo.png" alt="">Un alt="" vacío le indica explícitamente al lector de pantalla que ignore esa imagen por completo, sin leer nada ni interrumpir el flujo de la lectura con una descripción innecesaria. Es distinto de no poner el atributo en absoluto: sin alt, algunos lectores de pantalla llegan a leer el propio nombre del archivo (algo como "img guion bajo dos ocho cuatro siete punto jota pe ge"), que resulta bastante peor que no decir nada.
Cuando una imagen funciona además como enlace, el alt cobra todavía más importancia, porque suele ser la única pista de a dónde lleva ese enlace:
<a href="https://indaga.net">
<img src="logo-indaga.png" alt="Ir a la página de inicio de Indaga.net">
</a>Aquí el alt no describe literalmente el logo (algo como "logotipo con letras negras"), sino la función del enlace, que es lo que de verdad necesita saber alguien que no puede ver la imagen: que ese elemento lleva a la portada del sitio.
Sobre el efecto en SEO que menciona la respuesta corta: los buscadores no pueden "ver" el contenido visual de una imagen (salvo un análisis muy limitado), así que el alt es una de sus pocas fuentes de información directa sobre qué muestra esa imagen, lo cual influye en si esa imagen concreta puede aparecer en una búsqueda de imágenes relacionada con ese contenido. Aun así, conviene escribirlo siempre pensando primero en la persona que no puede ver la imagen, no en el buscador: un buen alt descriptivo suele beneficiar a ambos a la vez, mientras que uno escrito solo para "meter palabras clave" no ayuda a nadie de verdad y puede llegar a resultar contraproducente.
Son etiquetas que describen el significado de su contenido, no solo su apariencia: <header>, <nav>, <main>, <article>, <footer>, en lugar de usar <div> para todo. Ayudan a los lectores de pantalla y a los buscadores a entender la estructura de la página, y hacen el código más legible para cualquiera que lo retome después, incluido tú mismo meses más tarde.
El contraste se ve mejor comparando la misma estructura escrita de las dos formas. Con <div> para todo:
<div class="cabecera">...</div>
<div class="menu">...</div>
<div class="contenido-principal">
<div class="entrada">...</div>
</div>
<div class="pie">...</div>Y la misma estructura con etiquetas semánticas:
<header>...</header>
<nav>...</nav>
<main>
<article>...</article>
</main>
<footer>...</footer>Visualmente, con el mismo CSS aplicado, ambas versiones pueden verse exactamente igual en el navegador. La diferencia está en lo que cada etiqueta comunica sin necesidad de leer clases ni comentarios: <nav> dice "esto es un bloque de navegación" tanto a un lector de pantalla como a cualquier persona que abra el código por primera vez, mientras que <div class="menu"> solo lo dice si alguien se molesta en leer el nombre de la clase, que además podría cambiar sin previo aviso.
Cada una de las etiquetas mencionadas tiene un propósito concreto:
- <header>: contenido introductorio de la página o de una sección, normalmente con el título y la navegación principal.
- <nav>: un bloque de enlaces de navegación, ya sea el menú principal, unas migas de pan o una tabla de contenidos.
- <main>: el contenido principal y único de la página, el que la distingue del resto (excluye cabecera, menú lateral y pie); solo debe haber uno por página.
- <article>: una pieza de contenido independiente y autocontenida, que tendría sentido por sí sola fuera de la página, como una entrada de blog o una noticia.
- <footer>: información de cierre de una sección o de la página completa, como los créditos o los enlaces sociales.
Un lector de pantalla se apoya en estas etiquetas para ofrecer atajos de navegación reales: puede anunciar "hay 3 regiones de navegación en esta página" o permitir saltar directamente al contenido principal (<main>), sin tener que escuchar toda la cabecera y el menú leídos en voz alta cada vez que se visita una página nueva del sitio. Con <div> genéricos, esa información de estructura simplemente no existe para la tecnología asistiva, por bien organizado que esté el CSS.
Los buscadores hacen un uso parecido: al rastrear una página, <article> y <main> ayudan a identificar cuál es el contenido realmente relevante frente al que se repite en cada página del sitio (menú, cabecera, pie), lo cual puede influir en cómo se resume o destaca ese contenido en los resultados de búsqueda.
Un matiz importante: las etiquetas semánticas no sustituyen a <div> y <span> por completo, ni deben forzarse donde no encajan de verdad. Un elemento puramente decorativo o un contenedor que solo existe por motivos de maquetación (para aplicar un display: flex, por ejemplo, sin que represente ninguna sección con significado propio) sigue siendo un buen caso para <div>. La regla práctica es: si existe una etiqueta semántica que describe exactamente lo que ese bloque representa, usarla; si no existe ninguna que encaje, <div> sigue siendo la opción correcta, no un fallo por no usar algo "más moderno".
Un ejemplo real, tomado de la propia estructura del tema de Indaga.net: el archivo header.php envuelve la cabecera en <header id="masthead" class="site-header">, y el menú superior se monta dentro de un <nav> con su propio aria-label, en vez de un <div> genérico — justo el patrón descrito aquí, aplicado a un sitio real en producción.
Permiten guardar información personalizada directamente en una etiqueta HTML, sin recurrir a hacks como clases falsas o atributos inventados. Por ejemplo, <button data-categoria="wordpress"> guarda el valor "wordpress" en el atributo data-categoria, y luego se puede leer desde JavaScript con elemento.dataset.categoria. Los uso, por ejemplo, en el selector de categorías del plugin de FAQs para saber qué grupo de preguntas mostrar u ocultar.
Son dos etiquetas que cumplen funciones distintas y se combinan habitualmente para mostrar código correctamente en una página web.
<pre> (preformatted text)
Le indica al navegador que respete exactamente el formato del texto tal y como está escrito: espacios múltiples, saltos de línea y tabulaciones. Por defecto, HTML colapsa todos los espacios en blanco en uno solo y reorganiza el texto en párrafos fluidos; <pre> desactiva ese comportamiento. Además, los navegadores la renderizan por defecto con una fuente monoespaciada (tipo terminal).
<code>
Es una etiqueta semántica: le indica al navegador, a los lectores de pantalla y a los buscadores que ese contenido es código. No preserva por sí sola el formato del espacio en blanco, pero sí aplica normalmente una fuente monoespaciada vía CSS del navegador.
Por qué se combinan
- <code> sola: pierde los saltos de línea si el texto tiene varios, y semánticamente indica código en línea (inline), no un bloque completo.
- <pre> sola: preserva el formato, pero no marca semánticamente que se trata de código (podría ser un poema, arte ASCII, etc.).
- <pre><code>...</code></pre> juntas: se obtiene lo mejor de ambas — formato preservado (saltos de línea, indentación) y significado semántico correcto de «esto es un bloque de código».
Ejemplo práctico
sudo apt install -f
# o bien:
sudo apt install --fix-brokenCaracteres especiales a tener en cuenta
Dentro de <pre><code>, los caracteres <, > y & deben escaparse como <, > y & si aparecen literalmente en el texto, porque de lo contrario el navegador los interpretará como HTML. Por ejemplo, un comando como comando < archivo > salida necesitaría ese escapado para mostrarse correctamente.
Diferencia con otras formas de mostrar código
Existen alternativas según el contexto: <kbd> para indicar una tecla o combinación que el usuario debe pulsar, <samp> para la salida de un programa, y <var> para variables. Pero para bloques de comandos de terminal o fragmentos de código, la combinación <pre><code> es el estándar más extendido y el más correcto semánticamente.
Es la primera línea de un documento HTML (<!DOCTYPE html>) que le indica al navegador qué versión de HTML debe usar para interpretar la página. Sin ella, algunos navegadores activan un modo de compatibilidad con páginas antiguas que puede alterar cómo se calculan tamaños y estilos.
<button> está pensado para acciones dentro de la página (enviar un formulario, abrir un menú...) y no navega a ninguna URL por sí mismo. <a> está pensado para enlazar a otra página o recurso mediante el atributo href. Usar un <a href="#"> como si fuera un botón suele dar problemas de accesibilidad y de comportamiento.
action indica a qué URL se envían los datos del formulario al enviarlo. method indica cómo se envían: GET los añade a la URL (visibles y limitados en longitud), y POST los envía en el cuerpo de la petición, más adecuado para formularios con muchos campos o datos sensibles.
Es una etiqueta (<iframe>) que incrusta otro documento HTML dentro de la página actual, como una ventana dentro de la ventana. Se usa, por ejemplo, para insertar vídeos de YouTube: la URL del vídeo se coloca en el atributo src y el iframe carga esa página de forma independiente.
id identifica un único elemento en toda la página; no debería repetirse. class se puede aplicar a varios elementos a la vez, y un mismo elemento puede tener varias clases. En CSS, un id tiene más especificidad que una class, así que sus estilos son más difíciles de sobrescribir.
<meta name="viewport" content="width=device-width, initial-scale=1.0"> le dice al navegador móvil que use el ancho real del dispositivo en lugar de simular una pantalla de escritorio y luego reducir la página. Sin ella, un sitio responsive se vería diminuto y habría que hacer zoom para leerlo en el móvil.
<ul> es una lista desordenada, donde el orden de los elementos no importa (se muestran con viñetas). <ol> es una lista ordenada, donde la posición sí tiene significado, como los pasos de una receta (se numeran automáticamente). Ambas usan <li> para cada elemento.
Es el conjunto de prácticas para que una página se pueda usar independientemente de las capacidades de quien la visita: texto alternativo en imágenes, suficiente contraste de color, navegación por teclado, etiquetas semánticas y atributos ARIA cuando hace falta. No es solo una cuestión ética: también mejora el SEO y suele hacer el sitio más robusto en general.
Con GET, los datos viajan como parte de la URL (visibles, limitados en tamaño, y quedan en el historial del navegador), lo que lo hace adecuado para búsquedas o filtros. Con POST, los datos van en el cuerpo de la petición, sin límite práctico de tamaño ni exposición en la URL, por lo que es la opción correcta para formularios que crean o modifican datos, como el alta de una FAQ.
Un enlace con target="_blank" abre la página de destino en una pestaña nueva, pero esa pestaña puede, en navegadores antiguos, acceder a window.opener y redirigir la pestaña original a una página falsa (un ataque conocido como tabnabbing). Se evita añadiendo rel="noopener noreferrer" al enlace, algo que conviene incluir siempre que se usa target="_blank" en contenido de terceros.
Permite ofrecer varias versiones de una misma imagen (distintos tamaños o formatos) y dejar que el navegador elija la más adecuada según el tamaño de pantalla o el soporte de formato, usando varias etiquetas <source> dentro con una <img> de reserva. Sirve para servir imágenes más ligeras en móvil sin tener que redimensionarlas por JavaScript.
Marca un campo como obligatorio, de forma que el navegador impide enviar el formulario si ese campo está vacío, mostrando un aviso nativo sin necesidad de JavaScript. Por ejemplo, <input type="email" required> además comprueba que el texto tenga forma de correo electrónico antes de dejar enviar el formulario.
<label for="categoria"> vinculado a <input id="categoria"> hace que hacer clic en el texto de la etiqueta active también el campo, lo cual amplía la zona clicable y facilita marcar casillas o radios pequeños. Además, los lectores de pantalla anuncian esa etiqueta al llegar al campo, así que sin ella un formulario puede resultar confuso o inutilizable para alguien que navegue por voz.
Un elemento de bloque (como <p> o <h2>) ocupa todo el ancho disponible y empieza siempre en una línea nueva. Un elemento en línea (como <strong> o <a>) solo ocupa el espacio de su contenido y se coloca dentro del flujo del texto, sin forzar un salto de línea. CSS permite cambiar este comportamiento con la propiedad display, pero por defecto cada etiqueta HTML tiene uno de los dos tipos.
Etiquetas como <meta http-equiv="Cache-Control"> pueden sugerir al navegador cómo cachear una página, aunque en la práctica el control real de la caché se suele hacer mejor desde las cabeceras HTTP del servidor, no desde el HTML. Es un motivo más por el que herramientas como Autoptimize o WP Fastest Cache gestionan la caché a nivel de servidor y de archivo, en vez de depender solo de estas etiquetas.
Es el pequeño icono que aparece en la pestaña del navegador, en marcadores y en resultados de búsqueda, identificando visualmente el sitio. Se declara con <link rel="icon" href="favicon.ico"> en el <head>, aunque WordPress también permite configurarlo desde el panel de personalización sin tocar código.
<html lang="es"> indica el idioma principal del contenido de la página. Los lectores de pantalla lo usan para elegir la pronunciación correcta, los traductores automáticos lo usan para saber desde qué idioma traducir, y los buscadores lo tienen en cuenta para mostrar resultados en el idioma adecuado según cada usuario.
Son secuencias que representan caracteres que, de escribirse directamente, el navegador interpretaría como parte del propio código HTML. Por ejemplo, < representa el símbolo < y & representa el símbolo &. Se usan para mostrar código HTML como texto literal en una página, tal y como se ve en ejemplos de código como los de esta FAQ.
Es un tipo de codificación para formularios, indicado con enctype="multipart/form-data", necesario cuando el formulario incluye la subida de un archivo (como en el plugin de importación de Excel). Sin este atributo, el navegador no puede enviar el contenido binario del archivo correctamente junto con el resto de los campos del formulario.
Es la comprobación automática que hace el navegador sobre ciertos campos antes de permitir enviar un formulario, usando atributos como required, type="email", min, max o pattern, sin necesidad de escribir JavaScript. No sustituye a la validación en el servidor (los datos siempre se deben volver a comprobar ahí, porque la del navegador se puede saltar), pero mejora la experiencia al avisar de errores al instante.
Permite incrustar un vídeo directamente en la página, con controles nativos del navegador, sin depender de plugins externos como antiguamente hacía falta con Flash. Admite varias fuentes con <source> para ofrecer distintos formatos, y atributos como controls, autoplay o muted para configurar su comportamiento.
Es el equivalente a <video> pero para contenido de solo sonido: reproduce archivos de audio con controles nativos del navegador, usando <source> para indicar el archivo y su formato. Al igual que con vídeo, si el navegador no soporta ninguno de los formatos indicados, se puede mostrar un texto alternativo dentro de la propia etiqueta.
Permite ofrecer varias versiones de una misma imagen, en distintos tamaños, y dejar que el navegador elija cuál descargar según el tamaño real de pantalla y su densidad de píxeles, ahorrando datos en dispositivos donde no hace falta la versión más grande. Se combina con el atributo sizes para indicarle al navegador qué tamaño va a ocupar la imagen en cada situación.
<thead> agrupa la fila o filas de cabecera de una tabla, y <tbody> agrupa las filas de datos propiamente dichas. Además de aportar significado semántico (útil para lectores de pantalla), permiten aplicar estilos distintos a la cabecera con CSS, y en tablas muy largas el navegador puede incluso mantener la cabecera visible al hacer scroll.
Crean un bloque desplegable nativo del navegador, sin necesidad de JavaScript: <summary> es el texto siempre visible en el que se hace clic, y el resto del contenido dentro de <details> se muestra u oculta automáticamente al pulsarlo. Es una alternativa interesante a construir un acordeón a mano con botones y JavaScript, aunque ofrece menos control sobre la animación de apertura.
Al añadirlo a un <a href="archivo.pdf" download>, el navegador descarga el archivo en lugar de intentar abrirlo directamente en una pestaña nueva. Se le puede dar un valor, como download="informe-2026.pdf", para sugerir un nombre distinto al del archivo original al guardarlo.
<select> crea un desplegable de opciones dentro de un formulario, y cada <option> dentro de él representa una alternativa seleccionable. Se usa, por ejemplo, en el filtro por categoría del panel de administración del plugin de FAQs, generando una <option> por cada categoría existente.
Muestra un texto de ejemplo dentro de un campo de formulario vacío, que desaparece en cuanto el usuario empieza a escribir, como "Buscar preguntas..." en el buscador del plugin de FAQs. No sustituye a una etiqueta <label> real: el placeholder desaparece al escribir, así que no debe ser la única pista sobre qué contiene el campo.
Proporciona una superficie en blanco donde se puede dibujar mediante JavaScript: formas, texto, imágenes o incluso animaciones y gráficos generados dinámicamente, usando su API de contexto 2D (o WebGL para gráficos en 3D). A diferencia de una imagen SVG, lo que se dibuja en un canvas no queda como elementos individuales manipulables, sino como píxeles.
Es una herramienta (disponible en validator.w3.org) que comprueba si el HTML de una página cumple con las especificaciones oficiales, señalando etiquetas mal cerradas, atributos incorrectos o anidamientos no válidos. Un HTML sin errores no garantiza que la página se vea bien, pero sí reduce el riesgo de que distintos navegadores la interpreten de forma distinta entre sí.
var tiene alcance de función y puede dar problemas al reutilizarse dentro de bucles o condicionales. let y const tienen alcance de bloque, más predecible: let permite reasignar el valor más adelante, mientras que const no permite reasignar la variable una vez declarada (aunque si es un objeto o array, su contenido sí se puede modificar).
Una promesa representa el resultado de una operación asíncrona (como una petición a un servidor) que todavía no ha terminado. Puede acabar en éxito (resolve) o en error (reject), y se gestiona con .then() y .catch(), o de forma más legible con async/await.
El DOM (Document Object Model) es la representación en memoria de una página HTML como un árbol de objetos, donde cada etiqueta es un nodo. JavaScript no manipula directamente el HTML del archivo, sino este árbol: cuando cambias el texto de un botón o añades una clase con classList.add(), lo que haces es modificar el DOM, y el navegador se encarga de reflejar ese cambio en pantalla.
Es el comportamiento por el cual las declaraciones de variables (var) y funciones se "suben" mentalmente al principio de su ámbito antes de ejecutar el código, aunque estén escritas más abajo. Con var esto puede dar lugar a variables con valor undefined si se usan antes de asignarles algo. let y const también sufren hoisting, pero quedan en una "zona muerta temporal" que lanza un error si se intentan usar antes de su declaración, lo cual ayuda a detectar fallos antes.
Es una señal de que algo ha ocurrido en la página: un clic, una tecla pulsada, un formulario enviado, la carga completa del documento... Se pueden "escuchar" con addEventListener(), como en boton.addEventListener("click", function () { ... }), para ejecutar código en respuesta a la interacción del usuario.
== compara valores convirtiendo tipos si hace falta ("5" == 5 es true). === compara valor y tipo a la vez, sin conversiones ("5" === 5 es false). Igual que en PHP, conviene usar siempre === para evitar comparaciones con resultados inesperados.
Es el comportamiento por el cual un evento disparado en un elemento se propaga hacia arriba, a través de sus elementos padre, hasta llegar al documento. Por ejemplo, un clic en un botón dentro de un <div> también "burbujea" hasta ese <div>. Se puede aprovechar para escuchar eventos en un contenedor en lugar de en cada elemento hijo, o detener la propagación con event.stopPropagation().
Es una forma más corta de escribir funciones, con la sintaxis (parametros) => expresion. Además de ser más concisa, no crea su propio this: hereda el del contexto donde se define, lo cual evita muchos problemas habituales al usar funciones dentro de callbacks o de métodos de arrays como map() o filter().
JSON (JavaScript Object Notation) es un formato de texto para representar datos estructurados, muy usado para intercambiar información entre el navegador y un servidor. En JavaScript, JSON.stringify(objeto) convierte un objeto o array a texto JSON, y JSON.parse(texto) hace el proceso inverso, reconstruyendo el objeto original a partir del texto.
Es la función moderna del navegador para hacer peticiones HTTP desde JavaScript, sustituyendo a XMLHttpRequest. Devuelve una promesa, así que se suele usar con async/await: const respuesta = await fetch(url); const datos = await respuesta.json();. Se usa, por ejemplo, para consultar la REST API de WordPress sin recargar la página.
Es una referencia que apunta a distintos objetos según cómo se llame a la función donde se usa. Dentro de un método de un objeto, this suele apuntar a ese objeto; en una función normal llamada suelta, puede apuntar al objeto global; en una arrow function, hereda el this del contexto donde se definió. Es una de las fuentes de confusión más habituales para quien empieza con JavaScript.
map() transforma cada elemento y devuelve un array nuevo del mismo tamaño. filter() devuelve un array solo con los elementos que cumplen una condición. reduce() recorre el array acumulando un único resultado, como una suma total. Los tres devuelven un valor nuevo sin modificar el array original.
Es un almacén de datos del navegador donde se pueden guardar pares clave-valor de texto que persisten aunque se cierre la pestaña o el navegador, a diferencia de sessionStorage, que se borra al cerrar la sesión. Se usa con localStorage.setItem("clave", "valor") y localStorage.getItem("clave"). Es lo que uso, por ejemplo con sessionStorage, en el widget de frases aleatorias para no repetir citas ya mostradas.
Es una directiva que activa una versión más rigurosa de JavaScript, que convierte en error explícito comportamientos que de otra forma fallarían en silencio, como usar una variable sin declararla. Se activa escribiendo "use strict"; al principio de un script o de una función, aunque hoy en día es automático dentro de módulos y de clases.
Es una forma compacta de escribir un if/else que devuelve un valor, con la sintaxis condicion ? valorSiVerdadero : valorSiFalso. Por ejemplo, const texto = activo ? "Sí" : "No"; asigna un texto u otro según el valor de activo, en una sola línea.
Es una sintaxis para extraer valores de un array o de un objeto y asignarlos directamente a variables, sin acceder a cada uno por separado. Por ejemplo, const {categoria, pregunta} = faq; crea dos variables, categoria y pregunta, con los valores correspondientes del objeto faq.
Permite "expandir" los elementos de un array o las propiedades de un objeto dentro de otro. Por ejemplo, const copia = [...original]; crea un array nuevo con los mismos elementos, y const combinado = {...objeto1, ...objeto2}; combina las propiedades de dos objetos en uno nuevo, sin modificar los originales.
Es una forma de escribir cadenas de texto usando comillas invertidas (` `) que permite insertar variables directamente dentro del texto con ${variable}, sin necesidad de concatenar con +. También permite escribir texto en varias líneas de forma natural, algo que las comillas normales no permiten sin caracteres de escape.
undefined es el valor que JavaScript asigna automáticamente a una variable declarada pero sin valor, o a un parámetro de función que no se ha pasado. null es un valor que alguien asigna a propósito para indicar "aquí no hay nada", de forma intencionada. Con == ambos se consideran iguales entre sí, pero con === son distintos, porque son tipos diferentes.
Permite acceder a una propiedad anidada de un objeto sin que salte un error si alguno de los niveles intermedios no existe. Por ejemplo, usuario?.direccion?.ciudad devuelve undefined en lugar de un error si usuario o direccion son null o undefined, evitando tener que comprobar cada nivel a mano.
Un array está pensado para listas ordenadas de valores, accesibles por índice numérico (lista[0]), e incluye métodos propios como push() o map(). Un objeto está pensado para agrupar datos con nombre, accesibles por clave (objeto.propiedad o objeto["propiedad"]), sin un orden numérico implícito. Un array, de hecho, es un tipo especial de objeto por debajo.
Es la forma estándar de dividir el código JavaScript en archivos independientes que se pueden importar entre sí. Un archivo exporta lo que quiere hacer disponible con export function algo() {...}, y otro archivo lo usa con import { algo } from "./archivo.js";. Facilita organizar proyectos grandes en piezas pequeñas y reutilizables, en lugar de un único script gigante.
Es una técnica para evitar que una función se ejecute demasiadas veces seguidas, retrasando su ejecución hasta que ha pasado un tiempo sin que se vuelva a llamar. Se usa mucho en buscadores en tiempo real: en vez de filtrar resultados con cada tecla pulsada, se espera, por ejemplo, 300 milisegundos desde la última pulsación antes de ejecutar la búsqueda de verdad, para no sobrecargar de trabajo al navegador o al servidor.
NaN ("Not a Number") es el valor especial que JavaScript devuelve cuando una operación matemática no produce un número válido, como "texto" * 2. Curiosamente, NaN === NaN da false, así que no se puede comprobar con una comparación normal; hay que usar Number.isNaN(valor), que sí detecta correctamente este caso.
Recorre los valores de un elemento iterable, como un array o una cadena de texto, sin necesidad de gestionar un índice manualmente. Por ejemplo, for (const faq of faqs) { console.log(faq.pregunta); } recorre cada elemento del array faqs directamente.
Recorre las claves enumerables de un objeto (o los índices de un array, aunque no es su uso recomendado). Por ejemplo, for (const clave in objeto) { console.log(clave, objeto[clave]); } muestra cada propiedad del objeto con su valor. Para arrays, en la práctica se prefiere for...of o métodos como forEach().
Es una colección de valores únicos: si se intenta añadir un valor que ya existe, simplemente se ignora, sin generar duplicados. Se crea con new Set([1, 2, 2, 3]), que da como resultado un conjunto con solo 1, 2, 3. Es útil, por ejemplo, para obtener las categorías distintas de una lista de FAQs sin tener que comprobar duplicados a mano.
Es una colección de pares clave-valor, similar a un objeto normal, pero que admite cualquier tipo de dato como clave (no solo texto) y mantiene el orden en el que se insertaron los elementos. Se crea con new Map() y se usa con métodos como set(), get() y has(), en lugar de acceder directamente con corchetes como en un objeto.
Es una función que se define y se ejecuta inmediatamente, sin necesidad de llamarla aparte, envolviéndola entre paréntesis: (function () { ... })();. Se usaba mucho antes de que existieran los módulos ES, para crear un ámbito propio y evitar que las variables internas contaminaran el ámbito global de la página.
Es la zona del código donde una variable es accesible. Las variables declaradas con let o const dentro de un bloque ({ }) solo existen dentro de ese bloque; las declaradas fuera de cualquier función son globales y accesibles desde cualquier parte. Entender el ámbito evita errores como sobrescribir sin querer una variable con el mismo nombre usada en otra parte del código.
Es una función que "recuerda" las variables del ámbito en el que fue creada, aunque esa función se ejecute después de que ese ámbito haya terminado. Por ejemplo, una función interna definida dentro de otra puede seguir accediendo a las variables de la función externa incluso después de que esta haya devuelto su resultado. Es la base de patrones como los contadores privados o el debounce.
Permite añadir, quitar o comprobar clases CSS de un elemento sin manipular directamente la cadena de texto de class. Por ejemplo, elemento.classList.add('is-active'), elemento.classList.remove('is-active') o elemento.classList.toggle('is-active') alternan una clase según si ya está presente o no, tal y como hace el selector de categorías del plugin de FAQs.
Se dispara cuando el navegador ha terminado de construir el árbol DOM a partir del HTML, sin esperar a que terminen de cargar imágenes u otros recursos externos. Es el momento adecuado para engachar los listeners de eventos de un script, como se hace en el plugin de FAQs con document.addEventListener('DOMContentLoaded', function () { ... }), para asegurarse de que los elementos ya existen en la página.
Es una función que se pasa como argumento a otra función, para que esta la ejecute en el momento adecuado, normalmente cuando termina una operación. Por ejemplo, en elemento.addEventListener('click', miFuncion), miFuncion es un callback que se ejecuta cada vez que ocurre el clic, no de inmediato al escribir esa línea.
Una ruta absoluta empieza siempre desde la raíz del sistema (/) y no depende de dónde te encuentres, por ejemplo /home/usuario/documentos/archivo.txt. Una ruta relativa parte del directorio en el que estás situado en ese momento, por ejemplo documentos/archivo.txt o ../otra_carpeta. Las rutas relativas son más cómodas para moverse rápido, pero menos fiables dentro de scripts, donde conviene usar rutas absolutas para evitar sorpresas.
Cada archivo tiene tres bloques de permisos: propietario, grupo y resto de usuarios. Dentro de cada bloque, r permite leer, w permite escribir y x permite ejecutar (o entrar, si es un directorio). Por ejemplo, rwxr-xr-- significa que el propietario puede leer, escribir y ejecutar; el grupo puede leer y ejecutar; y el resto solo puede leer. Estos permisos también se pueden representar en números con chmod, por ejemplo chmod 754 archivo.
Un proceso en primer plano ocupa la terminal: no puedes escribir otro comando hasta que termina. Un proceso en segundo plano se lanza añadiendo & al final del comando (por ejemplo, comando &), y te devuelve el control de la terminal mientras sigue ejecutándose. Con jobs puedes ver los procesos en segundo plano de la sesión actual, y con fg traer uno de vuelta a primer plano.
El pipe conecta la salida de un comando con la entrada de otro, encadenando varias herramientas pequeñas en una sola línea. Por ejemplo, cat archivo.txt | grep "error" | wc -l muestra el contenido de un archivo, filtra las líneas que contienen "error" y cuenta cuántas hay. Es una de las ideas centrales de la filosofía Unix: comandos sencillos que hacen una cosa bien, combinados entre sí.
Busca líneas que coincidan con un patrón de texto dentro de uno o varios archivos, o en la salida de otro comando a través de una tubería. Por ejemplo, grep "error" registro.log muestra solo las líneas del archivo que contienen la palabra "error". Con la opción -i ignora mayúsculas y minúsculas, y con -r busca de forma recursiva en carpetas.
apt-get es el gestor de paquetes clásico de Debian y derivadas, con muchas opciones pensadas para scripts. apt es una interfaz más moderna y pensada para uso interactivo, con una salida más clara y una barra de progreso, que combina lo más usado de apt-get y apt-cache en un solo comando. Para uso manual en terminal, apt suele ser la opción recomendada hoy en día.
Es un archivo especial que apunta a otro archivo o carpeta, similar a un acceso directo. Se crea con ln -s /ruta/original /ruta/enlace, y a partir de ahí acceder al enlace equivale a acceder al original. Es muy útil para tener una misma carpeta o archivo accesible desde varias rutas sin duplicar su contenido.
Lista todos los procesos en ejecución en el sistema, con información como el usuario que los lanzó, el uso de CPU y memoria, y el comando exacto que los inició. Se combina muy a menudo con grep para localizar un proceso concreto, por ejemplo ps aux | grep mysql.
kill envía por defecto la señal SIGTERM, que pide al proceso que termine de forma ordenada, dándole ocasión de guardar datos o liberar recursos antes de cerrarse. kill -9 envía SIGKILL, que lo mata de inmediato sin darle esa oportunidad. Conviene probar primero con kill normal, y recurrir a -9 solo si el proceso no responde.
Es el archivo donde se programan tareas para que se ejecuten automáticamente a intervalos definidos, como cada noche o cada hora. Se edita con crontab -e, y cada línea define cuándo y qué comando ejecutar, por ejemplo 0 3 * * * /ruta/script.sh lo lanza todos los días a las 3 de la madrugada.
Muestra el espacio usado y disponible en cada sistema de archivos montado, con tamaños en formato legible (-h de human-readable: KB, MB, GB en vez de bytes). Es el primer comando al que recurrir cuando un servidor empieza a fallar por falta de espacio en disco.
root es el superusuario del sistema, con permisos totales. sudo ("superuser do") permite a un usuario normal ejecutar un comando concreto con esos permisos de forma puntual, dejando constancia en el registro de quién lo hizo. Es más seguro trabajar habitualmente como usuario normal y usar sudo solo cuando hace falta, que iniciar sesión directamente como root.
Muestra las últimas líneas de un archivo y se queda "escuchando" nuevas líneas a medida que se añaden, sin tener que volver a ejecutar el comando. Se usa mucho para vigilar en tiempo real un archivo de log, por ejemplo tail -f /var/log/nginx/error.log mientras se reproduce un problema.
Es un valor con nombre disponible para los procesos del sistema, usado para configuración (rutas, idiomas, claves de acceso...) sin tenerlo que escribir en el propio código. Se puede consultar con echo $NOMBRE y definir con export NOMBRE=valor. Aplicaciones y scripts las leen habitualmente para adaptar su comportamiento sin necesidad de modificar archivos de configuración.
Busca archivos y carpetas dentro de un directorio según distintos criterios: nombre, tamaño, fecha de modificación, tipo... Por ejemplo, find /var/www -name "*.php" busca todos los archivos PHP dentro de esa carpeta y sus subcarpetas. A diferencia de grep, que busca dentro del contenido de los archivos, find busca los propios archivos.
cp copia un archivo o carpeta, dejando el original intacto y creando una copia en el destino. mv mueve el archivo al destino (o lo renombra, si el destino está en la misma carpeta con otro nombre), sin dejar copia en el origen. Ambos aceptan -r para operar sobre carpetas completas de forma recursiva.
Es un proceso que ya ha terminado de ejecutarse, pero cuya información todavía aparece en la tabla de procesos porque el proceso padre no ha leído su código de salida. No consumen CPU ni memoria real, solo una entrada en la tabla de procesos, y normalmente desaparecen solos cuando el proceso padre termina o se reinicia; muchos procesos zombis acumulados sí pueden ser síntoma de un error en la aplicación que los generó.
Muestra el tamaño total ocupado por una carpeta (-s de resumen, sin desglosar cada subcarpeta) en formato legible (-h). Por ejemplo, du -sh wp-content/uploads indica de un vistazo cuánto espacio ocupan los archivos subidos, útil para localizar qué carpeta está llenando el disco antes de recurrir a df -h para ver el total del sistema.
SSH (Secure Shell) es un protocolo para conectarse de forma cifrada a la terminal de otro ordenador o servidor a través de la red, con un comando como ssh usuario@servidor. Permite ejecutar comandos, transferir archivos (con scp o sftp) o administrar un servidor remoto como si se estuviera físicamente delante, sin que la conexión pueda ser interceptada fácilmente.
ext4 es el sistema de archivos más habitual en distribuciones Linux para el disco principal: estable, con buen rendimiento y soporte para journaling, que ayuda a recuperar datos tras un corte de luz o un fallo. Existen alternativas como XFS o Btrfs, con características distintas (Btrfs añade snapshots e integridad de datos avanzada, por ejemplo), pero para la mayoría de servidores web ext4 sigue siendo una opción sólida y de sobra probada.
Cambia el propietario (y, opcionalmente, el grupo) de un archivo o carpeta. Por ejemplo, chown www-data:www-data archivo.php asigna tanto el usuario como el grupo www-data, que suele ser el usuario bajo el que corre el servidor web, algo habitual al ajustar permisos después de subir archivos por otra vía.
Es un atajo que asocia un nombre corto a un comando más largo o más repetitivo. Por ejemplo, alias ll="ls -la" hace que escribir ll ejecute realmente ls -la. Se suelen definir en el archivo de configuración de la shell (como .bashrc) para que estén disponibles en cada sesión nueva.
/ representa la raíz absoluta del sistema de archivos, el punto de partida de cualquier ruta absoluta. ~ es un atajo para la carpeta personal del usuario actual (por ejemplo, /home/guillermo), así que ~/documentos equivale a esa ruta completa, sea cual sea el usuario que la use.
wc (word count) cuenta líneas, palabras y caracteres de un archivo o de la entrada que reciba por una tubería. wc -l archivo.txt cuenta solo las líneas, algo muy usado junto con grep para contar cuántas coincidencias hay, como en grep "error" registro.log | wc -l.
Ambos muestran en tiempo real los procesos en ejecución y su consumo de CPU y memoria. top viene preinstalado en prácticamente cualquier sistema Linux, con una interfaz de texto básica. htop es una alternativa más visual, con colores, barras de uso y la posibilidad de desplazarse o interactuar con el ratón, pero no siempre viene instalada por defecto.
Recibe datos por una tubería y los muestra en pantalla a la vez que los guarda en un archivo, sin tener que elegir entre uno u otro. Por ejemplo, comando | tee salida.log permite ver el resultado en la terminal mientras se guarda también en salida.log para revisarlo más tarde.
Realiza peticiones HTTP (y de otros protocolos) desde la terminal, mostrando la respuesta directamente, sin necesidad de un navegador. Se usa mucho para probar APIs o comprobar cómo responde una URL, por ejemplo curl -I https://indaga.net muestra solo las cabeceras de la respuesta, útil para comprobar redirecciones o códigos de estado.
Ambos descargan contenido de una URL desde la terminal, pero wget está pensado sobre todo para descargar archivos y páginas completas (incluye opciones para descargar sitios enteros de forma recursiva), mientras que curl está más orientado a interactuar con APIs y protocolos diversos, mostrando la respuesta por defecto en lugar de guardarla directamente en un archivo.
Son archivos de configuración que la shell Bash ejecuta automáticamente al iniciar una sesión, donde se suelen definir alias, variables de entorno y ajustes personales de la terminal. .bashrc se carga en sesiones interactivas normales; .bash_profile (o .profile) se carga en sesiones de inicio de sesión, como al conectarse por SSH, y muchas configuraciones hacen que uno cargue al otro para no duplicar ajustes.
Muestra la lista de comandos ejecutados anteriormente en la sesión de terminal, numerados. Se puede volver a ejecutar uno de ellos con !numero, o buscar en el historial pulsando Ctrl+R y escribiendo parte del comando, lo cual ahorra mucho tiempo cuando se repiten comandos largos con frecuencia.
Es una herramienta para sincronizar archivos y carpetas entre dos ubicaciones (locales o remotas por SSH), copiando solo lo que ha cambiado en lugar de todo de nuevo cada vez. Se usa mucho para copias de seguridad incrementales o para desplegar cambios en un servidor, por ejemplo rsync -avz carpeta_local/ usuario@servidor:/ruta/destino/.
Un servicio es un programa que se ejecuta en segundo plano de forma continua, como el servidor web o MySQL. systemctl es la herramienta para gestionar esos servicios en sistemas modernos: systemctl status mysql, systemctl restart nginx o systemctl enable servicio (para que arranque automáticamente al iniciar el sistema).
Un usuario es una cuenta individual con su propio identificador. Un grupo agrupa varios usuarios para poder asignarles permisos comunes de golpe, en lugar de configurarlos uno a uno. Un archivo pertenece siempre a un usuario y a un grupo, y los permisos rwx vistos antes se aplican de forma distinta según si quien accede es el propietario, pertenece al grupo, o es parte del resto de usuarios del sistema.
Muestra el manual de un comando directamente en la terminal, con su descripción, opciones disponibles y ejemplos de uso, por ejemplo man grep. Es la fuente de referencia más fiable para cualquier comando de Linux, siempre disponible sin conexión a internet, y la que suelo consultar como primera fuente al documentar los comandos del plugin de Comandos Linux.
Es el campo (o combinación de campos) que identifica de forma única cada fila de una tabla. No puede repetirse ni quedar vacío, y MySQL crea automáticamente un índice sobre ella, lo que acelera mucho las búsquedas por ese campo. En la mayoría de tablas de WordPress, y en las de mis propios plugins, ese campo suele ser id con AUTO_INCREMENT.
INNER JOIN devuelve solo las filas que tienen coincidencia en ambas tablas. LEFT JOIN devuelve todas las filas de la tabla de la izquierda, tengan o no coincidencia en la otra tabla (rellenando con NULL cuando no la hay). Se usa LEFT JOIN, por ejemplo, para listar todos los posts aunque alguno no tenga comentarios todavía.
Un índice es una estructura auxiliar que MySQL mantiene sobre una o varias columnas para encontrar filas rápidamente, en lugar de recorrer toda la tabla fila por fila. Es como el índice de un libro: en vez de leer página a página, vas directo a donde está lo que buscas. Tiene un coste: ocupa espacio y ralentiza un poco las inserciones, así que conviene indexar las columnas por las que se filtra o se ordena a menudo (como hice con categoria en la tabla de FAQs), no todas.
CHAR(n) reserva siempre un espacio fijo de n caracteres, rellenando con espacios si el texto es más corto. VARCHAR(n) ocupa solo el espacio que realmente necesita el texto, hasta un máximo de n caracteres. Para campos de longitud variable como nombres, títulos o categorías, VARCHAR es casi siempre la opción correcta; CHAR tiene sentido para valores de longitud fija y conocida, como un código postal o un código de país.
Es un campo que enlaza una tabla con la clave primaria de otra, garantizando que el valor referenciado existe realmente. Por ejemplo, en una tabla de comentarios, un campo post_id como clave foránea hacia la tabla de entradas evita que se pueda guardar un comentario apuntando a una entrada inexistente.
ORDER BY cambia el orden en el que se muestran las filas del resultado, sin agruparlas. GROUP BY agrupa filas que comparten un valor en una o varias columnas, normalmente para calcular algo por grupo, como SELECT categoria, COUNT(*) FROM faqs GROUP BY categoria, que cuenta cuántas preguntas hay en cada categoría.
Es un conjunto de operaciones que se ejecutan como un todo: o se completan todas, o no se aplica ninguna. Se inicia con START TRANSACTION, y se confirma con COMMIT o se deshace con ROLLBACK. Es útil cuando varias tablas deben quedar coherentes entre sí, por ejemplo al insertar un pedido y sus líneas de detalle a la vez.
DELETE borra filas una a una (se puede filtrar con WHERE) y se puede deshacer dentro de una transacción. TRUNCATE vacía la tabla entera de golpe, reinicia el contador de AUTO_INCREMENT, y en la mayoría de motores no admite WHERE ni deshacerse con ROLLBACK. Para borrar todo el contenido de una tabla, TRUNCATE suele ser más rápido.
Es una consulta guardada que se comporta como si fuera una tabla: se puede hacer SELECT sobre ella con normalidad, pero los datos reales siguen viviendo en las tablas originales. Sirve para simplificar consultas complejas que se repiten a menudo, dándoles un nombre reutilizable sin duplicar datos.
Es el proceso de organizar las tablas para reducir la información duplicada, dividiendo los datos en tablas relacionadas mediante claves foráneas en lugar de repetirlos en cada fila. Por ejemplo, en vez de escribir el nombre de una categoría en cada FAQ, se podría guardar solo su identificador y mantener el nombre en una tabla de categorías aparte.
Son dos motores de almacenamiento de MySQL. MyISAM es más simple y rápido en lecturas puras, pero no soporta transacciones ni claves foráneas. InnoDB sí soporta transacciones, claves foráneas y bloqueo a nivel de fila, lo que lo hace mucho más robusto para aplicaciones con escrituras frecuentes. Desde hace años, InnoDB es el motor por defecto en WordPress y en la práctica totalidad de proyectos nuevos.
Es una consulta SELECT anidada dentro de otra consulta, cuyo resultado se usa como parte de la consulta principal. Por ejemplo, SELECT * FROM faqs WHERE categoria IN (SELECT categoria FROM faqs WHERE activo = 0) usa el resultado de la subconsulta para filtrar la consulta exterior.
Restringe el número de filas que devuelve una consulta. LIMIT 10 devuelve como máximo 10 filas, y combinado con OFFSET (LIMIT 10 OFFSET 20) permite "saltar" las primeras filas, que es justo la base de la paginación que usa el panel de administración del plugin de FAQs.
Es una técnica en la que alguien introduce código SQL malicioso a través de un campo de entrada (un formulario, una URL...) para manipular una consulta y acceder o modificar datos sin permiso. Se evita nunca concatenando directamente valores del usuario en el SQL, y usando siempre consultas preparadas, como $wpdb->prepare() en WordPress.
Combina los resultados de dos o más consultas SELECT en un único resultado, siempre que tengan el mismo número de columnas y tipos compatibles. Por defecto, UNION elimina filas duplicadas entre los resultados combinados; si se quieren conservar los duplicados, se usa UNION ALL, que además es más rápido al no tener que comprobar duplicados.
Es un tipo de índice pensado para buscar palabras dentro de texto largo de forma más eficiente y flexible que con LIKE '%palabra%', incluyendo relevancia de los resultados. Se crea con FULLTEXT (columna) y se consulta con MATCH (columna) AGAINST ('palabra'). Para tablas pequeñas como una de FAQs, un simple LIKE suele bastar, pero en tablas de miles de artículos marca una diferencia notable.
COUNT(*) cuenta todas las filas del resultado, sin importar si alguna columna tiene valores NULL. COUNT(columna) cuenta solo las filas donde esa columna concreta no es NULL. Por eso, si una columna puede estar vacía, ambos conteos pueden dar números distintos sobre la misma tabla.
Es una columna, normalmente la clave primaria, cuyo valor MySQL asigna automáticamente al insertar una fila nueva, aumentando en uno respecto al último valor usado. Evita tener que calcular manualmente el siguiente identificador disponible, y es la base del campo id en la práctica totalidad de tablas propias, incluida la de FAQs.
columna BETWEEN 10 AND 20 es una forma más legible de escribir columna >= 10 AND columna <= 20: hacen exactamente lo mismo, incluyendo ambos extremos del rango. La diferencia es puramente de estilo y claridad, no de rendimiento ni de resultado.
Es un bloque de código SQL guardado dentro de la propia base de datos, con un nombre, que se puede ejecutar con CALL nombre_procedimiento() en lugar de repetir la misma lógica en cada script. Puede recibir parámetros y ejecutar varias instrucciones seguidas, lo que resulta útil para operaciones que se repiten mucho, aunque en proyectos pequeños suele ser más simple mantener esa lógica en el propio código PHP.
Es una acción que la base de datos ejecuta automáticamente cuando ocurre un evento determinado sobre una tabla, como BEFORE INSERT o AFTER UPDATE. Por ejemplo, se podría usar un trigger para registrar automáticamente en una tabla de auditoría cada vez que se borra una fila de otra tabla, sin tener que acordarse de hacerlo desde el código de la aplicación.
NULL significa "no hay ningún valor", ausencia total de dato. Una cadena vacía ('') es un valor de texto concreto, simplemente sin caracteres. Son conceptualmente distintos: un campo NULL no se puede comparar con = (hay que usar IS NULL), mientras que una cadena vacía sí se compara con normalidad como cualquier otro texto.
Es una herramienta web para administrar bases de datos MySQL o MariaDB sin usar la línea de comandos: permite ver y editar tablas, ejecutar consultas SQL directamente (como los scripts de este mismo plugin), importar y exportar datos, y gestionar usuarios y permisos. Es habitual encontrarla preinstalada en el panel de la mayoría de hostings compartidos.
mysqldump es una herramienta de línea de comandos que exporta la estructura y los datos de una base de datos a un archivo .sql, que luego se puede volver a importar para restaurar esa base de datos tal cual estaba. Es una de las formas más fiables de hacer copias de seguridad antes de un cambio importante, como una migración o una actualización mayor.
Es una clave primaria formada por dos o más columnas combinadas, en lugar de una sola, usada cuando ningún campo por sí solo garantiza que la fila sea única, pero la combinación de varios sí. Por ejemplo, en una tabla de inscripciones podría usarse la combinación de alumno_id y curso_id como clave compuesta, para impedir que un alumno se inscriba dos veces al mismo curso.
Indica que, al borrar una fila de la tabla "padre", MySQL debe borrar automáticamente todas las filas relacionadas en la tabla "hija" que apuntan a ella mediante la clave foránea. Por ejemplo, al borrar una entrada, sus comentarios asociados se borrarían también sin necesidad de una consulta DELETE adicional. Conviene usarlo con cuidado, porque un borrado en cascada puede eliminar más datos de los que se esperaba a simple vista.
Muestra cómo MySQL va a ejecutar realmente una consulta: qué índices usa (o si no usa ninguno), en qué orden recorre las tablas y cuántas filas estima examinar. Se antepone a cualquier SELECT, por ejemplo EXPLAIN SELECT * FROM wp_indaga_faqs WHERE categoria = 'PHP', y es la primera herramienta a la que recurrir cuando una consulta va lenta y no se sabe por qué.
Es un modelo de base de datos donde la información se organiza en tablas con filas y columnas, y donde las relaciones entre distintas tablas se establecen mediante claves (primaria y foránea), en lugar de anidar toda la información en un único documento. MySQL, MariaDB y PostgreSQL son ejemplos de sistemas de bases de datos relacionales, frente a bases de datos no relacionales como MongoDB.
Es una codificación de caracteres que, a diferencia de la antigua utf8 de MySQL (que en realidad solo soportaba 3 bytes por carácter), permite representar el conjunto completo de Unicode, incluidos los emojis y algunos caracteres poco comunes. WordPress la usa por defecto desde hace años, y conviene comprobar que tanto la base de datos como las tablas la tengan configurada para evitar errores al guardar ciertos textos.
Es la configuración de uno o varios servidores "réplica" que copian automáticamente los cambios que ocurren en un servidor "principal", manteniendo una copia sincronizada de los datos. Se usa para repartir la carga de lecturas entre varios servidores, o como parte de una estrategia de alta disponibilidad, de forma que si el servidor principal falla, una réplica pueda asumir su papel.
Es un tipo de columna que solo admite uno de una lista fija de valores de texto definidos al crear la tabla, por ejemplo estado ENUM('activo', 'inactivo', 'pendiente'). Internamente MySQL lo guarda como un número, lo que lo hace eficiente, pero cambiar la lista de valores permitidos más adelante requiere modificar la estructura de la tabla, así que conviene usarlo solo para listas realmente estables.
WHERE filtra las filas antes de agruparlas, y no puede usar funciones de agregación como COUNT() o SUM(). HAVING filtra después de aplicar GROUP BY, y sí puede usar esas funciones de agregación, por ejemplo SELECT categoria, COUNT(*) as total FROM faqs GROUP BY categoria HAVING total > 5, que solo muestra las categorías con más de 5 preguntas.
Modifica la estructura de una tabla ya existente sin necesidad de borrarla y volver a crearla: añadir o quitar columnas, cambiar su tipo, añadir índices o claves. Es la sentencia que se usa, por ejemplo, para añadir el índice único uniq_categoria_pregunta a la tabla de FAQs sin perder ninguna de las filas ya guardadas.
Una copia completa guarda absolutamente todos los datos en cada ejecución, lo que ocupa más espacio pero es la más sencilla de restaurar. Una copia incremental solo guarda los cambios desde la última copia (completa o incremental), lo que ahorra espacio y tiempo, a cambio de que restaurar requiera aplicar la copia completa más todas las incrementales posteriores en orden.
== compara solo el valor, convirtiendo tipos si hace falta (por ejemplo, "5" == 5 es true). === compara valor y tipo a la vez, sin conversiones ("5" === 5 es false). Como norma general, conviene usar siempre === para evitar comparaciones inesperadas.
Es un array donde cada valor tiene asociada una clave de texto en lugar de un índice numérico. Se usa mucho para representar datos con nombre, como la fila de una tabla:
$comando = array(
'nombre' => 'grep',
'ejemplo' => 'grep "texto" archivo.txt'
);
echo $comando['nombre']; // grepAmbos incorporan el contenido de otro archivo PHP al script actual, pero se comportan distinto si el archivo no existe: include lanza un aviso (warning) y el script sigue ejecutándose; require lanza un error fatal y detiene la ejecución. Como norma general, uso require para archivos imprescindibles (como la conexión a la base de datos) e include para piezas opcionales, donde seguir sin ellas no rompe nada importante.
Es una función sin nombre que se puede guardar en una variable, pasar como argumento a otra función, o usar directamente donde se necesite, sin tener que declararla aparte. Es la base de los hooks de WordPress: cuando escribes add_action('init', function() { ... }), estás pasando una función anónima como callback. Con use ($variable) se puede además "capturar" una variable del entorno donde se definió la función.
Es un array cuyos valores son, a su vez, otros arrays, lo que permite representar datos con varios niveles. Por ejemplo, una lista de comandos con sus datos: $comandos = array(array("nombre" => "ls", "ejemplo" => "ls -la"), array("nombre" => "grep", "ejemplo" => "grep texto archivo"));. Se accede a cada nivel encadenando corchetes, como $comandos[0]["nombre"].
for recorre un rango controlado por un contador (for ($i = 0; $i < 10; $i++)), útil cuando se necesita el índice o un control preciso del avance. foreach recorre directamente los elementos de un array, sin gestionar contadores a mano (foreach ($comandos as $comando)), y es la opción más habitual y legible cuando solo hace falta iterar sobre los valores.
Es una función que acepta un número indefinido de argumentos, que PHP agrupa automáticamente en un array dentro de la función usando ... antes del nombre del parámetro. Por ejemplo, function suma(...$numeros) { return array_sum($numeros); } se puede llamar con suma(1, 2, 3) o con cualquier otra cantidad de argumentos.
Es un estilo de programación que organiza el código en clases (plantillas) y objetos (instancias de esas clases), agrupando datos y comportamiento relacionados. Una clase se define con class, sus datos son propiedades y su comportamiento son métodos. WordPress la usa, por ejemplo, en WP_Query o en WP_Widget.
Es una forma de agrupar clases, funciones y constantes bajo un espacio de nombres propio, para evitar colisiones cuando dos librerías definen algo con el mismo nombre. Se declara con namespace MiPlugin; al principio del archivo, y luego se referencia con MiPluginMiClase desde fuera. En plugins sueltos de WordPress no siempre es necesario, pero se vuelve importante cuanto más crece el proyecto.
isset() comprueba si una variable existe y no es null. empty() comprueba si una variable no existe, o si existe pero su valor se considera "vacío" ("", 0, "0", null, false, un array vacío). Por eso empty($variable) es más estricto: una variable con valor 0 pasa isset() pero no pasa empty().
Es el gestor de dependencias estándar de PHP: permite declarar en un archivo composer.json qué librerías necesita un proyecto, y Composer se encarga de descargarlas junto con sus propias dependencias en una carpeta vendor. Es habitual en proyectos PHP a medida, aunque en plugins ligeros como los que suelo montar para Indaga.net procuro evitarlo y no depender de librerías externas.
Es la estructura para capturar errores (excepciones) sin que detengan la ejecución del script. El código que puede fallar se coloca dentro de try { ... }, y si se lanza una excepción, la ejecución salta al bloque catch (Exception $e) { ... }, donde se puede gestionar el error o mostrar un mensaje adecuado en lugar de una pantalla en blanco.
Son los niveles de visibilidad de propiedades y métodos dentro de una clase. public es accesible desde cualquier parte. private solo es accesible desde dentro de la propia clase. protected es accesible desde la clase y desde las que heredan de ella, pero no desde fuera. Ayudan a proteger el estado interno de un objeto de modificaciones no controladas.
Son consultas SQL donde los valores variables se pasan por separado, en lugar de concatenarlos directamente en el texto de la consulta, evitando así inyección SQL. En WordPress se hacen con $wpdb->prepare("SELECT * FROM tabla WHERE id = %d", $id), que se encarga de escapar y dar formato correcto a cada valor antes de ejecutar la consulta.
Permite que una clase reutilice las propiedades y métodos de otra, extendiéndola con class Hijo extends Padre { ... }. La clase hija puede añadir sus propios métodos o sobrescribir los del padre, y acceder a la versión original con parent::metodo() si necesita ejecutar también el comportamiento heredado.
Una interface define qué métodos debe tener una clase, pero no cómo se implementan; una clase puede implementar varias interfaces a la vez. Una clase abstract sí puede incluir código compartido, además de métodos sin implementar que las clases hijas deben completar, pero una clase solo puede heredar de una clase abstracta. En resumen: interface para un "contrato", clase abstracta para compartir comportamiento común.
Es un mecanismo para reutilizar métodos entre clases que no comparten una jerarquía de herencia, algo que PHP no permite con herencia múltiple normal. Se define con trait y se incorpora a una clase con use NombreDelTrait;, como si sus métodos se hubieran copiado directamente dentro de la clase.
json_encode() convierte un array o un objeto PHP en una cadena de texto en formato JSON, lista para enviar a JavaScript o guardar en un archivo. json_decode() hace el proceso inverso: convierte una cadena JSON en un array o un objeto PHP; con el segundo parámetro a true devuelve un array asociativo en lugar de un objeto stdClass.
echo imprime directamente algo en la salida, en el punto donde se ejecuta. return devuelve un valor a quien llamó a la función, sin imprimir nada por sí mismo, y termina la ejecución de la función en ese punto. Una función bien diseñada normalmente debería usar return para devolver datos, y dejar que sea quien la llama quien decida si los imprime o los usa de otra forma.
Es un array asociativo con información sobre la petición HTTP actual y el servidor: la URL solicitada ($_SERVER["REQUEST_URI"]), el método usado ($_SERVER["REQUEST_METHOD"]), la IP del visitante ($_SERVER["REMOTE_ADDR"]) y muchos otros datos que PHP rellena automáticamente en cada petición, sin que el desarrollador tenga que calcularlos.
Aplica una función a cada elemento de un array y devuelve un array nuevo con los resultados, sin modificar el original. Por ejemplo, array_map("strtoupper", $palabras) devuelve todas las palabras en mayúsculas. Es el equivalente en PHP al map() de los arrays en JavaScript.
self:: siempre hace referencia a la clase donde está escrito el código, aunque se llame desde una clase hija. static:: hace referencia a la clase que realmente se está usando en tiempo de ejecución (lo que se conoce como late static binding), lo que permite que una clase hija sobrescriba un método y que ese cambio se respete incluso cuando lo llama código heredado de la clase padre.
password_hash() convierte una contraseña en una cadena cifrada de forma irreversible (con sal incluida automáticamente), y password_verify() comprueba si una contraseña introducida coincide con ese hash, sin necesidad de descifrarlo nunca. Guardar contraseñas en texto plano es un riesgo grave: si la base de datos se filtra, todas las cuentas quedan expuestas directamente, en lugar de protegidas por un cifrado que, aunque la base de datos se filtre, sigue siendo muy costoso de romper.
Son métodos con nombres reservados que PHP llama automáticamente en ciertas situaciones, sin que haya que invocarlos a mano. __construct() se ejecuta al crear un objeto con new; __get() y __set() se ejecutan al leer o asignar una propiedad que no existe directamente en la clase, lo que permite controlar ese acceso de forma personalizada.
Desde PHP 8.1, un enum es un tipo que representa un conjunto cerrado de valores posibles con nombre, por ejemplo enum Estado { case Activo; case Inactivo; }. Sustituye al patrón antiguo de usar constantes sueltas o cadenas de texto para representar opciones fijas, con la ventaja de que PHP comprueba que solo se usen los valores definidos.
Recorre un array y devuelve uno nuevo solo con los elementos que cumplen una condición, definida en una función que se le pasa como segundo argumento. Por ejemplo, array_filter($faqs, function($f) { return $f['activo'] == 1; }) devuelve solo las FAQs activas, sin modificar el array original.
Recorre un array acumulando un único resultado a partir de todos sus elementos, aplicando una función que combina el acumulado con cada valor. Por ejemplo, se puede usar para sumar el total de un array de precios, pasando una función que va sumando cada precio al total acumulado hasta el momento.
Ambos crean una constante, pero define('NOMBRE', valor) se puede usar en cualquier punto del código, incluso condicionalmente, y evalúa el valor en tiempo de ejecución. const NOMBRE = valor; se evalúa en tiempo de compilación, solo admite valores simples (no el resultado de una función), y se usa sobre todo dentro de clases o en el ámbito global de un archivo.
Devuelve el valor de la izquierda si existe y no es null; si no, devuelve el de la derecha. Por ejemplo, $nombre = $_GET['nombre'] ?? 'invitado'; evita el aviso de "índice indefinido" y asigna "invitado" si ese parámetro no llegó en la URL, en una sola línea en vez de un isset() con condicional.
Es la posibilidad de indicar explícitamente qué tipo de dato espera un parámetro de función o qué tipo devuelve, por ejemplo function suma(int $a, int $b): int { return $a + $b; }. PHP comprueba esos tipos en tiempo de ejecución y lanza un error si no coinciden, lo que ayuda a detectar fallos antes y hace el código más fácil de entender sin necesidad de leer la implementación.
error_reporting(E_ALL) le pide a PHP que informe de todos los tipos de error, incluidos avisos menores que de otra forma pasarían desapercibidos, como usar una variable no definida. Combinado con ini_set('display_errors', 1) durante el desarrollo, ayuda a detectar fallos pequeños antes de que lleguen a producción, donde en cambio conviene ocultarlos al visitante y guardarlos solo en un log.
Construye una cadena de texto sustituyendo marcadores de posición por valores, con control sobre el formato (decimales, relleno con ceros, alineación...). Por ejemplo, sprintf('Importación %s eliminada', $id) sustituye %s por el valor de $id. WordPress la usa mucho en textos traducibles, como en los mensajes de "eliminada" del plugin de FAQs.
serialize() convierte un array u objeto PHP en una cadena de texto que conserva su estructura, para poder guardarla en un campo de texto de la base de datos. unserialize() hace el proceso inverso. WordPress lo usa internamente en varias opciones, aunque hay que tener cuidado al hacer búsquedas y reemplazos directos en la base de datos, porque pueden romper el formato serializado si no se ajustan también las longitudes indicadas dentro de la cadena.
Es un mecanismo que permite cargar automáticamente el archivo de una clase la primera vez que se usa, sin tener que escribir un require por cada una al principio del script. Se configura con spl_autoload_register(), indicando una función que, dado el nombre de una clase, sepa deducir en qué archivo se encuentra. Composer genera automáticamente este autoload siguiendo el estándar PSR-4 para los proyectos que lo usan.
El tema controla el aspecto visual del sitio (maquetación, tipografías, colores). El plugin añade funcionalidad, independientemente de qué tema esté activo. Por eso, si cambias de tema, un plugin sigue funcionando igual, pero si desactivas un plugin, pierdes lo que hacía sin que el diseño se vea afectado.
Un hook es un punto del código de WordPress donde puedes "engancharte" para ejecutar tu propia función, sin tocar el núcleo. Las acciones (add_action()) ejecutan algo en un momento concreto (por ejemplo, al publicar un post). Los filtros (add_filter()) modifican un dato antes de que se muestre (por ejemplo, el contenido de una entrada) y siempre deben devolver un valor.
Es el bucle estándar que usa WordPress para recorrer y mostrar entradas, ya sea en la portada, en un archivo de categoría o en una página de resultados de búsqueda. Se apoya en funciones como have_posts() y the_post(), y dentro de él se usan las etiquetas de plantilla (the_title(), the_content()...) para pintar cada entrada. Prácticamente cualquier tema, incluido Twenty Seventeen, construye sus plantillas alrededor de este bucle.
Las entradas están pensadas para contenido cronológico y organizable: llevan fecha, se pueden clasificar por categorías y etiquetas, y aparecen en el feed RSS. Las páginas son para contenido más estático y jerárquico, como "Sobre mí" o "Aviso legal": no tienen categorías ni etiquetas, pero sí pueden tener páginas padre e hijas. Como norma práctica, si el contenido tiene sentido en una línea de tiempo, es una entrada; si es información fija de la web, es una página.
Es un tipo de contenido distinto de las entradas y páginas por defecto, pensado para estructurar información propia (por ejemplo, "Productos" o "Eventos"). Se registra con register_post_type(), normalmente en el hook init, y a partir de ahí WordPress lo trata igual que a un post: tiene su propia pantalla de administración, permalinks y, si se declara, soporte para campos personalizados.
Es un sistema de clasificación propio, similar a las categorías o etiquetas de las entradas, pero aplicado a cualquier tipo de contenido. Se registra con register_taxonomy() y se puede asociar a uno o varios post types. Por ejemplo, un custom post type "Recetas" podría tener la taxonomía "Tipo de cocina".
Es una etiqueta corta entre corchetes, como [indaga_faqs], que WordPress sustituye por el HTML que devuelve una función PHP asociada. Se registra con add_shortcode() y permite insertar funcionalidad dinámica dentro del contenido de una entrada o página sin tocar plantillas.
Es un bloque de contenido que se puede colocar en zonas concretas del tema, como la barra lateral o el pie de página, sin necesidad de tocar código. WordPress trae widgets básicos (texto, últimas entradas, menú de navegación...), y los plugins pueden registrar los suyos propios extendiendo la clase WP_Widget.
wp_register_script() declara un script (su URL, dependencias y versión) sin cargarlo todavía; sirve para tenerlo disponible y que otros plugins o el propio tema lo encolen cuando lo necesiten. wp_enqueue_script() lo declara y lo carga de inmediato en la página. En la práctica, si el script se usa siempre, basta con wp_enqueue_script() directamente.
Es el archivo donde un tema (o un child theme, como el de Indaga.net) engancha su propia lógica: registrar menús, encolar estilos y scripts, añadir filtros y acciones, crear shortcodes... Se ejecuta en cada carga del sitio, así que conviene mantenerlo organizado y no sobrecargarlo con lo que en realidad debería ser un plugin.
Es un tema que hereda la plantilla y los estilos de otro (el tema padre, en tu caso Twenty Seventeen) y permite sobrescribir solo lo necesario, en archivos propios. La ventaja es que al actualizar el tema padre no se pierden las personalizaciones, porque viven en una carpeta aparte que WordPress no toca.
Es una interfaz que expone los datos del sitio (entradas, páginas, usuarios, taxonomías...) en formato JSON a través de URLs como /wp-json/wp/v2/posts. Permite que aplicaciones externas, o el propio JavaScript del front end, lean o modifiquen contenido sin pasar por el HTML tradicional de WordPress.
Es un token de un solo uso que WordPress genera para verificar que una petición (enviar un formulario, hacer clic en un enlace de borrado...) proviene realmente de la página donde se mostró, y no de un tercero. Se crea con funciones como wp_nonce_field() y se valida con check_admin_referer() o wp_verify_nonce(), como en el panel de administración del plugin de FAQs.
wp-admin contiene el núcleo del panel de administración de WordPress: no se debe modificar, porque se sobrescribe en cada actualización. wp-content es donde vive todo lo personalizado del sitio: temas, plugins y archivos subidos (carpeta uploads). Es la única carpeta que de verdad hay que respaldar con cuidado.
Es la clase que WordPress usa internamente para consultar entradas según casi cualquier criterio: categoría, autor, rango de fechas, meta datos... Se puede instanciar directamente en una plantilla para crear un listado personalizado, por ejemplo new WP_Query(array("post_type" => "post", "posts_per_page" => 5)), sin tocar la consulta principal de la página.
Son una forma de guardar datos en caché durante un tiempo determinado, usando la misma tabla wp_options pero con expiración automática. Se guardan con set_transient("clave", $valor, 3600) (una hora, en este ejemplo) y se leen con get_transient("clave"), que devuelve false si ya caducó. Son útiles para no repetir cálculos o peticiones costosas en cada carga de página.
Un rol (administrador, editor, autor, colaborador, suscriptor) es un conjunto con nombre de capacidades concretas, como publish_posts o manage_options. En vez de comprobar el rol directamente, lo correcto es comprobar la capacidad con current_user_can("manage_options"), porque los roles se pueden personalizar y una misma capacidad puede pertenecer a varios roles.
Es un modo de instalación que permite gestionar varias webs (una red) desde una única instalación de WordPress, compartiendo núcleo, plugins y usuarios, pero con contenido y, normalmente, tablas de base de datos independientes para cada sitio. Se activa definiendo una constante en wp-config.php y es una decisión que conviene tomar desde el principio del proyecto, porque migrar a Multisite después es más laborioso.
WordPress no depende de un cron del sistema operativo para tareas programadas (como publicar una entrada programada o revisar actualizaciones): usa wp-cron, que se dispara con cada visita a la web, comprobando si hay tareas pendientes. Es cómodo porque no requiere configuración en el servidor, pero en sitios con poco tráfico las tareas pueden retrasarse; para mayor precisión se puede desactivar y sustituir por un cron real del servidor que llame a wp-cron.php a intervalos fijos.
Es el orden en el que WordPress busca qué archivo de plantilla usar para mostrar cada tipo de contenido. Para una entrada individual, por ejemplo, prueba primero single-{post-type}-{slug}.php, luego single-{post-type}.php, luego single.php, y así hasta llegar a index.php como último recurso. Entender esta jerarquía es clave para saber qué archivo tocar en un child theme.
Son pares clave-valor adicionales que se pueden asociar a una entrada, página o cualquier post type, más allá de los campos habituales como título o contenido. Se guardan en la tabla wp_postmeta y se leen con get_post_meta($post_id, "clave", true). Son la base sobre la que funcionan plugins de campos avanzados, aunque también se pueden usar directamente sin ningún plugin extra.
Es una acción que se ejecuta al principio de cada carga de una pantalla del panel de administración, antes de que se imprima cualquier HTML. Por eso es el lugar correcto para procesar formularios y hacer redirecciones con wp_safe_redirect(): si ese código se ejecuta más tarde, cuando ya se ha enviado la cabecera del admin, la redirección puede fallar en silencio, como me pasó al principio con el plugin de FAQs.
"mu" es de "must-use": son plugins colocados en la carpeta wp-content/mu-plugins que WordPress activa automáticamente en cada carga, sin que aparezcan en el listado de plugins ni se puedan desactivar desde el panel. Se usan para funcionalidad que debe estar siempre activa pase lo que pase, como la que uso para el aviso de última actualización en Indaga.net.
wp_options guarda la configuración general del sitio: desde el título hasta los ajustes de cada plugin, como pares clave-valor. Las opciones marcadas como autoload ("yes") se cargan de golpe en cada petición, aunque no se vayan a usar en esa página, así que guardar datos grandes o poco usados como autoload puede ralentizar todo el sitio; conviene marcarlos como autoload => "no" si no hace falta tenerlos siempre en memoria.
Es el archivo de configuración principal de WordPress: contiene los datos de conexión a la base de datos (nombre, usuario, contraseña, host), el prefijo de tablas, las claves de seguridad y constantes como WP_DEBUG. Se genera durante la instalación y conviene protegerlo bien, porque contiene credenciales sensibles.
Son cadenas aleatorias (AUTH_KEY, SECURE_AUTH_KEY, LOGGED_IN_KEY...) que WordPress usa para cifrar las cookies de sesión de los usuarios que han iniciado sesión. Cuanto más largas y aleatorias, más difícil resulta falsificar una cookie de sesión válida; WordPress ofrece un generador oficial para crearlas al configurar un sitio nuevo.
Es la función que declara, desde el tema, una zona donde se puede asignar un menú de navegación personalizado, por ejemplo register_nav_menu('top', 'Menú superior'). A partir de ahí, en Apariencia > Menús el usuario puede crear un menú y asignarlo a esa ubicación, que luego se imprime en la plantilla con wp_nav_menu().
Carga un fragmento de plantilla reutilizable dentro de otra plantilla, por ejemplo get_template_part('template-parts/footer', 'widgets') incluye el archivo template-parts/footer-widgets.php. Ayuda a dividir plantillas grandes en piezas más pequeñas y reutilizables, en lugar de repetir el mismo HTML en varios archivos.
Es el editor de contenido por defecto de WordPress desde la versión 5.0, que sustituye al editor clásico de un único campo de texto por bloques independientes (párrafo, imagen, lista, columnas...) que se pueden mover, configurar y combinar. Cada bloque guarda su contenido como HTML con comentarios especiales que WordPress usa para reconocerlo al volver a editar.
Es una acción que se dispara cada vez que se guarda una entrada, página o cualquier post type, ya sea al publicarla, actualizarla o guardarla como borrador. Se usa mucho para validar o procesar datos adicionales al guardar, aunque hay que tener cuidado con las llamadas duplicadas que WordPress hace en ciertos casos, comprobando por ejemplo que no se trate de un autoguardado con wp_is_post_autosave().
Es un sistema de caché en memoria (con wp_cache_get() y wp_cache_set()) pensado para guardar resultados de consultas u operaciones costosas durante una misma petición, o de forma persistente si el hosting tiene instalado algo como Redis o Memcached. Sin un backend persistente configurado, WordPress usa una caché de objetos solo válida durante esa carga de página, que igualmente evita repetir cálculos dentro de la misma petición.
Permite crear endpoints propios dentro de la REST API de WordPress, más allá de los que trae por defecto (entradas, usuarios...). Por ejemplo, se podría registrar una ruta /wp-json/indaga/v1/faqs que devuelva las preguntas frecuentes en JSON, útil si en algún momento se quisiera consumir esos datos desde una aplicación externa o desde JavaScript sin recargar la página.
Es donde WordPress guarda todos los archivos subidos a la biblioteca de medios: imágenes, PDFs, documentos... Se organiza automáticamente en subcarpetas por año y mes (uploads/2026/09/), lo cual evita tener miles de archivos sueltos en una sola carpeta y facilita localizar cuándo se subió algo.
Es una constante que se activa en wp-config.php (define('WP_DEBUG', true);) para que WordPress muestre avisos y errores de PHP que normalmente quedan ocultos, muy útil al desarrollar o depurar un plugin propio. En un sitio en producción conviene tenerlo desactivado, o combinado con WP_DEBUG_LOG para que los errores se guarden en un archivo en vez de mostrarse a los visitantes.
