Cómo compilar un componente de configuración

Una descripción general básica de cómo crear un componente de configuración de controles deslizantes y casillas de verificación.

En esta publicación, quiero compartir ideas sobre cómo crear un componente de configuración para la Web que sea responsivo, admita varias entradas de dispositivos y funcione en todos los navegadores. Prueba la demostración.

Demostración

Si prefieres un video o quieres una vista previa de la IU/UX de lo que estamos creando, aquí tienes una guía más breve en YouTube:

Descripción general

Desglosé los aspectos de este componente en las siguientes secciones:

  1. Diseños
  2. Color
  3. Entrada de rango personalizado
  4. Entrada de casilla de verificación personalizada
  5. Consideraciones de accesibilidad
  6. JavaScript

Diseños

Esta es la primera demostración de GUI Challenge que se realiza completamente con la cuadrícula de CSS. Aquí se muestra cada cuadrícula destacada con las Herramientas para desarrolladores de Chrome para cuadrículas:

Contornos coloridos y superposiciones de espacios que ayudan a mostrar todos los cuadros que componen el diseño de la configuración

Solo para la brecha

El diseño más común es el siguiente:

foo {
  display: grid;
  gap: var(--something);
}

Llamo a este diseño "solo para espacios" porque solo usa la cuadrícula para agregar espacios entre los bloques.

Cinco diseños usan esta estrategia. Aquí se muestran todos:

Diseños de cuadrícula verticales destacados con contornos y espacios rellenados

El elemento fieldset, que contiene cada grupo de entrada (.fieldset-item), usa gap: 1px para crear los bordes finos entre los elementos. No hay una solución de borde complicada.

Brecha cubierta
.grid {
  display: grid;
  gap: 1px;
  background: var(--bg-surface-1);

  & > .fieldset-item {
    background: var(--bg-surface-2);
  }
}
Truco del borde
.grid {
  display: grid;

  & > .fieldset-item {
    background: var(--bg-surface-2);

    &:not(:last-child) {
      border-bottom: 1px solid var(--bg-surface-1);
    }
  }
}

Ajuste natural de la cuadrícula

El diseño más complejo terminó siendo el diseño macro, el sistema de diseño lógico entre <main> y <form>.

Cómo centrar el contenido de unión

Tanto Flexbox como Grid proporcionan capacidades para align-items o align-content, y, cuando se trata de elementos de ajuste, las alineaciones de diseño de content distribuirán el espacio entre los elementos secundarios como un grupo.

main {
  display: grid;
  gap: var(--space-xl);
  place-content: center;
}

El elemento principal usa la notación abreviada de alineación de place-content: center para que los elementos secundarios se centren vertical y horizontalmente en los diseños de una y dos columnas.

Mira en el video anterior cómo el "contenido" permanece centrado, incluso aunque se haya producido el ajuste.

Repetir el ajuste automático de minmax

La <form> usa un diseño de cuadrícula adaptable para cada sección. Este diseño cambia de una a dos columnas según el espacio disponible.

form {
  display: grid;
  gap: var(--space-xl) var(--space-xxl);
  grid-template-columns: repeat(auto-fit, minmax(min(10ch, 100%), 35ch));
  align-items: flex-start;
  max-width: 89vw;
}

Esta cuadrícula tiene un valor diferente para row-gap (--space-xl) que para column-gap (--space-xxl) para darle ese toque personalizado al diseño responsivo. Cuando las columnas se apilan, queremos un espacio grande, pero no tanto como si estuviéramos en una pantalla ancha.

La propiedad grid-template-columns usa 3 funciones de CSS: repeat(), minmax() y min(). Una Kravets tiene una excelente entrada de blog sobre diseño al respecto, en la que lo llama RAM.

Hay 3 adiciones especiales en nuestro diseño, si lo comparas con el de Una:

  • Pasamos una función min() adicional.
  • Especificamos align-items: flex-start.
  • Hay un estilo max-width: 89vw.

Evan Minto describe bien la función min() adicional en su blog en la publicación Cuadrícula de CSS intrínsecamente responsiva con minmax() y min(). Te recomiendo que la leas. La corrección de alineación flex-start consiste en quitar el efecto de estiramiento predeterminado, de modo que los elementos secundarios de este diseño no necesiten tener alturas iguales, sino que puedan tener alturas naturales e intrínsecas. En el video de YouTube, se incluye un desglose rápido de esta adición de alineación.

