Interfaz de usuario elástica (Stretchy morph UI)
Este skill produce interfaces donde cada cambio de estado se transforma: un indicador de pestaña se estira como un gusano en camino a la siguiente pestaña, un botón se comprime en un círculo de carga y se expande de nuevo, una imagen de producto se levanta de su tarjeta, se encoge hasta convertirse en un chip y cae en el carrito. Se destiló a partir de la construcción de una muestra de 24 componentes (en assets/showcase.html) y los errores y correcciones de diseño encontrados en el camino.
El aspecto proviene de una idea: el estiramiento tiene una causa. Un elemento se deforma porque sus partes no están de acuerdo (un borde llega antes que el otro), porque se mueve rápido (se alarga a lo largo de su trayectoria), porque está siendo comprimido (se abulta en la otra dirección para mantener su volumen), o porque acaba de ser golpeado (oscila). Un bamboleo aleatorio en bucle se ve barato; un estiramiento con causa parece material.
Paso 0, cada vez: audita el código del usuario y confirma
Antes de construir o editar cualquier cosa, descubre qué partes de la interfaz del usuario pueden volverse elásticas y permite que el usuario elija. Los detalles, criterios de juicio y plantillas de mensajes están en references/audit.md; la versión resumida:
- Encuentra el código: código pegado, archivos adjuntos (en claude.ai, lista
/mnt/user-data/uploads), el directorio de trabajo o repositorio, un enlace de artefacto (léelo con la herramienta de artefactos), o una fuente que el usuario indique. Si no hay ninguno, di lo que buscaste y ofrece dos caminos: compartir el código, o construir algo nuevo desde cero. - Escanea:
python scripts/audit_ui.py <paths>informa sobre el stack, el movimiento existente, la gestión faltante de movimiento reducido, y candidatos por categoría de UI conarchivo:línea, cada uno mapeado a un componente de muestra y técnica. - Lee cada acierto en contexto y conserva solo los candidatos reales. Para cada uno, anota dónde está, qué ocurre ahora, la frase de transformación que sería, el esfuerzo (pequeño, mediano, grande) y cualquier riesgo (marcado de biblioteca de componentes, una biblioteca de animación existente, un control nativo cuyo selector móvil se perdería, listas muy largas).
- Prioriza por visibilidad × frecuencia × ajuste, agrupa duplicados (todos los botones que usan un
Buttoncompartido son un solo elemento), limita la lista a unos diez, y enumera cualquier cosa que aconsejes en contra con el motivo. - Pide confirmación en el idioma del usuario: las propuestas numeradas, luego una pregunta que invite a elegir números, tomar todas las recomendadas, o escribir partes adicionales que quieran elásticas que no estén en la lista. Usa una herramienta de opciones seleccionables si el entorno la tiene, si no, texto plano. Termina el turno aquí; no hagas ediciones antes de la respuesta.
- Construye lo que eligieron más cualquier cosa que añadieron, luego sigue el Flujo de trabajo a continuación e informa por cada elemento.
Si el usuario ya nombró las partes que quiere, aún así audita: pon sus partes primero, ofrece el resto como opcional, y mantén la pregunta breve.
Archivos
Todas las rutas son relativas a la carpeta de este skill (la carpeta que contiene este SKILL.md), no al proyecto del usuario. Resuélvelas contra la carpeta del skill cuando las leas o ejecutes.
| Ruta | Qué es | Cuándo abrirlo |
|---|---|---|
assets/stretchy-core.js | El motor de resortes más ayudantes (pares de bordes, indicador segmentado, escala de volumen, bordes SVG abultados, reordenamiento FLIP, objetos lanzados) | Pega en cada página que construyas. Léelo una vez para conocer los ayudantes. |
assets/starter.html | Esqueleto de página: tokens claro/oscuro, fuente variable con capacidad tailandesa, gestión de movimiento reducido, el núcleo incrustado, un componente de ejemplo | Comienza una página nueva desde aquí |
assets/showcase.html | Los 24 componentes funcionales de la construcción original. Cada sección JS comienza con un banner como /* ---------- pestañas ---------- */ | Cuando una solicitud coincide con un componente del catálogo, usa grep -n "/\\* ---------- " assets/showcase.html y lee solo esa sección más su CSS |
references/audit.md | Cómo encontrar, escanear, juzgar y priorizar el código del usuario, y las plantillas de mensajes de confirmación (tailandés e inglés) | En el Paso 0, cada vez |
references/techniques.md | Las 11 técnicas de estiramiento en profundidad, con código y números de ajuste | Antes de construir algo que no hayas construido antes |
references/components.md | Catálogo: receta de cada componente, valores de resortes y errores comunes | Cuando la solicitud nombre o se parezca a un componente del catálogo |
references/new-components.md | Cómo diseñar una versión elástica de cualquier componente, más 14 diseños desarrollados que no están en la muestra | Siempre que la solicitud esté fuera del catálogo |
scripts/audit_ui.py | Escáner para el código del usuario: stack, movimiento existente y candidatos elásticos con archivo:línea | En el Paso 0, cada vez |
scripts/check_page.py | Verificación de navegador sin cabeza: errores JS, desbordamiento lateral en móvil y escritorio, elementos invisibles que roban clics, y hojas de contacto de fotogramas de animación | Después de cada construcción, antes de entregar cualquier cosa |
Si hay disponible un skill de diseño visual (por ejemplo frontend-design o ui-ux-pro-max), léelo también. Ese skill decide cómo se ve la página (paleta, tipografía, diseño); este decide cómo se mueve. El pulido del movimiento no puede rescatar una página de aspecto genérico.
Flujo de trabajo (después de la confirmación del usuario)
- Escribe la frase de transformación para cada componente. "Cuando disparador, forma A se convierte en forma B, y se estira porque causa." Ejemplo: "Cuando se hace clic en una pestaña, el indicador se mueve hacia ella, y se estira porque su borde delantero es más rígido que su borde trasero." Si no puedes completar la causa, elige una de la tabla a continuación. Esta frase es el diseño; todo lo demás es implementación.
- Elige la(s) técnica(s) de la tabla. La mayoría de los componentes usan un estiramiento principal más un pequeño bamboleo de retroalimentación.
- Define los estados de reposo como números: anchos, altos, radios, posiciones, progreso de 0 a 1. Los resortes animan números y los escribes en
styleenonUpdate. Las transiciones CSS manejan solo color y opacidad (y fundidos cruzados de contenido corto). - Planifica el contenido. El texto y los iconos no deben refluir ni aplastarse mientras el contenedor se transforma. Dale a cada estado de contenido su propia capa en su tamaño final, anclada (generalmente centrada), y haz un fundido cruzado: salida rápida (≈150 ms), entrada después de un retraso de ≈100–150 ms con una ligera escala o desenfoque. El
overflow:hiddendel contenedor revela el contenido a medida que crece. - Construye desde
assets/starter.htmlcon el núcleo incrustado. Escribe los estilos directamente en las devoluciones de llamada del resorte; nunca desencadenes un re-renderizado del framework por fotograma. - Pase de accesibilidad e interrupción (lista de verificación a continuación).
- Prueba y observa. Ejecuta
scripts/check_page.py, luego captura hojas de contacto de fotogramas de las interacciones importantes y míralas realmente. La animación de agregar al carrito en la construcción original "funcionaba" pero se veía barata hasta que se inspeccionaron los fotogramas; las correcciones están en "Cómo hacer que se vea bien".
De dónde viene el estiramiento
| Situación | Técnica (detalles en references/techniques.md) | Visto en |
|---|---|---|
| Algo se mueve entre ranuras | Par de bordes: dos resortes para bordes delantero y trasero, el delantero rígido, intercambiados por dirección | Pestañas, puntos de carrusel, conmutadores, chips de elección, punto de radio, stepper, resaltado de desplegable, cápsula de calificación, navegación de barra lateral |
| Un contenedor cambia de tamaño o forma | Ejes divididos: resortes separados para ancho, alto, radio con diferente rigidez, para que un eje lidere | Isla dinámica, FAB a formulario, modal desde botón, desplegable, botón de agregar al carrito |
| Algo se vuelve más estrecho o ancho rápidamente | Preservación de volumen: escala el otro eje por velocidad, scaleY = clamp(1 − v/k) | Botón de enviar, campo de búsqueda, ancho de desplegable |
| Algo viaja rápido | Estiramiento por velocidad: alargar en la dirección del movimiento, adelgazar a través de él | Pulgar de deslizador, elemento de carrito lanzado, tarjetas de carrusel (sesgado), tooltip |
| Una superficie grande se mueve | Borde abultado: dibujar la superficie como una ruta SVG cuyo punto de control de borde está desplazado por −velocidad | Barra lateral, hoja inferior |
| Una parte cuelga de una parte en movimiento | Seguidor rezagado: un resorte más suave persigue al principal; usa el espacio para inclinación o desplazamiento | Burbuja de valor de deslizador, texto interno de acordeón |
| Retroalimentación para un toque o llegada | Patada: añadir velocidad a un resorte en reposo en 0, oscila y se estabiliza | Casilla de verificación, chips, carrito, estrellas, insignia, sacudida de límite de cantidad |
| Las burbujas se unen o dividen | Filtro Goo: desenfoque más umbral alfa, fuente nítida compuesta encima | Menú Goo |
| Un elemento se convierte en un elemento diferente | Transformación de contenedor: comenzar en el rectángulo del disparador, saltar al rectángulo objetivo, fundido cruzado de contenido | Modal, FAB, lanzamiento al carrito |
| El cambio necesita una pausa en medio | Transformación secuenciada a través de una forma intermedia que tenga significado (spinner, check) | Botón de enviar, botón de agregar al carrito |
| Arrastrar más allá de un límite, o una lista se reordena | Banda elástica (dividir el desbordamiento por ≈3) y FLIP con resortes que mantienen la velocidad | Carrusel, hoja, lista de productos ordenada |
El motor en un párrafo
Spring es un oscilador armónico amortiguado integrado con Euler semi-implícito en 8 sub-pasos por fotograma (delta de fotograma limitado a 1/30 s para que una pestaña en segundo plano no pueda explotarlo). to(target) redestina sin reiniciar la velocidad, que es lo que hace que los clics rápidos se sientan continuos en lugar de reiniciarse. set(x) salta sin animación, drag(x, v) sigue un puntero mientras registra la velocidad, kick(v) inyecta un impulso. El amortiguamiento se multiplica por un factor global bounce en cada tic, de modo que un control a nivel de página (suave 1.8, saltarín 1, elástico 0.5) reajusta cada componente en vivo. Bajo prefers-reduced-motion: reduce, to() salta al objetivo y dispara onRest inmediatamente, para que la lógica que espera una animación aún se ejecute.
Relación de amortiguamiento ζ = c / (2√k). Los cambios de estado de UI se sienten correctos alrededor de ζ 0.6–0.8; retroalimentación juguetona en 0.3–0.5; por encima de 1 nada sobrepasa. Los preajustes utilizados en la muestra:
| Rol | k | c | Notas |
|---|---|---|---|
| Borde delantero de un par de bordes | 420–520 | 30–34 | Llega primero |
| Borde trasero | 140–170 | 17–19 | Se retrasa, lo que crea el estiramiento |
| Ancho del contenedor (transformación) | 240–320 | 18–24 | |
| Alto del contenedor (transformación) | 150–170 | 15–17 | Más lento que el ancho, por lo que la forma se estira lateralmente primero |
| Seguidor de puntero (deslizador) | 650 | 42 | Ajustado, pero aún tiene velocidad para leer |
| Seguidor rezagado (burbuja) | 150 | 11 | Suelto a propósito |
| Retroalimentación de patada | 380–420 | 12–16 | Patada con 6–10 |
| Alto con sobrepaso visible (acordeón) | 210 | 14 |
Reglas prácticas
- Limita cada deformación. Controles: preservación de volumen dentro de ±7–15 %. Cosas pequeñas y rápidas (un chip lanzado, un pulgar de deslizador): hasta 35–70 % de estiramiento. Siempre limita los tamaños a un mínimo positivo, ya que un resorte subamortiguado puede subestimar por debajo de cero.
- Intercambia la rigidez por dirección. En un par de bordes, el borde que mira hacia la dirección del viaje recibe el resorte rígido. Sin el intercambio, el indicador se estira correctamente en una dirección y se comprime en la otra.
- Tiempo opuesto en el camino de regreso. Si el ancho lidera la altura al abrir, deja que la altura lidere el ancho al cerrar. La forma entonces retrocede por un camino diferente, lo que se lee como elástico en lugar de un rebobinado.
- Resortes para cambios de estado, curvas basadas en tiempo para caminos cuya forma importa. Un objeto lanzado necesita un arco predecible (Bézier cuadrático, progreso ease-in-out, alrededor de 800 ms). Los resortes x/y independientes dan caminos errantes. Deriva el estiramiento de la velocidad fotograma a fotograma de cualquier manera.
- Mantén el contenido erguido. Para estirar algo a lo largo de una dirección arbitraria sin rotar su imagen o texto, usa
rotate(a) scale(1+s, 1−s·0.45) rotate(−a). - Un movimiento audaz por componente. El resto del componente permanece tranquilo: un cambio de color, un fundido corto.
- Calma cuando está inactivo. Nada oscila por sí solo. Como máximo un momento orquestado al cargar la página (la muestra abre la isla una vez después de 900 ms).
Cómo hacer que se vea bien (lecciones de la construcción)
La primera versión de agregar al carrito volaba una bola de color plano a lo largo de dos resortes independientes, encogía el botón en un óvalo violeta vacío y aplastaba el carrito. El usuario dijo que no era bonito. Lo que lo arregló se generaliza:
- Mueve lo real. Clona la imagen del producto, levántala ligeramente con una sombra, luego encógela en un chip redondo que aún muestre la imagen mientras vuela. Los puntos abstractos se leen como salida de depuración.
- Las formas intermedias deben tener significado. Un botón encogido es una burbuja vacía a menos que muestre algo: un spinner, una marca que se dibuja sola, un contador. El botón de agregar ahora se convierte en un círculo verde con una marca, luego crece hasta convertirse en el stepper − n +.
- Volumen sutil en los controles. Un abultamiento vertical de ±18 % en un botón parecía un huevo en forma de píldora. ±7 % se lee como material suave.
- La llegada necesita un receptor. El icono del carrito se inclina y se inclina, un anillo pulsa hacia afuera, la insignia aparece. Algo debe atrapar visiblemente lo que fue lanzado.
- No recortes las marcas de estado. Una insignia dentro de una píldora con
overflow:hiddenque se transforma se corta; colócala en un envoltorio fuera del clip. - No repitas información. La píldora del carrito mostraba "3 ชิ้น 195 บาท" junto a una insignia que decía 3. Ahora muestra solo el total.
- Dibuja las ilustraciones con una relación de aspecto fija. Las formas CSS posicionadas en porcentaje dentro de un mosaico se estiran cuando el mosaico se ensancha. Usa un SVG en línea con un viewBox, centrado en el mosaico.
Lista de verificación de accesibilidad y robustez
-
Respeta el movimiento reducido (el núcleo lo hace; asegúrate de que las keyframes y transiciones CSS también se corten en una consulta de medios
prefers-reduced-motion). -
Usa
button,input,role="switch",role="tab",role="slider"reales, etc., con el estadoaria-*correcto, actualizado en la misma llamada que inicia la animación. -
El contenido oculto por una transformación recibe
inert(y no debe interceptar clics: da a las capas invisiblespointer-events:none, luego actívalo de nuevo con el estado visible). -
Mueve el foco deliberadamente cuando el control enfocado desaparece en una transformación: al control de reemplazo, de regreso al disparador después de que se cierra un diálogo, y solo después de que el elemento sea visible nuevamente (un elemento con
visibility:hiddenno puede recibir foco).## Notas de implementación -
El archivo
assets/stretchy-core.jsexportaSpringy ayudantes como funciones globales. Inclúyelo en el<head>de tu página. -
Usa
requestAnimationFramepara el bucle de animación; el motor lo maneja internamente si está presente. -
Para componentes con múltiples estados, usa un único
Springpor propiedad animada y actualiza el estado de la UI enonUpdate. -
Prueba en navegadores reales; el verificador de navegador (
scripts/check_page.py) detecta errores comunes.
Ejemplo de flujo de trabajo
- El usuario dice: "Haz que mis pestañas sean elásticas".
- Ejecutas
python scripts/audit_ui.py .para escanear el código. - Encuentras pestañas en
index.html:45y propones: "1. Pestañas: el indicador se estira con par de bordes". - El usuario confirma.
- Abres
assets/starter.html, copias el esqueleto, incrustasassets/stretchy-core.js. - Lees la sección de pestañas en
assets/showcase.htmlpara la técnica. - Implementas: un
Springpara el borde izquierdo, otro para el borde derecho, con rigidez intercambiada por dirección. - Actualizas los roles ARIA y añades
prefers-reduced-motion. - Ejecutas
scripts/check_page.pyy corriges cualquier error. - Entregas el resultado.
Este skill está diseñado para ser utilizado con plataformas de agentes de IA como Claude Code, Cursor o Codex. Sigue el flujo de trabajo descrito para obtener los mejores resultados.