Cómo crear un menú de opciones tipo rueda con CSS y HTML

- Andrés Cruz

EN In english

Cómo crear un menú de opciones tipo rueda con CSS y HTML

Los menús radiales o de tipo rueda son una excelente solución de diseño para optimizar el espacio en interfaces modernas, especialmente en dispositivos móviles. Inspirados originalmente por el concepto de Floating Action Button (FAB) de Material Design de Google, estos elementos permiten agrupar múltiples acciones en un solo punto focal, desplegándolas de forma elegante y animada.

En esta guía, exploraremos cómo crear diferentes estilos de menús circulares utilizando exclusivamente CSS y HTML, con un toque mínimo de JavaScript para manejar las interacciones de clic. Veremos desde diseños minimalistas hasta opciones más avanzadas con imágenes.

Fundamentos de un Menú Circular

La lógica detrás de un menú de opciones tipo rueda es sencilla pero potente:

  1. Estado Inicial: Todas las opciones del menú están ocultas y posicionadas exactamente debajo del botón central (el "trigger").
  2. Activación: Al hacer clic o pasar el cursor, aplicamos una transición de CSS que mueve cada opción a una coordenada específica mediante transform: translate(x, y).
  3. Animación: Utilizamos transition para que el movimiento sea fluido, creando ese efecto de "abanico" o "rueda".

Ejemplo de menú circular desplegable

1. Menú Radial con Interacción de Clic

Este primer diseño es el más fiel al concepto de Material Design. El menú se despliega únicamente cuando el usuario decide interactuar con él.

El JavaScript: Para este ejemplo, utilizamos un script sencillo que añade o quita la clase .open al contenedor principal.

// Manejador para abrir/cerrar el menú
document.querySelectorAll('.toggle-btn').forEach(btn => {
    btn.addEventListener('click', () => {
        const parent = btn.closest('.filter-btn');
        parent.classList.toggle('open');
    });
});

// Cerrar al seleccionar una opción
document.querySelectorAll('.filter-btn a').forEach(link => {
    link.addEventListener('click', () => {
        link.closest('.filter-btn').classList.remove('open');
    });
});

El CSS Principal: Definimos la base del botón y las transiciones. Es importante notar el uso de cubic-bezier para darle un efecto de rebote más natural.

.filter-btn {
  position: absolute;
  width: 40px;
  height: 40px;
  transition: all 0.4s ease;
}

.filter-btn a {
  position: absolute;
  background: #DE5513;
  border-radius: 50%;
  width: 40px;
  height: 40px;
  line-height: 40px;
  text-align: center;
  color: #fff;
  z-index: 1;
  transition: all .4s cubic-bezier(.68, -0.55, .265, 1.55);
  box-shadow: 0 4px 10px rgba(0,0,0,0.3);
}

/* Posicionamiento de las opciones al abrirse */
.filter-btn.open a:nth-child(1) { transform: translate(0, -60px); }
.filter-btn.open a:nth-child(2) { transform: translate(-55px, -35px); }
.filter-btn.open a:nth-child(3) { transform: translate(55px, -35px); }

Como podemos ver, es un diseño básico en donde principalmente se definen forma, color y tamaño del botón; además de otros aspectos como la transiciones y posición de las opciones que forman el menú tipo rueda/circular definido con filter-btn a.

Y el segundo para mostrar el botón de rueda/circular ya desplegado:

Menú de opciones tipo Rueda 1

.filter-btn.open a:nth-child(1) {
  transform: translate(0,-55px);
}
.filter-btn.open a:nth-child(2) {
  transform: translate(-50px,-34px);
}
.filter-btn.open a:nth-child(3) {
  transform: translate(50px,-34px);
}
.filter-btn.open a:nth-child(4) {
      transform: translate(50px,20px);
}
.filter-btn.open a:nth-child(5) {
      transform: translate(-50px,20px);
}
.filter-btn.open a:nth-child(6) {
      transform: translate(0,55px);
}
.filter-btn.open span.toggle-btn.ion-android-funnel {
  background-color: #AC3D07;  
}
.filter-btn.open .ion-android-funnel:before {
  content: "\f2d7";
}

Finalmente obtenemos:

2. Variante con Fondo Expansivo (Efecto "After")

Podemos añadir una capa extra de sofisticación utilizando el pseudoelemento ::after para crear un fondo circular que se expande detrás de los iconos, mejorando la visibilidad de las opciones.