max-width: 89vw merece un pequeño desglose en esta publicación. Te mostraré el diseño con y sin el estilo aplicado:

¿Qué sucede? Cuando se especifica max-width, se proporciona contexto, tamaño explícito o tamaño definido para que el algoritmo de diseño de auto-fit sepa cuántas repeticiones puede incluir en el espacio. Si bien parece obvio que el espacio es de "ancho completo", según la especificación de cuadrícula de CSS, se debe proporcionar un tamaño o un tamaño máximo definido. Proporcioné un tamaño máximo.

Entonces, ¿por qué 89vw? Porque "funcionó" para mi diseño. Junto con otras personas del equipo de Chrome, estamos investigando por qué un valor más razonable, como 100vw, no es suficiente y si, de hecho, se trata de un error.

Espaciado

La mayor parte de la armonía de este diseño proviene de una paleta limitada de espaciado, 7 para ser exactos.

:root {
  --space-xxs: .25rem;
  --space-xs:  .5rem;
  --space-sm:  1rem;
  --space-md:  1.5rem;
  --space-lg:  2rem;
  --space-xl:  3rem;
  --space-xxl: 6rem;
}

El uso de estos flujos funciona muy bien con la cuadrícula, CSS @nest y la sintaxis de nivel 5 de @media. Este es un ejemplo, el conjunto de estilos de diseño completamente <main>.

main {
  display: grid;
  gap: var(--space-xl);
  place-content: center;
  padding: var(--space-sm);

  @media (width >= 540px) {
    & {
      padding: var(--space-lg);
    }
  }

  @media (width >= 800px) {
    & {
      padding: var(--space-xl);
    }
  }
}

Una cuadrícula con contenido centrado, con un relleno moderado de forma predeterminada (como en los dispositivos móviles) Sin embargo, a medida que hay más espacio disponible en el viewport, se extiende aumentando el padding. El CSS de 2021 se ve bastante bien.

¿Recuerdas el diseño anterior, "solo para el espacio"? Aquí tienes una versión más completa de cómo se ven en este componente:

header {
  display: grid;
  gap: var(--space-xxs);
}

section {
  display: grid;
  gap: var(--space-md);
}

Color

El uso controlado del color ayudó a que este diseño se destacara como expresivo, pero minimalista. Yo lo hago de la siguiente manera:

:root {
  --surface1: lch(10 0 0);
  --surface2: lch(15 0 0);
  --surface3: lch(20 0 0);
  --surface4: lch(25 0 0);

  --text1: lch(95 0 0);
  --text2: lch(75 0 0);
}

Asigno números a los colores de la superficie y del texto, en lugar de nombres como surface-dark y surface-darker, porque en una consulta de medios los cambiaré, y los nombres claro y oscuro no tendrán sentido.

Los invierto en una consulta de medios de preferencia como esta:

:root {
  ...

  @media (prefers-color-scheme: light) {
    & {
      --surface1: lch(90 0 0);
      --surface2: lch(100 0 0);
      --surface3: lch(98 0 0);
      --surface4: lch(85 0 0);

      --text1: lch(20 0 0);
      --text2: lch(40 0 0);
    }
  }
}

Es importante obtener un panorama rápido de la estrategia general antes de profundizar en los detalles de la sintaxis de color. Pero, como me adelanté un poco, retrocedamos un poco.

¿LCH?

Sin profundizar demasiado en la teoría del color, LCH es una sintaxis orientada a los seres humanos, que se adapta a cómo percibimos el color, no a cómo lo medimos con matemáticas (como 255). Esto le da una ventaja clara, ya que los humanos pueden escribirlo con más facilidad y otros humanos estarán en sintonía con estos ajustes.

Captura de pantalla de la página web pod.link/csspodcast, con el episodio Color 2: Perception abierto
Obtén más información sobre el color perceptual (y mucho más) en el podcast de CSS

Por hoy, en esta demostración, enfoquémonos en la sintaxis y los valores que cambio para crear el modo claro y el oscuro. Veamos 1 color de superficie y 1 color de texto:

