Convivencia con tu CSS
Casi ninguna página que adopta un framework está vacía. Hay un tema, una hoja heredada, un plugin que da estilo a button y una fecha de entrega. Esta página va de esa situación: qué hace iVOLT para no estorbar, qué no hace, y las reglas exactas que deciden quién gana cuando dos hojas de estilo discrepan.
La versión corta: .iv-root limita dónde se aplican los estilos de elemento, los prefijos evitan colisiones de nombres y las capas de cascada ordenan las reglas propias de iVOLT. Las capas no aíslan el framework de tu CSS, y conviene entender por qué antes de depurar una sorpresa.
Ámbito y prefijos
Todos los selectores de elemento de base.css empiezan por .iv-root. Fuera de esa clase, iVOLT no da estilo a ningún h1, ni a, ni input. El ámbito lo eliges tú:
<!-- página entera -->
<html lang="es" class="iv-root" data-iv-theme="system">
<!-- una zona de un sitio existente -->
<div class="iv-root" data-iv-theme="light">
<!-- marcado de iVOLT aquí; la página que lo rodea queda intacta -->
</div>Todo lo demás va con prefijo y una prueba de contrato lo hace cumplir: las clases empiezan por iv- (iv-button, iv-card), las utilidades por iv-u-, las propiedades personalizadas por --iv-, los atributos de datos por data-iv-, y la compilación IIFE expone exactamente un global, IVOLT. Una colisión con tu propio .button o --color-primary es, por construcción, imposible.
Dos advertencias. .iv-root también fija box-sizing: border-box en sí misma y en todos sus descendientes, así que los widgets de terceros que estén dentro del ámbito lo heredan; y el ámbito de tema vuelve a aplicar color y background-color en cualquier elemento que lleve data-iv-theme, que es lo que permite que una isla oscura viva dentro de una página clara.
Las reglas de las capas de cascada, exactamente
La entrada en capas declara su orden por adelantado y asigna la capa en el momento de importar:
@layer iv.reset, iv.tokens, iv.base, iv.layout, iv.components, iv.utilities, iv.overrides;
@import url("./tokens.css") layer(iv.tokens);
@import url("./base.css") layer(iv.base);
/* … */Los módulos fuente no declaran capa propia, y eso es lo que permite que el mismo archivo sirva para la compilación en capas, la plana y un import por componente. De ahí se derivan tres reglas, y la tercera es la que muerde:
- Dentro de las capas, gana la última.
iv.utilitiesgana aiv.componentsincluso con menos especificidad. Por esoiv-u-gap-4sobrescribe de forma fiable a un modificador de componente. - Cualquier declaración normal (sin
!important) que esté fuera de las capas gana a todas las declaraciones normales que estén en capas, sea cual sea su especificidad. Un solobutton { background: red }en un tema heredado sobrescribe.iv-buttonenivolt.css. Esto no es un error de iVOLT ni de tu navegador; es así como la cascada ordena las reglas sin capa: como si estuvieran en una capa final implícita. - Entre declaraciones
!importantel orden se invierte por completo. Ganan las capas iniciales y las declaraciones sin capa pasan a ser las más débiles. Consecuencia:.iv-u-sr-only, la única regla del paquete que usa!important, no se puede sobrescribir desde tu CSS sin capa enivolt.css, mientras que enivolt.flat.csssí se puede.
Así que las capas ordenan iVOLT por dentro y te facilitan sobrescribirlo; no lo defienden. Dilo en voz alta una vez y la depuración se acorta mucho.
Elegir una estrategia
| Tu situación | Usa | Por qué |
|---|---|---|
| Página nueva, controlas el CSS | css/ivolt.css | Orden por capas, y es fácil sobrescribir desde tus propias reglas sin capa |
| Hoja heredada que no puedes editar | css/ivolt.flat.css | Sin @layer: compite por especificidad y orden de origen como cualquier otra hoja |
| Hoja heredada que sí puedes importar | ivolt.css + envolver la hoja heredada en una capa | Pone el CSS antiguo por debajo de iVOLT sin reescribirlo |
El truco de envolverla, con el orden de capas declarado antes que las de iVOLT para que las reglas heredadas queden por debajo:
@layer legacy, iv.reset, iv.tokens, iv.base, iv.layout, iv.components, iv.utilities, iv.overrides;
@import url("legacy-theme.css") layer(legacy);
@import url("ivolt/css/ivolt.css");En cuanto la hoja heredada está en una capa, pierde su privilegio de estar fuera de las capas y los componentes de iVOLT ganan. Tus reglas nuevas, escritas sin capa, siguen ganando a ambas, que suele ser justo lo que quieres.
Para sobrescribir a propósito dentro de la compilación en capas, la capa iv.overrides existe exactamente para eso y se declara la última:
@layer iv.overrides {
.iv-button { border-radius: 0; }
}Ninguna regla del paquete usa !important salvo .iv-u-sr-only, y los selectores de componente se quedan en (0,2,0) de especificidad o por debajo — las dos cosas las hacen cumplir pruebas de contrato —, así que con una clase propia y corriente basta para ganar cuando las capas lo permiten.
El reset opcional
css/reset.css se distribuye sin capa a propósito. Enlazado tal cual quedaría fuera de todas las capas y, por la regla 2 de arriba, ganaría a los componentes bajo los que se supone que debe estar. Usa una de las dos formas admitidas:
@import url("ivolt/css/reset.css") layer(iv.reset); /* le asignas tú la capa */
<link rel="stylesheet" href="ivolt/css/reset.layer.css"> <!-- ya viene envuelto -->En un sitio que ya existe, lo normal es que no quieras ningún reset: el sitio ya tiene uno, y una segunda normalización es el origen habitual de los cambios de espaciado inexplicables. Los estilos base no dependen de él: .iv-root fija su propio box-sizing, así que la garantía se mantiene en cualquier caso.
WordPress y otros sitios en PHP
No hay ningún paso de compilación que añadir. Copia packages/ivolt/dist dentro del tema (por ejemplo en assets/ivolt/) y encola los archivos.
function theme_ivolt_assets() {
$base = get_template_directory_uri() . '/assets/ivolt';
wp_enqueue_style('ivolt', $base . '/css/ivolt.flat.css', array(), '0.5.0-beta');
wp_enqueue_script('ivolt', $base . '/js/auto.js', array(), '0.5.0-beta', true);
}
add_action('wp_enqueue_scripts', 'theme_ivolt_assets');Notas propias de este entorno:
- Usa mejor la compilación plana. Los temas y los plugins casi nunca usan capas, así que con
ivolt.csssus reglas de elemento sin capa sobrescribirían a iVOLT en todas partes. - Pon
iv-rooten un contenedor, no en<html>, salvo que estés convirtiendo el tema entero. Un contenedor alrededor del área de contenido deja que la barra de administración, los widgets de los plugins y las plantillas antiguas conserven sus estilos. - Registra el script como módulo si usas
auto.js(WordPress necesitatype="module", que las versiones recientes admiten mediante las estrategias de script o un filtroscript_loader_tag). Si eso te resulta incómodo, encolajs/ivolt.iife.min.jsy llama tú aIVOLT.init(): no inicializa nada al cargar. - No hay escáner de clases ni paso de purga. La matriz de utilidades es finita y está generada por completo, así que las clases escritas dentro de plantillas PHP, shortcodes, el editor de bloques o un campo de la base de datos funcionan todas. Nada tiene que ver tu marcado en tiempo de compilación.
- Contenido del editor. Si el contenido de las entradas se renderiza dentro de
.iv-root, los títulos, listas, tablas y código de ese contenido reciben los estilos base. Eso suele ser lo deseable; si no lo es, poniv-rooten la estructura de la página y no en el cuerpo de la entrada.
Astro, Vite y otros empaquetadores
Instala el tarball que produce npm pack dentro de packages/ivolt e importa a través de los exports del paquete. Las rutas profundas dentro de dist/ no forman parte de la API y pueden moverse.
import "@intervolutions/ivolt/css/ivolt.css";
import { init } from "@intervolutions/ivolt";
if (typeof document !== "undefined") init(document);Lo que importa cuando un framework renderiza en el servidor:
- Ningún módulo toca
document,window,navigatornimatchMediaen el momento de importarlo, así que importar el paquete durante el renderizado en servidor no lanza. Una prueba unitaria lo verifica en un entorno de Node. - Importar
@intervolutions/ivoltno tiene efectos secundarios. Solo@intervolutions/ivolt/autoinicializa al cargar, y antes comprueba que hay un documento. Prefiere elinit()explícito en un script de cliente cuando sea el framework quien controle la hidratación. init()es idempotente y solo promociona los elementos marcados condata-iv-component, así que volver a llamarlo tras una navegación en el cliente o un renderizado parcial es seguro. Cuando desmontes una zona, llama adestroy()para que se restauren los atributos.- El paquete está anotado respecto a efectos secundarios, así que un empaquetador elimina por tree-shaking los componentes que no importas; la prueba de humo del empaquetado lo verifica.
- Islas. El marcado renderizado por un componente que nunca se hidrata sigue funcionando para las familias de solo CSS, y conserva las alternativas con
<details>y:targetpara las interactivas.
Content-Security-Policy
El paquete funciona bajo script-src 'self'. No contiene eval, ni new Function, ni innerHTML con datos externos, ni atributos on*, y no hace peticiones de red, no carga fuentes remotas y no envía telemetría.
Hay una excepción que conviene prever. Si usas el script de lectura previa del tema — ese script en línea diminuto que lee la preferencia guardada antes del primer pintado, para que la página no parpadee con el tema equivocado —, es en línea por necesidad, y una política estricta necesita su hash o un nonce:
<script>try{document.documentElement.classList.add("docs-js");var t=localStorage.getItem("iv-theme");if(t==="light"||t==="dark"||t==="system")document.documentElement.setAttribute("data-iv-theme",t)}catch(e){}</script>Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-jUAUQ8+jWVPo7EbSE9FVWWmn/FVSalWrOarVKCFs7BU='; style-src 'self'El hash de arriba se calcula al compilar a partir del fragmento exacto que inyecta este sitio (sha256-jUAUQ8+jWVPo7EbSE9FVWWmn/FVSalWrOarVKCFs7BU=), así que los dos no pueden divergir.
Esta documentación publica el hash del fragmento que distribuye. Si modificas el fragmento aunque sea en un carácter, el hash cambia: vuelve a generarlo o sirve el script con un nonce. Saltarse el fragmento por completo también es una opción válida: el tema se aplica entonces después de que se hayan ejecutado la hoja de estilo y el script.
El único estilo dinámico que aplica el paquete es fijar propiedades personalizadas mediante style.setProperty, que cuenta como un atributo de estilo en línea. Si tu style-src los prohíbe, eso es lo que hay que probar.