Menú de opciones tipo Rueda 2
.filter-btn::after {
  content: '';
  width: 170px;
  height: 170px;
  background-color: rgba(222, 85, 19, 0.9);
  position: absolute;
  top: -65px;
  right: -65px;
  border-radius: 50%;
  transform: scale(0);
  transition: transform 0.3s ease-in-out;
  z-index: 0;
}

.filter-btn.open::after {
  transform: scale(1);
}

Finalmente obtenemos:

3. Menú Circular basado en Hover (Sin JavaScript)

Si prefieres una solución más ligera y rápida para escritorio, puedes utilizar el evento :hover. Esto elimina la necesidad de JavaScript, aunque debes tener cuidado con la experiencia en dispositivos táctiles.

Menú de opciones tipo Rueda 3

En este modelo, el menú se encuentra rotado 180 grados por defecto y vuelve a su posición original al pasar el puntero:

.menu ul {
    transform: rotate(180deg) translateY(-2em);
    transition: 1s all ease;
    opacity: 0;
}

.menu:hover ul {
    transform: rotate(0deg) translateY(-1em);
    opacity: 1;
}

4. Menú Radial con Imágenes y Recortes

Este es uno de los diseños más creativos. En lugar de iconos simples, utilizamos imágenes que forman una circunferencia perfecta al desplegarse. Cada imagen es un sector del círculo.

.wrap a:nth-child(1) {
  border-radius: 40vmin 0 0 0;
  transform-origin: 110% 110%;
  background-image: url('https://farm3.staticflickr.com/2827/10384422264_d9c7299146.jpg');
  background-size: cover;
}

Como podemos ver, (entre varias cosas) definimos el tamaño para cada ítem cuyo alto es de 1.4em:

.menu li {
/* ... */
    height: 1.4em;
    opacity: 0;
    z-index: -1;
}

También se especifica que todas las opciones no se muestren mediante la propiedad opacity establecida en cero; una vez que nos posicionamos sobre el menú circular (el círculo del medio) le cambiamos la opacidad a uno para que de esta forma sea visible:

.menu:hover li {
    opacity: 1;
}

Además que activamos la rotación del menú para que regrese a su posición original:

.menu:hover ul {
    transform: rotate(0deg) translateY(-1em);
}

Que por defecto el menú circular se encuentra rotado en 180 grados:

.menu ul {
    transform: rotate(180deg) translateY(-2em);
    transition: 1s all;
}

Y de aquí podemos entender cómo funciona la transición de los elementos del menú cuando son mostrados, los cuales realizan un desplazamiento de unos 180 grados; por último como elemento fundamental es conocer la composición de cada ítem en el menú:

.menu li:nth-of-type(1) {
    transform: rotate(-90deg);
    position: absolute;
    left: -1.2em;
    top: -4.2em;
}
.menu li:nth-of-type(2) {
    transform: rotate(-45deg);
    position: absolute;
    left: 2em;
    top: -3em;
}
.menu li:nth-of-type(3) {
    position: absolute;
    left: 3.4em;
    top: 0.3em;
}
.menu li:nth-of-type(4) {
    transform: rotate(45deg);
    position: absolute;
    left: 2em;
    top: 3.7em;
}
.menu li:nth-of-type(5) {
    transform: rotate(90deg);
    position: absolute;
    left: -1.2em;
    top: 5em;
}

5. Menú tipo rueda/circular en base a imágenes y evento hover

El siguiente menú que veremos es de un tipo que despliega un conjunto de imágenes recortadas a través de una circunferencia:

Este menú presenta un funcionamiento similar al anterior y es que muestra el abanico de opciones al postrar el cursor encima de la sección visible del menú (evento hover) el cual es una circunferencia con el típico icono de menú de hamburguesa; el resto de las opciones se encuentran ocultas mediante la propiedad opacity establecida en cero:

.wrap{
  position:relative;
  width:80vmin; height:80vmin;
  margin:0 auto;
  background:inherit;
  transform:scale(0.2) translatez(0px);
  opacity:0;
  transition:transform .5s, opacity .5s;
}

Una vez que nos posicionamos encima del menú de hamburguesa se muestran todas las opciones:

span:hover + .wrap, .wrap:hover{
  transform:scale(.8) translateZ(0px);
  opacity:1;
}