:root {
  --surface1: lch(10 0 0);
  --text1:    lch(95 0 0);

  @media (prefers-color-scheme: light) {
    & {
      --surface1: lch(90 0 0);
      --text1:    lch(40 0 0);
    }
  }
}

--surface1: lch(10 0 0) se traduce en una luminosidad de 10%, un cromatismo de 0 y un matiz de 0: un gris incoloro muy oscuro. Luego, en la consulta de medios para el modo claro, la luminosidad se cambia a 90% con --surface1: lch(90 0 0);. Y esa es la esencia de la estrategia. Para comenzar, cambia solo la luminosidad entre los 2 temas, manteniendo las relaciones de contraste que requiere el diseño o lo que puede mantener la accesibilidad.

La ventaja de lch() aquí es que la luminosidad está orientada a los seres humanos, y podemos sentirnos bien con un cambio de %, que será perceptiva y coherentemente así de % diferente. hsl(), por ejemplo, no es tan confiable.

Hay más información sobre los espacios de color y lch() si te interesa. ¡Ya viene!

En este momento, CSS no puede acceder a estos colores en absoluto. Permíteme repetirlo: No tenemos acceso a un tercio de los colores en la mayoría de los monitores modernos. Y no son colores cualquiera, sino los colores más vívidos que puede mostrar la pantalla. Nuestros sitios web se ven descoloridos porque el hardware de los monitores evolucionó más rápido que las especificaciones de CSS y las implementaciones de los navegadores.

Lea Verou

Controles de formulario adaptables con esquema de colores

Muchos navegadores incluyen controles de tema oscuro, actualmente Safari y Chromium, pero debes especificar en CSS o HTML que tu diseño los usa.

En el ejemplo anterior, se muestra el efecto de la propiedad desde el panel Styles de DevTools. En la demostración, se usa la etiqueta HTML, que, en mi opinión, suele ser una mejor ubicación:

<meta name="color-scheme" content="dark light">

Obtén más información en este artículo de color-scheme de Thomas Steiner. Hay mucho más para ganar que las entradas de casillas de verificación oscuras.

CSS accent-color

Hubo actividad reciente en torno a accent-color en los elementos de formulario, que es un solo estilo de CSS que puede cambiar el color de tono que se usa en el elemento de entrada de los navegadores. Obtén más información aquí en GitHub. Lo incluí en mis estilos para este componente. Como los navegadores lo admiten, mis casillas de verificación estarán más en sintonía con el tema con los toques de color rosa y morado.

input[type="checkbox"] {
  accent-color: var(--brand);
}

Una captura de pantalla de Chromium en Linux con casillas de verificación de color rosa

Color pops con gradientes fijos y focus-within

El color se destaca más cuando se usa con moderación, y una de las formas en que me gusta lograrlo es a través de interacciones coloridas de la IU.

En el video anterior, hay muchas capas de comentarios y de interacción de la IU, que ayudan a darle personalidad a la interacción de las siguientes maneras:

  • Destacamos el contexto.
  • Proporcionar comentarios de la IU sobre "qué tan lleno" está el valor en el rango
  • Proporcionar comentarios de la IU de que un campo acepta entradas

Para proporcionar comentarios cuando se interactúa con un elemento, CSS usa la pseudoclase :focus-within para cambiar la apariencia de varios elementos. Analicemos .fieldset-item, es muy interesante:

.fieldset-item {
  ...

  &:focus-within {
    background: var(--surface2);

    & svg {
      fill: white;
    }

    & picture {
      clip-path: circle(50%);
      background: var(--brand-bg-gradient) fixed;
    }
  }
}

Cuando uno de los elementos secundarios de este elemento tiene focus-within:

  1. Al fondo de .fieldset-item, se le asigna un color de superficie de mayor contraste.
  2. El svg anidado se rellena de blanco para obtener un mayor contraste.
  3. El clip-path <picture> anidado se expande a un círculo completo y el fondo se rellena con el gradiente fijo brillante.

Período personalizado

Con el siguiente elemento de entrada HTML, te mostraré cómo personalicé su apariencia:

<input type="range">

Hay 3 partes de este elemento que debemos personalizar:

  1. Elemento o contenedor de rango
  2. Realice un seguimiento
  3. Thumb

Estilos de elementos de rango

