Nocturna Eléctrica
Sistema de diseño dark-first, enérgico y de alto contraste. El fondo casi negro es el escenario; el celeste eléctrico es el reflector.
Principios
- Contraste con propósito — el acento solo aparece donde hay acción o dato clave.
- Profundidad por luz — la elevación aclara superficies, no oscurece bordes.
- Energía contenida — paleta vibrante, espaciado disciplinado: nunca ruidosa.
- Datos protagonistas — cifras y gráficas brillan; el chrome se mantiene discreto.
- Movimiento con propósito — la animación explica cambios de estado, nunca decora sola.
Voz y contenido
Español, técnico pero cercano. Tuteo implícito. Frases cortas y declarativas, con energía contenida.
Sentence case en headings y botones: «Crear proyecto», no «Crear Proyecto». Overlines en uppercase con tracking +0.08em.
Verbo + objeto, corto: «Guardar cambios», «Ver informe».
Concretos y accionables: dicen qué pasó y qué hacer.
Siempre en JetBrains Mono o tabular-nums; grandes en dashboards, con count-up de 350ms al montar.
No se usan. El estado se comunica con color + icono + texto.
Colores
Haz clic en cualquier swatch para copiar su variable CSS.
Acento — Celeste Eléctrico
Secundario — Índigo Profundo
Neutrales — Escala Nocturna
Semánticos
Regla 60-30-10
60% fondos nocturnos · 30% texto y estructura (índigo como fondo de marca) · 10% acento eléctrico y semánticos. El celeste es el único color que grita: un CTA primario por pantalla. Texto masivo en night-100, nunca blanco puro.
Tipografía
Escala (ratio 1.25, base 16px)
Espaciado
Escala base 4px. Aliases de uso: --card-padding: 24px · --section-gap: 64px · --form-gap: 20px · --touch-target: 44px.
Bordes, radios y focus
Borde por defecto 1px night-500; énfasis night-600. Focus ring global de teclado: 2px accent-400 con offset 2px — pruébalo navegando con Tab.
Elevación
Sobre negro las sombras solas no bastan: la elevación combina superficie más clara + sombra + borde sutil.
night-950
night-900 + shadow-1
night-800 + shadow-2
night-800 + borde night-600 + shadow-3
Glow de acento
máximo uno por pantalla
Iconografía
Set sustituto: Lucide — trazo 2px, esquinas redondeadas, fill: none, tamaño default 20px, color currentColor. Tamaños: 20px en botones · 18px en inputs · 16px en chevrons. Sin emoji, sin unicode-como-icono.
Marca / wordmark
No hay logo dibujado: el wordmark es tipográfico — Space Grotesk 700, tracking -0.02em, con el punto separador en accent-400.
Componentes
Los 35 componentes del sistema, renderizados en vivo con las clases del framework nocturna-electrica.css. Cada bloque incluye su código de uso.
Botones
Alturas 36 / 44 / 52px. El primario lleva texto oscuro (contraste 9.50:1) — nunca blanco. Un solo primario por pantalla.
Ver código
<button class="btn btn--primary">Guardar cambios</button>
<button class="btn btn--secondary">Ver informe</button>
<button class="btn btn--outline">Exportar</button>
<button class="btn btn--ghost">Cancelar</button>
<button class="btn btn--danger">Eliminar cuenta</button>
<!-- Tamaños: .btn--sm (36px) · default (44px) · .btn--lg (52px) -->
<!-- Loading: fijar min-width y sustituir el texto por .spinner -->Spinner
Trazo 2px, rotación 0.8s. Si la carga dura menos de 300ms, no mostrar spinner.
Ver código
<span class="spinner"></span>
<!-- Hereda currentColor; default accent-400 -->Input
Fondo --surface-2, label siempre visible (nunca placeholder-como-label).
Ver código
<div class="field">
<label class="field__label" for="email">Correo</label>
<input class="input" id="email" type="email" placeholder="tu@correo.com">
<span class="field__help">Texto de ayuda.</span>
</div>
<!-- Obligatorio: .field--required en el .field pinta el asterisco (decorativo); -->
<!-- el control lleva required + aria-required="true" -->
<div class="field field--required">
<label class="field__label" for="name">Nombre</label>
<input class="input" id="name" type="text" required aria-required="true">
</div>
<!-- Estados: .input--error (+ .field__error) · .input--success · disabled -->
<!-- Icono leading: envolver en .input-wrap con un svg antes del input -->Textarea
Fondo --surface-2, label siempre visible (nunca placeholder-como-label).
Ver código
<div class="field">
<label class="field__label" for="desc">Descripción</label>
<textarea class="textarea" id="desc" placeholder="Describe el proyecto…"></textarea>
<span class="field__count">0 / 280</span>
</div>Select
Ver código
<div class="select-wrap">
<select class="select"><option>Opción</option></select>
</div>SegmentedControl
2–4 opciones mutuamente excluyentes. Contenedor --surface-2, opción activa en --surface-3 con texto --accent-300 y peso 600. Para más de 4 opciones, usar Select.
Ver código
<div class="segmented" role="radiogroup" aria-label="Vista">
<input type="radio" name="vista" id="lista" checked><label for="lista">Lista</label>
<input type="radio" name="vista" id="tablero"><label for="tablero">Tablero</label>
</div>
<!-- Compacto: añadir segmented--sm -->Slider
Track 6 px, porción rellena en --accent-400 sobre --surface-3; thumb --night-50 con borde de acento que escala 1.15 al presionar.
Ver código
<div class="slider-row">
<span class="slider-label">Límite de gasto</span>
<span class="slider-value">$ 350</span>
</div>
<input class="slider" type="range" min="0" max="1000" value="350">
<!-- JS: --slider-fill = ((v-min)/(max-min))*100 + '%' -->Checkbox y Radio
Ver código
<label class="check-row"><input type="checkbox" class="checkbox"> Etiqueta</label>
<label class="check-row"><input type="radio" name="g" class="radio"> Opción</label>
<!-- Indeterminate = tercer estado (guion horizontal). Solo por JS: -->
<!-- document.querySelector('.checkbox').indeterminate = true; -->Switch
Ver código
<label class="check-row"><input type="checkbox" class="switch"> Activar</label>Badge y Tag
Ver código
<span class="badge badge--success">Activo</span>
<span class="tag">diseño <button class="tag__x" aria-label="Quitar">×</button></span>Avatar
Iniciales en Space Grotesk sobre índigo; punto de estado opcional.
Ver código
<span class="avatar">HS<span class="avatar__dot avatar__dot--online"></span></span>
<!-- Tamaños: --xs 24 · --sm 32 · default 40 · --lg 48 · --xl 64 -->
<!-- Con imagen: <span class="avatar"><img src="…" alt="Nombre"></span> -->AvatarGroup
Avatares solapados 8px con borde 2px del color del fondo; el chip .avatar-group__more resume los ocultos.
Ver código
<div class="avatar-group" role="group" aria-label="Equipo del proyecto">
<span class="avatar">HS</span>
<span class="avatar">ML</span>
<span class="avatar">AR</span>
<span class="avatar avatar-group__more">+3</span>
</div>
<!-- Solapamiento -8px + borde del color del fondo (--surface-1 / --bg). -->Divider
Regla hairline de 1px sobre --border; variante con label centrado y variante vertical para separar acciones en línea.
Ver código
<hr class="divider">
<div class="divider--label" role="separator">o continúa con</div>
<span class="divider--vertical" role="separator"></span>Accordion
Ver código
<div class="accordion">
<div class="accordion__item">
<button class="accordion__header" aria-expanded="false">
Pregunta <svg>chevron</svg>
</button>
<div class="accordion__panel"><div class="accordion__inner">
<div class="accordion__content">Respuesta…</div>
</div></div>
</div>
</div>
<!-- JS: alternar aria-expanded en el header -->Tooltip
Delay de 400ms; máximo una línea. Pasa el cursor:
Ver código
<span class="has-tooltip">
<button class="btn btn--outline">Pasa el cursor</button>
<span class="tooltip" role="tooltip">Exportar como CSV</span>
</span>Alert
Banner inline y persistente (a diferencia del Toast, que es transitorio). Icono + texto + color — el estado nunca depende solo del color.
Nueva versión disponible
La versión 2.4 incluye exportación a PDF. Actualiza cuando quieras.
Pago confirmado
El recibo se envió a tu correo.
Tu plan expira en 5 días
Renueva antes del 12 de julio para no perder acceso.
No se pudo guardar el formulario
Revisa los campos marcados e inténtalo de nuevo.
Ver código
<div class="alert alert--warning" role="status">
<svg>…icono…</svg>
<div>
<p class="alert__title">Tu plan expira en 5 días</p>
<p class="alert__body">Renueva antes del 12 de julio.</p>
</div>
<button class="alert__close" aria-label="Cerrar aviso">…x…</button>
</div>
<!-- Variantes: --info · --success · --warning · --danger -->Toast
Top-right, auto-dismiss a los 5s (pausable con hover), máximo 3 apilados.
Ver código
<div class="toast-stack" role="status" aria-live="polite">
<div class="toast toast--success">
<svg>…</svg>
<div>
<div class="toast__title">Cambios guardados</div>
<div class="toast__body">El proyecto se actualizó correctamente.</div>
</div>
</div>
</div>Progress y ProgressRing
Barra para procesos lineales (determinada/indeterminada); anillo circular para progreso compacto en cards y dashboards. Comparten --accent-400 y --data-color.
Ver código
<div class="progress__head">
<span class="progress__label">Subiendo…</span>
<span class="progress__value">64 %</span>
</div>
<div class="progress" role="progressbar" aria-valuenow="64" aria-valuemin="0" aria-valuemax="100">
<div class="progress__bar" style="width:64%"></div>
</div>
<!-- Variantes: .progress--indeterminate · .progress--no-glow -->
<div class="progress-ring" role="progressbar" aria-valuenow="64" aria-valuemin="0" aria-valuemax="100">
<svg width="56" height="56" viewBox="0 0 56 56">
<circle class="progress-ring__track" cx="28" cy="28" r="24" stroke-width="5"/>
<circle class="progress-ring__bar" cx="28" cy="28" r="24" stroke-width="5"/>
</svg>
<span class="progress-ring__value">64%</span>
</div>
<!-- JS: stroke-dasharray = 2πr; stroke-dashoffset = c * (1 - value/100) -->Modal
Overlay con blur 4px; se cierra con Esc o clic fuera (excepto flujos destructivos con data-modal-persistent). Al abrir, el foco entra al panel y queda atrapado (Tab/Shift+Tab ciclan dentro); al cerrar, el foco vuelve al botón que lo abrió.
Ver código
<div class="modal-overlay">
<!-- aria-modal, aria-labelledby (título) y aria-describedby (descripción, si la hay);
tabindex="-1" permite mover el foco al panel al abrir -->
<div class="modal" role="dialog" aria-modal="true"
aria-labelledby="mTitle" aria-describedby="mDesc" tabindex="-1">
<div class="modal__header">
<h4 class="modal__title" id="mTitle">Título</h4>
<button class="icon-btn icon-btn--ghost" data-close-modal aria-label="Cerrar">×</button>
</div>
<p id="mDesc">Contenido…</p>
<div class="modal__footer">
<button class="btn btn--outline" data-close-modal>Cancelar</button>
<button class="btn btn--primary" data-close-modal>Confirmar</button>
</div>
</div>
</div>
<!-- JS: al abrir guarda document.activeElement y enfoca el panel; Tab cicla dentro
(focus-trap); Esc / clic en overlay / data-close-modal cierran y devuelven el foco.
Destructivo: añade data-modal-persistent al .modal-overlay. -->Tabla
Zebra (.table--zebra) + encabezado fijo (.table--sticky, haz scroll dentro del recuadro) + columna ordenable (clic en «Proyecto» o «Ingresos») + columna de selección. La fila marcada usa .row--selected; el checkbox maestro está en estado indeterminate.
| Estado | Cambio | |||
|---|---|---|---|---|
| API de pagos | Beta | $31 990 | +2,4 % | |
| App móvil | Pendiente | $12 040 | −1,1 % | |
| Panel analítico | Activo | $48 210 | +8,2 % | |
| Portal de socios | Activo | $27 500 | +5,0 % | |
| Reportes | Beta | $9 800 | −0,4 % |
Ver código
<table class="table table--zebra table--sticky">
<thead><tr>
<th scope="col" class="col-select">
<input type="checkbox" class="checkbox" aria-label="Seleccionar todas"></th>
<th scope="col" aria-sort="ascending">
<button class="th-sort" type="button">Proyecto
<svg class="sort-icon" …><path d="m18 15-6-6-6 6"/></svg>
</button></th>
<th scope="col" class="num">Ingresos</th>
</tr></thead>
<tbody>
<tr><td class="col-select"><input type="checkbox" class="checkbox" aria-label="Seleccionar API de pagos"></td>…</tr>
<tr class="row--selected">…</tr>
</tbody>
</table>
<!-- El icono se atenúa si aria-sort no está fijado; JS reordena y alterna aria-sort. -->Stat
Cifra de dashboard con count-up de 350ms al entrar en pantalla (respeta prefers-reduced-motion).
Ver código
<div class="stat">
<span class="stat__label">Ingresos del mes</span>
<span class="stat__value">$128 450</span>
<span class="stat__delta stat__delta--up">▲ +12,4 %</span>
</div>Pagination
1-indexado, bajo tablas y listas largas · trunca con elipsis a partir de 7 páginas · página activa con fondo --accent-400 + peso 700 (no depende solo del color) y aria-current="page".
Ver código
<ul class="pagination" aria-label="Paginación">
<li><button class="pagination__btn" data-page="prev" aria-label="Página anterior">‹</button></li>
<li><button class="pagination__btn" data-page="1">1</button></li>
<li><span class="pagination__ellipsis">…</span></li>
<li><button class="pagination__btn is-active" data-page="5" aria-current="page">5</button></li>
<li><button class="pagination__btn" data-page="6">6</button></li>
<li><button class="pagination__btn" data-page="next" aria-label="Página siguiente">›</button></li>
</ul>Skeleton y EmptyState
Ver código
<span class="skeleton" style="height:12px;width:70%"></span>
<span class="skeleton skeleton--circle" style="width:40px;height:40px"></span>
<div class="empty-state">
<svg>…</svg>
<span class="empty-state__title">Aún no hay proyectos</span>
<span class="empty-state__desc">Crea tu primer proyecto…</span>
<button class="btn btn--primary btn--sm">Crear proyecto</button>
</div>Card, Link y Code
Card
Tarjeta en reposo
Superficie 1, borde sutil, sombra nivel 1.
Tarjeta interactiva
Al pasar el cursor sube de nivel: −2px + superficie más clara.
Ver código
<div class="card">…</div>
<div class="card card--interactive">…</div>
<!-- .card--glow: firma de marca — MÁXIMO UNO POR PANTALLA -->Link y Code
Ver código
<a class="link" href="…">Link con subrayado animado</a>
<span class="code">var(--accent-400)</span>Combobox y Search
Combobox con autocompletado: filtra en vivo y se navega con ↑/↓, Enter selecciona, Esc cierra. El listado vive sobre --surface-2. Al lado, un campo de búsqueda con botón para limpiar.
- Angular
- Astro
- React
- Solid
- Svelte
- Vue
Ver código
<div class="combobox">
<input class="input combobox__input" role="combobox" aria-expanded="false"
aria-controls="lista" aria-autocomplete="list" aria-activedescendant="">
<ul class="combobox__list" id="lista" role="listbox" aria-label="Frameworks" hidden>
<li class="combobox__option" role="option" id="op-0" aria-selected="false">Angular</li>
<!-- .is-active = resaltado por teclado · aria-selected="true" = elegida -->
</ul>
</div>
<div class="search">
<svg class="search__icon" aria-hidden="true">…lupa…</svg>
<input class="input search__input" type="search" aria-label="Buscar">
<button class="search__clear" aria-label="Limpiar" hidden>×</button>
</div>
<!-- JS: filtra en vivo, ↑/↓/Enter/Esc; el clear vacía y devuelve el foco. -->Drawer
Panel que entra deslizándose desde un borde (derecha por defecto; --left / --bottom). Igual que el modal: role="dialog" + aria-modal, foco atrapado, retorno de foco, y cierre con Esc o clic en el overlay. El panel usa overscroll-behavior: contain.
Ver código
<div class="drawer drawer--right" hidden>
<div class="drawer__overlay" data-close-drawer></div>
<aside class="drawer__panel" role="dialog" aria-modal="true"
aria-labelledby="dTitle" tabindex="-1">
<div class="drawer__header">
<h3 class="drawer__title" id="dTitle">Filtros</h3>
<button class="icon-btn icon-btn--ghost" data-close-drawer aria-label="Cerrar">×</button>
</div>
<div class="drawer__body">…</div>
<div class="drawer__footer">…</div>
</aside>
</div>
<!-- Variantes: --right (default) · --left · --bottom (sheet).
JS: .is-open dispara el slide; Esc / overlay cierran y devuelven el foco. -->Popover
Se abre por clic —eso lo distingue del Tooltip, que es hover— y crece desde el transform-origin del disparador. role="dialog", el foco entra al panel; cierra con Esc o clic fuera. Panel sobre --surface-2.
Compartir proyecto
Cualquiera con el enlace podrá ver este proyecto en modo lectura.
Ver código
<div class="popover">
<button class="btn btn--outline popover__trigger" type="button"
aria-haspopup="dialog" aria-expanded="false" aria-controls="panel">Compartir</button>
<div class="popover__panel" role="dialog" aria-label="Compartir proyecto" id="panel">
<p>Contenido de varias líneas…</p>
<button class="btn btn--primary btn--sm">Copiar</button>
<a class="link" href="…">Ver permisos avanzados</a>
</div>
</div>
<!-- Abre por CLICK (no hover); el foco entra al panel; Esc / clic fuera cierran. -->DatePicker
Campo de fecha con calendario en popover: el input abre el calendario al hacer clic, eliges un día con el ratón o las flechas y el valor vuelve al campo. El panel reutiliza el look de .popover__panel sobre --surface-2. El .calendar__grid es role="grid" y cada día un <button role="gridcell"> con la fecha completa en aria-label.
Ver código
<div class="datepicker">
<div class="input-wrap">
<svg aria-hidden="true">…calendario…</svg>
<input class="input datepicker__input" readonly aria-haspopup="dialog"
aria-expanded="false" aria-controls="cal" value="15 de julio de 2026">
</div>
<div class="datepicker__calendar" id="cal" role="dialog" aria-label="Elegir fecha" hidden>
<div class="calendar">
<div class="calendar__header">
<button class="icon-btn icon-btn--ghost icon-btn--sm" aria-label="Mes anterior">‹</button>
<span class="calendar__title" aria-live="polite">julio 2026</span>
<button class="icon-btn icon-btn--ghost icon-btn--sm" aria-label="Mes siguiente">›</button>
</div>
<div class="calendar__weekdays" role="row">
<span role="columnheader" aria-label="lunes">L</span> …
</div>
<div class="calendar__grid" role="grid">
<div class="calendar__week" role="row">
<button class="calendar__day" role="gridcell"
aria-label="15 de julio de 2026" aria-selected="true">15</button> …
</div>
</div>
</div>
</div>
</div>
<!-- JS: clic abre; flechas mueven el día (±1, ±7) cruzando meses; Enter elige; Esc cierra. -->FileUpload
Zona de arrastre con lista de archivos. El input file real va oculto accesiblemente (.visually-hidden, sigue tabulable) y el ring de foco se pinta sobre el label. Al arrastrar un archivo encima, .is-dragover tiñe el borde de acento. Un ítem subiendo incrusta el .progress; el error se marca con icono + texto, no solo por color.
-
informe-trimestral-q3.pdf 2.4 MB · subiendo…
-
mockup-portada.png 840 KB · completo
-
video-demo.mov Error: supera los 10 MB
Ver código
<div class="file-upload">
<input class="file-upload__input visually-hidden" type="file" id="f" multiple
aria-describedby="hint">
<label class="file-upload__label" for="f">
<svg aria-hidden="true">…subir…</svg>
<span>Arrastra archivos aquí o <span class="file-upload__button">explora</span></span>
<span class="file-upload__hint" id="hint">PNG, JPG o PDF · máximo 10 MB</span>
</label>
<ul class="file-upload__list">
<li class="file-upload__item">
<svg aria-hidden="true">…archivo…</svg>
<div>
<span class="file-upload__name">informe.pdf</span>
<span class="file-upload__size">2.4 MB · subiendo…</span>
<div class="progress" role="progressbar" aria-valuenow="60" …>
<div class="progress__bar" style="width:60%"></div></div>
</div>
<button class="file-upload__remove" aria-label="Cancelar informe.pdf">×</button>
</li>
<li class="file-upload__item file-upload__item--error">…icono + texto de error…</li>
</ul>
</div>
<!-- JS: dragover añade .is-dragover; el botón × quita el ítem. -->Stepper / Wizard
Indicador de progreso por pasos. Es un <ol>, así que el orden y el conteo se anuncian solos; el paso en curso lleva aria-current="step" y los pasos completos muestran un check (estado no solo por color). El conector se tiñe de acento en el tramo ya recorrido. Variante .stepper--vertical para apilarlos.
- Completado: Cuenta
- Completado: Perfil
- Paso actual: 3 Pago
- Pendiente: 4 Confirmación
- Completado: Compilar el proyecto
- Paso actual: 2 Ejecutar pruebas
- Pendiente: 3 Publicar en producción
Ver código
<ol class="stepper" aria-label="Progreso de la configuración">
<li class="step step--complete">
<span class="step__marker"><svg aria-hidden="true">…check…</svg></span>
<span class="step__label">Cuenta</span>
<span class="step__connector" aria-hidden="true"></span>
</li>
<li class="step step--current" aria-current="step">
<span class="step__marker">3</span>
<span class="step__label">Pago</span>
<span class="step__connector" aria-hidden="true"></span>
</li>
<li class="step step--upcoming">
<span class="step__marker">4</span><span class="step__label">Confirmación</span>
</li>
</ol>
<!-- El último paso no lleva conector. Variante: .stepper--vertical. -->Kbd
Tecla o atajo de teclado sobre <kbd>: JetBrains Mono, borde --border-strong y sombra sutil, alto y ancho mínimo 20px. Para combos se encadenan varias con un "+" literal. Es informativa, nunca un botón.
Ver código
Pulsa <kbd class="kbd">Esc</kbd> para cerrar.
Abre el buscador con <kbd class="kbd">⌘</kbd> + <kbd class="kbd">K</kbd>.
Guarda con <kbd class="kbd">Ctrl</kbd> + <kbd class="kbd">S</kbd>.Motion
Solo transform y opacity. Salida ~20% más corta que la entrada. Distancias de 4–8px. Stagger 30ms/ítem (máximo 6).
| Token | Valor | Uso |
|---|---|---|
| --duration-instant | 100ms | press, toggle |
| --duration-fast | 150ms | hover, focus, color |
| --duration-base | 220ms | dropdowns, tooltips, switches |
| --duration-slow | 250ms | modales, toasts |
| --duration-page | 350ms | transiciones de página |
| --ease-out | cubic-bezier(0.2, 0, 0, 1) | entradas (default) |
| --ease-in | cubic-bezier(0.4, 0, 1, 1) | salidas |
| --ease-in-out | cubic-bezier(0.65, 0, 0.35, 1) | moverse de A a B (indicador de tabs) |
| --ease-spring | cubic-bezier(0.34, 1.4, 0.64, 1) | toggles, checks |
Firma de marca: los elementos con acento entran con fade + glow — solo en el elemento hero/CTA. prefers-reduced-motion colapsa todo a 0.01ms.
Accesibilidad
| Combinación | Contraste | Veredicto |
|---|---|---|
| night-100 sobre night-950 | 17.25:1 | AAA |
| accent-400 sobre night-950 | 9.50:1 | AA — sirve como texto/link |
| night-950 sobre accent-400 (CTA) | 9.50:1 | AA — texto del CTA oscuro |
| indigo-500 sobre night-950 | 2.09:1 | Solo fondo, nunca texto |
| night-50 sobre indigo-500 | 9.29:1 | AAA |
| accent-600 sobre blanco (modo claro) | 4.01:1 | Solo UI — nunca texto |
| accent-700 sobre blanco (texto de acción en claro) | 5.95:1 | AA |
| accent-800 sobre blanco (hover en claro) | 8.74:1 | AAA |
| info sobre info-bg (badge en claro) | 5.20:1 | AA |
- Mínimos WCAG AA: 4.5:1 texto normal, 3:1 texto grande y componentes UI.
- Ratios calculados con la fórmula WCAG 2.x, no estimados: mcp/test/contraste-tokens.test.ts los recalcula desde tokens.css en cada corrida.
- En modo claro, accent-600 (#1f87bb) vale como UI (focus, bordes, iconos) pero no como texto: da 4.01:1. El texto de acción usa accent-700.
- Focus visible siempre (:focus-visible global); estado nunca solo con color — siempre color + icono + texto.
- Touch targets de 44×44px. IconButton siempre con aria-label.
- role="progressbar" en Progress, aria-expanded en Accordion, aria-current en navegación.
Tokens y descarga
El sistema se distribuye en dist/nocturna-electrica/: nocturna-electrica.css (framework completo), tokens.css (solo variables), tailwind.css (bloque @theme para Tailwind v4) y tokens.json (formato estándar W3C DTCG 2025.10, interoperable con Figma / Tokens Studio / Style Dictionary).
Variables CSS (núcleo)
tokens :root (aliases semánticos)
:root {
/* Superficies */
--bg: #010203;
--surface-1: #0b0d12; --surface-2: #14171f; --surface-3: #1e222d;
--border: #3d4354; --border-strong: #2a2f3d;
/* Texto */
--text-primary: #e8eaef; --text-secondary: #c5c9d4;
--text-muted: #9aa0b0; --text-disabled: #6b7183;
--text-heading: #f6f7f9; --text-on-accent: #010203;
/* Acento e índigo */
--accent-400: #52baed; --accent-500: #2fa3dd; --accent-600: #1f87bb;
--indigo-500: #353d7c; --indigo-700: #22284f; --indigo-800: #1a1e3b;
--accent-ghost-hover: rgba(82,186,237,.08);
--accent-selected: rgba(82,186,237,.12);
/* Semánticos */
--success: #4ade80; --warning: #fbbf24; --danger: #f87171; --info: #52baed;
/* Tipografía */
--font-display: "Space Grotesk", system-ui, sans-serif;
--font-body: "Manrope", system-ui, sans-serif;
--font-mono: "JetBrains Mono", monospace;
/* Radios y elevación */
--radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px; --radius-full: 9999px;
--shadow-1: 0 1px 2px rgba(0,0,0,.4);
--shadow-2: 0 4px 12px rgba(0,0,0,.5);
--shadow-3: 0 12px 32px rgba(0,0,0,.6);
--glow-accent: 0 0 0 1px rgba(82,186,237,.35), 0 4px 20px rgba(82,186,237,.18);
--ring: 0 0 0 3px rgba(82,186,237,.2);
/* Motion */
--duration-fast: 150ms; --duration-base: 220ms; --duration-slow: 250ms;
--ease-out: cubic-bezier(0.2, 0, 0, 1);
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
--ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
}Tailwind CSS v4 — @theme
tailwind.css
@import "tailwindcss";
@theme {
--color-night-950: #010203; --color-night-900: #0b0d12;
--color-night-800: #14171f; --color-night-700: #1e222d;
--color-night-600: #2a2f3d; --color-night-500: #3d4354;
--color-night-400: #6b7183; --color-night-300: #9aa0b0;
--color-night-200: #c5c9d4; --color-night-100: #e8eaef;
--color-night-50: #f6f7f9;
--color-accent-100: #d9f0fb; --color-accent-200: #a8ddf6;
--color-accent-300: #7dccf1; --color-accent-400: #52baed;
--color-accent-500: #2fa3dd; --color-accent-600: #1f87bb;
--color-accent-700: #186a94; --color-accent-800: #12506f;
--color-indigo-300: #7a83c4; --color-indigo-400: #565fa3;
--color-indigo-500: #353d7c; --color-indigo-600: #2b3264;
--color-indigo-700: #22284f; --color-indigo-800: #1a1e3b;
--color-success: #4ade80; --color-warning: #fbbf24;
--color-danger: #f87171; --color-info: #52baed;
--font-display: "Space Grotesk", system-ui, sans-serif;
--font-body: "Manrope", system-ui, sans-serif;
--font-mono: "JetBrains Mono", monospace;
--radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px; --radius-xl: 24px;
--shadow-1: 0 1px 2px rgba(0,0,0,.4);
--shadow-2: 0 4px 12px rgba(0,0,0,.5);
--shadow-3: 0 12px 32px rgba(0,0,0,.6);
--shadow-glow: 0 0 0 1px rgba(82,186,237,.35), 0 4px 20px rgba(82,186,237,.18);
--ease-out: cubic-bezier(0.2, 0, 0, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
--ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
}Instalación
El sistema se distribuye como paquete npm (@higinio/design-css) y también como archivos sueltos en dist/nocturna-electrica/. Instálalo o cópialos, y elige qué importar según tu stack (A/B/C/D). En todas: dark por defecto; <html data-theme="light"> activa el modo claro, y los componentes consumen solo aliases semánticos, nunca hex.
Instalar desde npm (recomendado)
Un solo paquete con los dos sistemas en subrutas /celeste/ y /nocturna/. Instálalo y usa las vías de abajo apuntando a la subruta, en vez de a un archivo copiado.
terminal + CSS de entrada
npm i @higinio/design-css
/* framework completo (vía A) */
@import "@higinio/design-css/nocturna/framework.css";
/* o solo tokens (vía B) */
@import "@higinio/design-css/nocturna/tokens.css";
/* o Tailwind v4 (vía C) */
@import "@higinio/design-css/nocturna/tailwind.css";A · CSS drop-in (framework completo)
Para cualquier proyecto — HTML plano o cualquier framework. Trae variables + clases de componentes listas (tipo Bootstrap) y ya importa las fuentes.
index.html
<link rel="stylesheet" href="/estilos/nocturna-electrica.css">
<!-- luego usas las clases directo: -->
<button class="btn btn--primary">Guardar</button>
<div class="card">…</div>B · Solo tokens
Si diseñas tus propios componentes: solo las variables, sin clases.
styles.css
@import "./tokens.css";
.mi-panel {
background: var(--surface-1);
color: var(--text-primary);
border: 1px solid var(--border);
}C · Tailwind v4
Tailwind v4 se configura por CSS. El tailwind.css ya incluye @import "tailwindcss", los tokens y el bloque @theme — úsalo como tu hoja de entrada (mantén tokens.css a su lado, que es lo que importa).
terminal + CSS de entrada
# 1) instala Tailwind v4
npm i tailwindcss @tailwindcss/postcss
# 2) copia tailwind.css + tokens.css a tu carpeta de CSS
# y usa tailwind.css como entrada. Ya tienes utilidades:
<div class="bg-night-900 text-accent-400">…</div>D · Pipelines de tokens
Para Figma, Tokens Studio o Style Dictionary: importa tokens.json (formato estándar W3C DTCG 2025.10).
MCP
El paquete @higinio/design-mcp expone ambos sistemas de diseño — Celeste Clásica y Nocturna Eléctrica — a cualquier IDE o cliente compatible con Model Context Protocol: tokens, componentes, reglas duras y validación de código. Complementa a las skills de Claude, no las reemplaza: misma fuente de verdad, accesible también fuera del entorno de Claude.
Las 8 tools
| Tool | Qué hace |
|---|---|
| list_design_systems | Lista los sistemas de diseño disponibles. |
| get_token | Devuelve el valor resuelto de un token o alias de un sistema. |
| search_tokens | Busca tokens por nombre, categoría o valor. |
| list_components | Lista los componentes documentados de un sistema. |
| get_component_spec | Devuelve la especificación completa de un componente: estados, medidas, variantes. |
| get_rules | Devuelve las reglas duras (no negociables) del sistema. |
| validate_code | Audita CSS/HTML contra las reglas del sistema — hex directos, contraste, aliases. |
| generate_snippet | Genera un snippet de componente listo para usar, con los aliases correctos. |
Conectar (remoto)
Añade el servidor a tu .mcp.json:
.mcp.json
{
"mcpServers": {
"design": {
"type": "http",
"url": "https://design-mcp.higiniolab.tech/mcp",
"headers": { "Authorization": "Bearer <tu-token>" }
}
}
}Ejemplo
Consultar un token de este sistema:
get_token
get_token(system="nocturna", name="--accent")
→ "#52baed"Auditar código antes de integrarlo:
validate_code
validate_code(system="nocturna", code="…")
→ { ok: false, issues: ["hex directo #52baed en línea 12 — usa var(--accent-400)"] }