Las imágenes que se muestran al posicionar el cursor encima del ícono de tipo hamburguesa tienen la siguiente definición:

a:nth-child(1){
  border-radius:40vmin 0 0 0;
  transform-origin: 110% 110%;
  transition:transform .4s .15s;
}
a:nth-child(1) div{
  background-image:url('https://farm3.staticflickr.com/2827/10384422264_d9c7299146.jpg');
}
a:nth-child(2){
  border-radius:0 40vmin 0 0;
  left:52.5%;
  transform-origin: -10% 110%;
  transition:transform .4s .2s;
}
a:nth-child(2) div{
  background-image:url('https://farm7.staticflickr.com/6083/6055581292_d94c2d90e3.jpg');
}
a:nth-child(3){
  border-radius:0 0 0 40vmin;
  top:52.5%;
  transform-origin: 110% -10%;
  transition:transform .4s .25s;
}

Como podemos apreciar se varía la imagen según el enlace mediante la propiedad :nth-child que también ha sido tema en anteriores entradas:

La pseudo-class nth-child en CSS

Finalmente con las propiedades top, left desplazamos los figuras que componen el menú desplegado.

Modernización: Variables CSS y Accesibilidad

Uso de Variables CSS

En lugar de calcular manualmente cada translate(x, y), puedes usar variables para controlar la distancia y el ángulo, facilitando cambios globales:

.filter-btn {
    --distancia: 80px;
}
.open a:nth-child(1) { transform: translateY(calc(-1 * var(--distancia))); }

Accesibilidad (A11y)

No olvides que estos menús deben ser operables con el teclado. Asegúrate de:

  • Utilizar etiquetas <button> para el activador principal.
  • Añadir atributos aria-expanded="false" que cambien a true vía JS.
  • Asegurar que el contraste de color entre el icono y el fondo sea suficiente (ratio 4.5:1).

Conclusión

Crear un menú de opciones tipo rueda con CSS y HTML no solo mejora la estética de tu sitio, sino que también ofrece una experiencia de usuario interactiva y fluida. Ya sea que elijas una implementación basada en clics o en hover, la clave está en el uso inteligente de las transformaciones y transiciones de CSS.

Aprende a crear menús radiales y circulares modernos con CSS y HTML. Guía paso a paso con ejemplos de interacción (clic/hover), uso de variables y accesibilidad

Acepto recibir anuncios de interes sobre este Blog.

Andrés Cruz

EN In english
<script> window.addEventListener('scroll', function() { if (window.scriptsLoaded) return; loadThirdPartyScripts(); }, { once: true }); window.addEventListener('mousemove', function() { if (window.scriptsLoaded) return; loadThirdPartyScripts(); }, { once: true }); window.addEventListener('touchstart', function() { if (window.scriptsLoaded) return; loadThirdPartyScripts(); }, { once: true }); // Fallback if no interaction window.addEventListener('load', function() { setTimeout(function() { if (!window.scriptsLoaded) loadThirdPartyScripts(); }, 8000); }); function loadThirdPartyScripts() { if (window.scriptsLoaded) return; window.scriptsLoaded = true; console.log('Loading third party scripts...'); // Google Analytics var gtagScript = document.createElement('script'); gtagScript.src = 'https://www.googletagmanager.com/gtag/js?id=G-F22688T9RL'; gtagScript.async = true; document.head.appendChild(gtagScript); gtagScript.onload = function() { window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag('js', new Date()); gtag('config', 'G-F22688T9RL'); }; // Google ADS const adScript = document.createElement('script'); adScript.src = "https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"; adScript.setAttribute('data-ad-client', 'ca-pub-5280469223132298'); adScript.async = true; document.head.appendChild(adScript); // Facebook Pixel (function(f, b, e, v, n, t, s) { if (f.fbq) return; n = f.fbq = function() { n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments) }; if (!f._fbq) f._fbq = n; n.push = n; n.loaded = !0; n.version = '2.0'; n.queue = []; t = b.createElement(e); t.async = !0; t.src = v; s = b.getElementsByTagName(e)[0]; s.parentNode.insertBefore(t, s); })(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js'); fbq('init', '1643487712945352'); fbq('track', 'PageView'); } </script> <noscript> <img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1643487712945352&ev=PageView&noscript=1" /> </noscript>