input[type="range"] {
  /* style setting variables */
  --track-height: .5ex;
  --track-fill: 0%;
  --thumb-size: 3ex;
  --thumb-offset: -1.25ex;
  --thumb-highlight-size: 0px;

  appearance: none;         /* clear styles, make way for mine */
  display: block;
  inline-size: 100%;        /* fill container */
  margin: 1ex 0;            /* ensure thumb isn't colliding with sibling content */
  background: transparent;  /* bg is in the track */
  outline-offset: 5px;      /* focus styles have space */
}

Las primeras líneas de CSS son las partes personalizadas de los estilos, y espero que etiquetarlas con claridad sea de ayuda. El resto de los estilos son, en su mayoría, estilos de restablecimiento, para proporcionar una base coherente para compilar las partes difíciles del componente.

Estilos de pistas

input[type="range"]::-webkit-slider-runnable-track {
  appearance: none; /* clear styles, make way for mine */
  block-size: var(--track-height);
  border-radius: 5ex;
  background:
    /* hard stop gradient:
        - half transparent (where colorful fill we be)
        - half dark track fill
        - 1st background image is on top
    */
    linear-gradient(
      to right,
      transparent var(--track-fill),
      var(--surface1) 0%
    ),
    /* colorful fill effect, behind track surface fill */
    var(--brand-bg-gradient) fixed;
}

El truco consiste en "revelar" el color de relleno vibrante. Esto se hace con el gradiente de detención brusca en la parte superior. El gradiente es transparente hasta el porcentaje de relleno y, después de eso, usa el color de la superficie de la pista sin rellenar. Detrás de esa superficie sin relleno, hay un color de ancho completo, que espera que la transparencia lo revele.

Estilo de relleno de la pista

Mi diseño requiere JavaScript para mantener el estilo de relleno. Existen estrategias solo de CSS, pero requieren que el elemento de control deslizante tenga la misma altura que la pista, y no pude encontrar una armonía dentro de esos límites.

/* grab sliders on page */
const sliders = document.querySelectorAll('input[type="range"]')

/* take a slider element, return a percentage string for use in CSS */
const rangeToPercent = slider => {
  const max = slider.getAttribute('max') || 10;
  const percent = slider.value / max * 100;

  return `${parseInt(percent)}%`;
};

/* on page load, set the fill amount */
sliders.forEach(slider => {
  slider.style.setProperty('--track-fill', rangeToPercent(slider));

  /* when a slider changes, update the fill prop */
  slider.addEventListener('input', e => {
    e.target.style.setProperty('--track-fill', rangeToPercent(e.target));
  })
})

Creo que esto mejora la visualización. El control deslizante funciona muy bien sin JavaScript, la prop --track-fill no es obligatoria, simplemente no tendrá un estilo de relleno si no está presente. Si JavaScript está disponible, propaga la propiedad personalizada y, al mismo tiempo, observa los cambios del usuario, sincronizando la propiedad personalizada con el valor.

Aquí tienes una excelente publicación en CSS-Tricks de Ana Tudor, en la que se muestra una solución solo de CSS para el relleno de la pista. También me pareció muy inspirador este elemento de range.

Estilos de miniaturas

input[type="range"]::-webkit-slider-thumb {
  appearance: none; /* clear styles, make way for mine */
  cursor: ew-resize; /* cursor style to support drag direction */
  border: 3px solid var(--surface3);
  block-size: var(--thumb-size);
  inline-size: var(--thumb-size);
  margin-top: var(--thumb-offset);
  border-radius: 50%;
  background: var(--brand-bg-gradient) fixed;
}

La mayoría de estos estilos son para crear un círculo agradable. Nuevamente, verás el gradiente de fondo fijo que unifica los colores dinámicos de los controles deslizantes, las pistas y los elementos SVG asociados. Separé los estilos de la interacción para ayudar a aislar la técnica de box-shadow que se usa para el resaltado de desplazamiento:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

::-webkit-slider-thumb {
  …

  /* shadow spread is initally 0 */
  box-shadow: 0 0 0 var(--thumb-highlight-size) var(--thumb-highlight-color);

  /* if motion is OK, transition the box-shadow change */
  @media (--motionOK) {
    & {
      transition: box-shadow .1s ease;
    }
  }

  /* on hover/active state of parent, increase size prop */
  @nest input[type="range"]:is(:hover,:active) & {
    --thumb-highlight-size: 10px;
  }
}

