Starlight 0.42: menos JavaScript, menú móvil roto en silencio
El viernes actualicé la documentación de un proyecto interno a Starlight 0.42. npx @astrojs/upgrade, build en verde, Lighthouse un punto mejor. Cerré el portátil.
El lunes un compañero me pasa una captura desde el móvil. El menú abría perfecto. Pero el botón se quedaba gris. Nuestro color de marca al abrir el menú había desaparecido.
Nada había fallado. Ese es el problema. El CSS no lanza errores: cuando un selector deja de coincidir con algo, se calla y sigue. El build no te avisa, los tests no lo ven y tú te enteras cuando alguien abre el sitio en un teléfono.
El anuncio oficial de la release vende una paradoja simpática —menos JavaScript y más JavaScript a la vez— y para los detalles te remite al CHANGELOG. Este post es el CHANGELOG explicado, con el CSS concreto que tienes que cambiar.
El titular es "menos JavaScript". La letra pequeña es que si tocaste el menú móvil con CSS o JS propio, la 0.42 te lo rompe sin decir nada.
La paradoja de Starlight 0.42: "un 100 % más de JavaScript"
El chiste es del propio anuncio y tiene truco. Hay dos JavaScript distintos aquí y el post oficial los mezcla a propósito.
Uno es el JavaScript que va dentro del paquete de npm. Ese sube: Starlight ha dejado de publicar TypeScript y ahora distribuye JavaScript compilado.
El otro es el JavaScript que llega al navegador de quien lee tu documentación. Ese baja: el menú móvil ya no necesita JS para funcionar.
Build-time contra runtime. Son cosas distintas y confundirlas es el error clásico al leer notas de release. Con la 0.42 en la mano, un sitio Starlight medio sigue enviando entre 3 y 13 veces menos JavaScript al usuario final que herramientas comparables: MkDocs, Sphinx, VitePress, Nextra, GitBook o Docusaurus.
Es la misma lógica que ya movía a Astro con las server islands: decidir con precisión qué se ejecuta en el servidor y qué viaja al cliente, en vez de mandarlo todo y confiar. Astro lleva varias releases insistiendo en lo mismo, desde que server islands y actions se estabilizaron en la 6.2.
Por qué Starlight publicaba TypeScript y por qué ha parado
Hay una rareza de los proyectos Astro que mucha gente no conoce: puedes publicar archivos .ts directamente en npm y funcionan cuando alguien los instala. Sin build step. Genial para prototipar rápido, y así se venía publicando Starlight desde el principio.
El coste aparecía en tu proyecto, no en el suyo.
Si haces typecheck con tsc, TypeScript revisa cualquier .ts que encuentre importado, incluso dentro de node_modules. Así que acababas comprobando el código fuente de Starlight. Y no con la configuración del paquete, sino con la tuya. En algunos casos el rendimiento de tipos en el editor también se resentía. En una base de código del tamaño de Starlight, esos problemas se acumulan.
La 0.42 compila sus fuentes y distribuye JavaScript más archivos de declaración (.d.ts). El resultado esperable es un typecheck más rápido en tu máquina — y más aún cuando aterrice el compilador de TypeScript escrito en Go.
Detalle que conviene tener claro: un .d.ts describe tipos en tiempo de compilación y desaparece en tiempo de ejecución. No valida nada.
La validación de verdad ya la estás usando aunque no te hayas fijado: el frontmatter de tus páginas lo comprueba Starlight con esquemas de Zod durante el build. Y si tu sitio además tiene formularios o endpoints, ahí hace falta el mismo tipo de esquema pero ejecutándose en runtime. Tipos y validación resuelven problemas distintos, aunque la gente los meta en el mismo saco.
La Popover API: por qué el menú móvil deja de necesitar tu JavaScript
Aquí está el cambio de fondo.
Antes, el menú móvil era una máquina de estados escrita a mano: un custom element <starlight-menu-button> que escuchaba clics, cambiaba aria-expanded en el botón y ponía un atributo en el <body>. Si el JavaScript no llegaba a ejecutarse, no había menú.
Y hay más razones de las que la gente cree para que el JavaScript no se ejecute. La red se cae a medias. Otro script peta antes y se lleva por delante el resto del bundle. Una extensión del navegador se mete por medio. Alguien lo tiene desactivado. En un sitio de documentación —que muchas veces es la primera vez que alguien te ve— eso es una puerta cerrada.
La 0.42 delega ese estado en la Popover API del navegador. El menú abre aunque tu JavaScript nunca llegue.
Menos código propio, más primitiva nativa. Es la dirección correcta.
Pero ojo con la consecuencia: si el estado ya no vive en un atributo del botón, tu CSS no tiene a qué agarrarse.
Los 3 breaking changes de Starlight 0.42, en una tabla
Starlight 0.42 rompe tres cosas y solo tres. Esta es la traducción directa de código viejo a código nuevo:
| Qué desaparece | Reemplazo en 0.42 | A quién afecta |
|---|---|---|
<starlight-menu-button> y aria-expanded |
.sl-menu-button y .sl-menu-button:has(~ :popover-open) |
Estilos, temas o component overrides que apuntaban al botón del menú móvil |
body[data-mobile-menu-expanded] |
body:has(sl-sidebar-pane:popover-open) |
CSS o JS propio que reaccionaba a la apertura del menú |
Opción tagline en astro.config |
Sin reemplazo: se borra | Configs que la declararon (nunca hizo nada) |
Si ninguna de las tres filas aparece en tu código, la actualización es transparente. Si aparece alguna, las tres secciones siguientes son tuyas.
Breaking change 1: el botón del menú móvil
El botón ya no va envuelto en el custom element <starlight-menu-button> y ya no usa aria-expanded. Ahora apuntas al botón con la clase .sl-menu-button y lees el estado abierto con la pseudo-clase :popover-open.
Así estaba tu CSS antes:
/* Antes: Starlight 0.41 y anteriores */
starlight-menu-button button {
border-radius: 999px;
background: var(--sl-color-gray-6);
}
starlight-menu-button[aria-expanded="true"] button {
background: var(--dc-brand);
color: var(--sl-color-white);
}
Y así queda en la 0.42:
/* Después: Starlight 0.42 */
.sl-menu-button {
border-radius: 999px;
background: var(--sl-color-gray-6);
}
/* El estado abierto lo expone el navegador en el panel, no en el botón.
Seleccionas el botón que tiene un popover abierto como hermano. */
.sl-menu-button:has(~ :popover-open) {
background: var(--dc-brand);
color: var(--sl-color-white);
}
Fíjate en lo que ha cambiado de verdad. No es un nombre de clase: es de quién es el estado.
Antes el estado estaba en el botón, porque lo escribía JavaScript. Ahora está en el panel, porque lo gestiona el navegador. Por eso el selector nuevo tiene que ser relacional: :has() con ~ :popover-open significa "este botón, cuando un hermano posterior suyo está abierto".
Breaking change 2: adiós a data-mobile-menu-expanded
El atributo data-mobile-menu-expanded que Starlight añadía al <body> mientras el menú estaba abierto ya no existe. Si lo usabas para ocultar una barra flotante, bloquear el scroll o apagar una animación, ese bloque de CSS ha dejado de aplicarse.
/* Antes */
body[data-mobile-menu-expanded] .dc-cta-flotante {
display: none;
}
/* Después */
body:has(sl-sidebar-pane:popover-open) .dc-cta-flotante {
display: none;
}
Si además tenías JavaScript propio reaccionando al menú, el cambio te ahorra código. Antes tocaba vigilar un atributo:
// Antes: espiar el atributo que ponía Starlight
const boton = document.querySelector(39;starlight-menu-button button39;);
new MutationObserver(() => {
const abierto = boton.getAttribute(39;aria-expanded39;) === 39;true39;;
document.body.classList.toggle(39;menu-abierto39;, abierto);
}).observe(boton, { attributeFilter: [39;aria-expanded39;] });
Ahora el navegador te lo cuenta él solo con un evento nativo:
// Después: el panel es el popover y emite un evento toggle
const panel = document.querySelector(39;sl-sidebar-pane[popover]39;);
panel?.addEventListener(39;toggle39;, (event) => {
const abierto = event.newState === 39;open39;;
document.body.classList.toggle(39;menu-abierto39;, abierto);
});
Un MutationObserver menos en tu sitio. Esa es la parte buena de apoyarse en primitivas del navegador: el código que borras no puede fallar.
Breaking change 3: la opción tagline ya no existe
Se elimina de la configuración. Nunca se llegó a usar para nada, así que no hay reemplazo: la borras de tu astro.config y listo. Si no la quitas, la validación de config te lo dirá.
Requisitos mínimos y navegadores que se caen
Starlight 0.42 exige Astro v7.2.10 o superior. Si usas @astrojs/markdown-satteri, necesitas 0.4.0 o superior. Si sigues con @astrojs/markdown-remark, 7.3.0 o superior.
Si tu sitio todavía está en Astro v6, el salto grande no es este release, es el anterior: repasa primero las novedades de Astro v7 y hazlo en un PR aparte. Actualizar dos majors en el mismo commit es la forma más rápida de perder la tarde.
Y hay una lista de navegadores que dejan de tener soporte oficial:
| Navegador | Versión mínima soportada |
|---|---|
| Chromium | 116 (agosto de 2023) |
| Safari | 17.0 (septiembre de 2023) |
| Firefox | 125 (abril de 2024) |
No es un capricho, pero tampoco es el mínimo exacto de la API. Firefox 125 y Safari 17.0 son justo las versiones donde aterrizó la Popover API. En Chromium llegó antes, en la 114, así que ahí Starlight se ha guardado dos versiones de margen. Y en iPhone no hay sorpresa: el Safari de iOS la soporta desde la misma 17.0 que el de escritorio.
El precio de apoyarse en el navegador es aceptar su calendario. Mira tus analíticas antes de decidir: en un sitio de documentación técnica, ese tráfico suele redondear a cero.
Lo que mejora sin que hagas nada
Dos cosas llegan gratis con la actualización.
Rendimiento. La última versión de Sätteri —el motor de Markdown y MDX de Astro escrito en Rust, que llegó en Astro 6.4 y es el motor por defecto desde Astro 7— les ha permitido optimizar componentes y plugins de Markdown, y reducir el número de dependencias de Starlight. El procesado de datos del sidebar es ahora hasta 1.400 veces más rápido, y eso se nota de verdad en sitios con sidebars grandes o muy anidados.
Ojo con un detalle que no es opcional. Sätteri no ejecuta plugins de remark ni de rehype: tiene su propio sistema de plugins mdast y hast, y no hay fallback automático.
Y aquí no eliges tú. Como es el motor por defecto de Astro 7, y la 0.42 exige Astro v7.2.10 o superior, la actualización te lo puede cambiar sola. Si tu pipeline depende de remark o rehype, tienes que quedarte explícitamente en @astrojs/markdown-remark 7.3.0 o superior. Compruébalo antes de lanzar el upgrade, no después.
Accesibilidad. El menú móvil atrapa el foco mientras está abierto, para que no puedas tabular hacia la página que queda escondida debajo. Detrás está el criterio de éxito WCAG 2.4.11, «Focus Not Obscured (Minimum)»: el elemento con el foco no puede quedar tapado por lo que hay encima. Se agradece en viewports pequeños o con mucho zoom. Llegó en la 0.41.3, así que si vienes de la última 0.41.x ya lo tienes. Si esto lo habías parcheado tú a mano, bórralo: ahora hay dos implementaciones peleándose por el foco.
Cómo actualizar a Starlight 0.42 paso a paso
-
Comprueba que estás en Astro v7. Starlight 0.42 exige Astro v7.2.10 o superior. Si vienes de Astro v6, haz ese salto antes y en un PR aparte.
-
Busca lo que se va a romper. Cuatro
grepantes de tocar nada:grep -rn "starlight-menu-button" src/ grep -rn "data-mobile-menu-expanded" src/ grep -rn "aria-expanded" src/styles/ grep -rn "tagline" astro.config.*Cada resultado es una línea que hay que migrar. Si no aparece nada, actualiza tranquilo.
-
Actualiza. Un solo comando sube Starlight, Astro y el resto de integraciones a la vez:
npx @astrojs/upgrade -
Migra el CSS y el JS con la tabla de equivalencias de más arriba:
.sl-menu-button,.sl-menu-button:has(~ :popover-open)ybody:has(sl-sidebar-pane:popover-open). -
Abre el sitio en un móvil de verdad y toca el botón del menú. No en el simulador de Chrome: en un teléfono. Es el único sitio donde estos tres breaking changes se manifiestan.
Este tipo de migración quirúrgica —buscar patrones muertos, cambiarlos y verificar— es exactamente lo que un agente hace bien si le das el CHANGELOG y los selectores concretos. Es el flujo que enseño en Construye con IA: contexto preciso primero, ejecución después.
La conclusión que te llevas
Un release que quita JavaScript casi siempre mueve el estado a otro sitio. Y el CSS que apuntaba al estado viejo no se queja: se apaga. Es el mismo patrón que en los breaking changes de pnpm 12: la nota de release vende el titular y el trabajo real está tres párrafos más abajo.
Si mantienes un sitio con Starlight, haz hoy los cuatro grep de arriba antes de actualizar. Tardas menos de lo que has tardado en leer este post, y te ahorras la captura de un compañero el lunes por la mañana.
En Dominicode Labs trabajamos este tipo de migraciones sobre proyectos reales, con el diff delante en vez de con el anuncio de marketing. Y el anuncio original, por si quieres la versión oficial, está en el blog de Astro.
Preguntas frecuentes
¿Cómo actualizo a Starlight 0.42?
Ejecuta npx @astrojs/upgrade, que sube Starlight, Astro y el resto de integraciones a la vez. Antes de lanzarlo, busca en tu proyecto starlight-menu-button, data-mobile-menu-expanded, aria-expanded en tus estilos y tagline en astro.config: cada coincidencia es una línea que hay que migrar. Necesitas Astro v7.2.10 o superior. Después, abre el sitio en un móvil real y comprueba el botón del menú.
¿Starlight 0.42 envía más o menos JavaScript al navegador?
Menos. El "100 % más de JavaScript" del anuncio se refiere al paquete de npm, que ahora se distribuye compilado a JavaScript en lugar de TypeScript. Lo que llega al navegador de quien lee tu documentación baja, porque el menú móvil se apoya en la Popover API nativa en vez de en código propio. Un sitio Starlight medio sigue enviando entre 3 y 13 veces menos JavaScript al usuario final que MkDocs, Sphinx, VitePress, Nextra, GitBook o Docusaurus.
¿Por qué ha dejado de aplicarse mi CSS del menú móvil tras actualizar?
Porque el botón ya no va envuelto en el custom element <starlight-menu-button> y ya no usa el atributo aria-expanded. Cualquier selector construido sobre esos dos elementos deja de coincidir con nada y el navegador lo ignora en silencio. La migración es apuntar al botón con .sl-menu-button y leer el estado abierto con .sl-menu-button:has(~ :popover-open).
¿Por qué el selector nuevo usa :has() en lugar de una clase en el botón?
Porque el estado ha cambiado de dueño. Antes lo escribía JavaScript en el botón; ahora lo gestiona el navegador en el panel del menú, que es el elemento popover. Para estilar el botón según ese estado necesitas un selector relacional: :has(~ :popover-open) significa "este botón, cuando un hermano posterior suyo está abierto". Para el <body>, el equivalente del antiguo data-mobile-menu-expanded es body:has(sl-sidebar-pane:popover-open).
¿Qué versiones mínimas necesito para actualizar a Starlight 0.42?
Astro v7.2.10 o superior. Si usas @astrojs/markdown-satteri, 0.4.0 o superior. Si usas @astrojs/markdown-remark, 7.3.0 o superior. El comando npx @astrojs/upgrade se encarga de subir Starlight, Astro y el resto de integraciones a la vez.
¿Debería preocuparme por los navegadores que pierden soporte?
Depende de tus analíticas, pero casi nunca. Se cae el soporte oficial de Chromium anterior a la 116 (agosto de 2023), Safari anterior a 17.0 (septiembre de 2023) y Firefox anterior a 125. Son los mínimos que exige la Popover API. En documentación técnica ese tráfico suele ser residual; míralo antes de bloquear la actualización por si acaso.
¿Qué hago si tenía JavaScript propio escuchando la apertura del menú?
Bórralo y escucha el evento nativo. El panel es el popover, así que un panel.addEventListener('toggle', ...) con event.newState === 'open' te da lo mismo que antes conseguías con un MutationObserver sobre aria-expanded, con la mitad de código y sin depender de detalles internos de Starlight.
¿Y la opción tagline de la configuración?
Se ha eliminado y no tiene reemplazo, porque nunca se llegó a usar. Bórrala de tu astro.config al actualizar.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
¿Te resultó útil este artículo?
Compártelo con tu comunidad y ayuda a otros desarrolladores.