El objetivo era crear un elemento visual animado y fácil de administrar para destacar los comentarios de los usuarios. Si uso una sombra de cuadro, puedo evitar activar el diseño con el efecto. Para ello, creo una sombra que no está desenfocada y que coincide con la forma circular del elemento de control deslizante. Luego, cambio y hago la transición de su tamaño de dispersión cuando se coloca el cursor sobre él.

Si tan solo el efecto de resaltado fuera tan fácil en las casillas de verificación…

Selectores entre navegadores

Descubrí que necesitaba estos selectores -webkit- y -moz- para lograr la coherencia entre navegadores:

input[type="range"] {
  &::-webkit-slider-runnable-track {}
  &::-moz-range-track {}
  &::-webkit-slider-thumb {}
  &::-moz-range-thumb {}
}

Casilla de verificación personalizada

Con el siguiente elemento de entrada HTML, te mostraré cómo personalicé su apariencia:

<input type="checkbox">

Hay 3 partes de este elemento que debemos personalizar:

  1. Elemento de casilla de verificación
  2. Etiquetas asociadas
  3. Efecto de resaltado

Elemento de casilla de verificación

input[type="checkbox"] {
  inline-size: var(--space-sm);   /* increase width */
  block-size: var(--space-sm);    /* increase height */
  outline-offset: 5px;            /* focus style enhancement */
  accent-color: var(--brand);     /* tint the input */
  position: relative;             /* prepare for an absolute pseudo element */
  transform-style: preserve-3d;   /* create a 3d z-space stacking context */
  margin: 0;
  cursor: pointer;
}

Los estilos transform-style y position se preparan para el seudoelemento que introduciremos más adelante para aplicar diseño al elemento destacado. De lo contrario, se trata principalmente de cuestiones de estilo menores y subjetivas de mi parte. Me gusta que el cursor sea un puntero, me gustan los desplazamientos de contorno, las casillas de verificación predeterminadas son demasiado pequeñas y, si se admite accent-color, estas casillas de verificación se incluyen en el esquema de colores de la marca.

Etiquetas de casillas de verificación

Es importante proporcionar etiquetas para las casillas de verificación por 2 motivos. La primera es representar para qué se usa el valor de la casilla de verificación, para responder "¿activado o desactivado para qué?". En segundo lugar, para la UX, los usuarios web se acostumbraron a interactuar con las casillas de verificación a través de sus etiquetas asociadas.

entrada
<input
  type="checkbox"
  id="text-notifications"
  name="text-notifications"
>
etiqueta
<label for="text-notifications">
  <h3>Text Messages</h3>
  <small>Get notified about all text messages sent to your device</small>
</label>

En tu etiqueta, coloca un atributo for que apunte a una casilla de verificación por ID: <label for="text-notifications">. En la casilla de verificación, duplica el nombre y el ID para asegurarte de que se encuentre con diferentes herramientas y tecnologías, como un mouse o un lector de pantalla: <input type="checkbox" id="text-notifications" name="text-notifications">. :hover, :active y más se incluyen de forma gratuita con la conexión, lo que aumenta las formas en que se puede interactuar con tu formulario.

Destacado de casilla de verificación

Quiero que mis interfaces sean coherentes, y el elemento del control deslizante tiene un buen destaque de miniatura que me gustaría usar con la casilla de verificación. La miniatura pudo usar box-shadow y su propiedad spread para aumentar y reducir la escala de una sombra. Sin embargo, ese efecto no funciona aquí porque nuestras casillas de verificación son, y deben ser, cuadradas.

Pude lograr el mismo efecto visual con un seudoelemento y una cantidad desafortunada de CSS complicado:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

input[type="checkbox"]::before {
  --thumb-scale: .01;                        /* initial scale of highlight */
  --thumb-highlight-size: var(--space-xl);

  content: "";
  inline-size: var(--thumb-highlight-size);
  block-size: var(--thumb-highlight-size);
  clip-path: circle(50%);                     /* circle shape */
  position: absolute;                         /* this is why position relative on parent */
  top: 50%;                                   /* pop and plop technique (https://web.dev/centering-in-css#5-pop-and-plop) */
  left: 50%;
  background: var(--thumb-highlight-color);
  transform-origin: center center;            /* goal is a centered scaling circle */
  transform:                                  /* order here matters!! */
    translateX(-50%)                          /* counter balances left: 50% */
    translateY(-50%)                          /* counter balances top: 50% */
    translateZ(-1px)                          /* PUTS IT BEHIND THE CHECKBOX */
    scale(var(--thumb-scale))                 /* value we toggle for animation */
  ;
  will-change: transform;

  @media (--motionOK) {                       /* transition only if motion is OK */
    & {
      transition: transform .2s ease;
    }
  }
}

/* on hover, set scale custom property to "in" state */
input[type="checkbox"]:hover::before {
  --thumb-scale: 1;
}

Crear un pseudoelemento circular es un trabajo sencillo, pero colocarlo detrás del elemento al que está adjunto fue más difícil. Aquí está el antes y el después de que lo arreglé:

Sin duda, es una microinteracción, pero es importante para mí mantener la coherencia visual. La técnica de escalamiento de la animación es la misma que usamos en otros lugares. Establecemos una propiedad personalizada en un valor nuevo y dejamos que CSS la haga la transición según las preferencias de movimiento. La función clave aquí es translateZ(-1px). El elemento superior creó un espacio en 3D, y este elemento secundario de pseudoelemento lo aprovechó colocándose un poco más atrás en el espacio Z.

Accesibilidad

En el video de YouTube, se muestra una excelente demostración de las interacciones del mouse, el teclado y el lector de pantalla para este componente de configuración. Destacaré algunos de los detalles aquí.

Opciones de elementos HTML

<form>
<header>
<fieldset>
<picture>
<label>
<input>

Cada uno de estos contiene sugerencias y sugerencias para la herramienta de navegación del usuario. Algunos elementos proporcionan sugerencias de interacción, otros conectan la interactividad y otros ayudan a dar forma al árbol de accesibilidad por el que navega un lector de pantalla.

Atributos HTML

Podemos ocultar elementos que no necesitan los lectores de pantalla, en este caso, el ícono junto al control deslizante:

<picture aria-hidden="true">

En el video anterior, se muestra el flujo del lector de pantalla en macOS. Observa cómo el enfoque de entrada se mueve directamente de un control deslizante al siguiente. Esto se debe a que ocultamos el ícono que podría haber sido una parada en el camino hacia el siguiente control deslizante. Sin este atributo, el usuario tendría que detenerse, escuchar y pasar la imagen que tal vez no pueda ver.

El SVG es un montón de matemáticas, así que agreguemos un elemento <title> para un título que aparezca cuando se coloque el mouse sobre él y un comentario legible por humanos sobre lo que crean las matemáticas:

<svg viewBox="0 0 24 24">
  <title>A note icon</title>
  <path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
</svg>

Aparte de eso, usamos suficiente HTML marcado con claridad para que el formulario se pruebe muy bien con el mouse, el teclado, los controles de videojuegos y los lectores de pantalla.

JavaScript

Ya expliqué cómo se administraba el color de relleno de la barra desde JavaScript, así que ahora veamos el código de JavaScript relacionado con <form>:

const form = document.querySelector('form');

form.addEventListener('input', event => {
  const formData = Object.fromEntries(new FormData(form));
  console.table(formData);
})

Cada vez que se interactúa con el formulario y se modifica, la consola lo registra como un objeto en una tabla para que se pueda revisar fácilmente antes de enviarlo a un servidor.

Captura de pantalla de los resultados de console.table(), en la que se muestran los datos del formulario en una tabla

Conclusión

Ahora que sabes cómo lo hice, ¿cómo lo harías tú? Esto hace que la arquitectura de componentes sea divertida. ¿Quién creará la primera versión con ranuras en su framework favorito? 🙂

Diversifiquemos nuestros enfoques y aprendamos todas las formas de crear en la Web. Crea una demostración, envíame un tweet con los vínculos y la agregaré a la sección Remixes de la comunidad que aparece a continuación.

Remixes de la comunidad

  • @tomayac con su estilo en relación con el área de desplazamiento para las etiquetas de las casillas de verificación. Esta versión no tiene espacio de desplazamiento entre los elementos: demostración y fuente.