Menús Grandes, Pantallas Pequeñas: Navegación Multi-Nivel Adaptiva
() translation by (you can also view the original English article)
Si alguna vez has trabajado en un sitio web adaptivo, sin duda tuvo que hacer frente a uno de los problemas más difíciles en este campo emergente: la navegación. Para una navegación simple, las soluciones pueden ser directas. Sin embargo, si usted está trabajando en algo un poco más complejo, quizás con múltiples listas anidadas y listas desplegables, un reordenamiento más dramático puede ser necesario.
En este enfoque a la navegación adaptiva, vamos a utilizar un método que puede acomodar grandes menús de navegación multi-nivel utilizando media queries y jQuery, mientras tratamos de mantener nuestro marcado simple y nuestros recursos externos al mínimo.
La Meta: Menú Desplegable Adaptivo
Esto es lo que buscamos:



- En pantallas más grandes, muestre un menú desplegable horizontal con hasta 2 niveles de sub-menús que aparecen cuando se pasa el cursor sobre el elemento padr.
- En pantallas más pequeñas, un botón "menú" que muestra nuestro menú verticalmente, desplegando submenús cuando se hace clic/toca el elemento principal.
Paso 1: El Marcado
Nuestro marcado es una lista no ordenada bastante directa con listas anidadas contenidas en los elementos de la lista. No utilizo intencionalmente ninguna clase o ID en ningún elemento, sólo en la lista no ordenada padre, de modo que el menú sea compatible con los menús generados por CMS.
1 |
<div class="container"> |
2 |
|
3 |
<a class="toggleMenu" href="#">Menu</a> |
4 |
<ul class="nav"> |
5 |
<li class="test"> |
6 |
<a href="#">Shoes</a> |
7 |
<ul>
|
8 |
<li>
|
9 |
<a href="#">Womens</a> |
10 |
<ul>
|
11 |
<li><a href="#">Sandals</a></li> |
12 |
<li><a href="#">Sneakers</a></li> |
13 |
<li><a href="#">Wedges</a></li> |
14 |
<li><a href="#">Heels</a></li> |
15 |
<li><a href="#">Loafers</a></li> |
16 |
<li><a href="#">Flats</a></li> |
17 |
</ul>
|
18 |
</li>
|
19 |
<li>
|
20 |
<a href="#">Mens</a> |
21 |
<ul>
|
22 |
<li><a href="#">Loafers</a></li> |
23 |
<li><a href="#">Sneakers</a></li> |
24 |
<li><a href="#">Formal</a></li> |
25 |
</ul>
|
26 |
</li>
|
27 |
</ul>
|
28 |
</li>
|
Paso 2: Estilo Básico
Vamos a añadir un estilo muy básico para que nuestra lista parezca una barra de navegación:
1 |
body, nav, ul, li, a {margin: 0; padding: 0;} |
2 |
body {font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } |
3 |
a {text-decoration: none;} |
4 |
.container { |
5 |
width: 90%; |
6 |
max-width: 900px; |
7 |
margin: 10px auto; |
8 |
}
|
9 |
.toggleMenu { |
10 |
display: none; |
11 |
background: #666; |
12 |
padding: 10px 15px; |
13 |
color: #fff; |
14 |
}
|
15 |
.nav { |
16 |
list-style: none; |
17 |
*zoom: 1; |
18 |
background:#175e4c; |
19 |
position: relative; |
20 |
}
|
21 |
.nav:before, |
22 |
.nav:after { |
23 |
content: " "; |
24 |
display: table; |
25 |
}
|
26 |
.nav:after { |
27 |
clear: both; |
28 |
}
|
29 |
.nav ul { |
30 |
list-style: none; |
31 |
width: 9em; |
32 |
}
|
33 |
.nav a { |
34 |
padding: 10px 15px; |
35 |
color:#fff; |
36 |
*zoom: 1; |
37 |
}
|
38 |
.nav > li { |
39 |
float: left; |
40 |
border-top: 1px solid #104336; |
41 |
z-index: 200; |
42 |
}
|
43 |
.nav > li > a { |
44 |
display: block; |
45 |
}
|
46 |
.nav li ul { |
47 |
position: absolute; |
48 |
left: -9999px; |
49 |
z-index: 100; |
50 |
}
|
51 |
.nav li li a { |
52 |
display: block; |
53 |
background: #1d7a62; |
54 |
position: relative; |
55 |
z-index:100; |
56 |
border-top: 1px solid #175e4c; |
57 |
}
|
58 |
.nav li li li a { |
59 |
background:#249578; |
60 |
z-index:200; |
61 |
border-top: 1px solid #1d7a62; |
62 |
}
|



Hemos flotado nuestros elementos de lista en una línea horizontal, establecido algunos colores y ocultado los submenús de la pantalla con posicionamiento absoluto. Si usted se está preguntando sobre la línea 20, es un método simple de clearfix que encuentro efectivo en este tipo de situaciones (lea más en el blog de Nicolas Gallagher).
Paso 3: Menú Desplegable Horizontal
A continuación, vamos a obtener los menús desplegables horizontales. Si bien esto podría hacerse con CSS puro usando el pseudo-selector :hover
, voy a añadirlo usando JavaScript ya que vamos a intercambiar menús que se activen al hacer clic en nuestra versión de pantalla pequeña.
Ya que estamos utilizando posicionamiento absoluto para mover nuestros submenús fuera de la pantalla, vamos a añadir algunas reglas .hover
que posicionarán los submenús en relación con sus padres cuando la clase .hover
esté presente (de la que nos encargaremos con jQuery).
1 |
.nav li { |
2 |
position: relative; |
3 |
}
|
4 |
.nav > li.hover > ul { |
5 |
left: 0; |
6 |
}
|
7 |
.nav li li.hover ul { |
8 |
left: 100%; |
9 |
top: 0; |
10 |
}
|
Ahora vamos a añadir un par de líneas de jQuery simple para agregar la clase .hover a los elementos de la lista cuando se pasa el cursor sobre ellos.
1 |
$(document).ready(function() { |
2 |
$(".toggleMenu").css("display", "none"); |
3 |
$(".nav li").hover(function() { |
4 |
$(this).addClass('hover'); |
5 |
}, function() { |
6 |
$(this).removeClass('hover'); |
7 |
}); |
8 |
}); |



Y de esta manera tenemos un menú desplegable funcional, de varios niveles.
Paso 4: Menú Desplegable Vertical
Nuestro encantador menú desplegable horizontal, por desgracia, parece bastante pequeño en las pantallas móviles, así que nos vamos a asegurar de que los navegadores móviles se ampliarán por completo al cargar nuestra página si añadimos la etiqueta meta viewport.
1 |
<meta name="viewport" content="width=device-width, initial-scale=1"> |
¡Claro, ahora nuestro menú desplegable se ve aún peor en dispositivos móviles y la mayoría de él ni siquiera encaja en la pantalla! Vamos a añadir algunas media queries para dar estilo a nuestra lista en una lista vertical debajo de nuestro "breakpoint". Nuestro breakpoint está determinado por el punto en el que nuestro menú se rompe en dos líneas, en mi caso eso es aproximadamente en 800px.
En nuestro breakpoint, eliminaremos los floats y estableceremos el ancho de los elementos de la lista y las listas no ordenadas a width: 100%
. En este momento, cuando pasamos el cursor sobre los elementos padre, sus listas hijo se muestran por encima de los elementos debajo de ellas. Preferiríamos que los otros elementos de nivel de lista superior se desplacen. Así que en lugar de cambiar la posición left
de la lista no ordenada, simplemente estableceremos el valor de position
como static
.
1 |
@media screen and (max-width: 800px) { |
2 |
.nav > li { |
3 |
float: none; |
4 |
}
|
5 |
.nav ul { |
6 |
display: block; |
7 |
width: 100%; |
8 |
}
|
9 |
.nav > li.hover > ul , .nav li li.hover ul { |
10 |
position: static; |
11 |
}
|
12 |
}
|

Paso 5: Conversión del Paso del Cursor a Clic
Puesto que usted no se puede pasar el cursor en una pantalla táctil (todavía), vamos a crear algún código condicional para comprobar el ancho de la ventana, luego escribiremos el código para establecer los eventos click()
y hover()
.
1 |
$(document).ready(function() { |
2 |
var ww = document.body.clientWidth; |
3 |
if (ww < 800) { |
4 |
$(".toggleMenu").css("display", "inline-block"); |
5 |
$(".nav li a").click(function() { |
6 |
$(this).parent("li").toggleClass('hover'); |
7 |
}); |
8 |
} else { |
9 |
$(".toggleMenu").css("display", "none"); |
10 |
$(".nav li").hover(function() { |
11 |
$(this).addClass('hover'); |
12 |
}, function() { |
13 |
$(this).removeClass('hover'); |
14 |
}); |
15 |
} |
16 |
}); |
Para el evento click
, hemos tenido que cambiar el destino del elemento de lista al elemento padre, ya que las listas están anidadas y hacer clic sobre un elemento de lista podría abrir también a sus nietos. Sin embargo, el problema con este cambio es que al hacer clic en una etiqueta ancla recargará la página, pero no podemos evitar el comportamiento predeterminado en todas las etiquetas ancla que son descendientes de los elementos de la lista.
Para corregir esto, vamos a añadir un poco de jQuery para agregar la clase .parent
a cualquier elemento de lista cuyo hijo ancla tenga hermanos, a continuación, sólo apunte a estas anclas (de nuevo, tratando de mantener el marcado flexible).
1 |
$(".nav li a").each(function() { |
2 |
if ($(this).next().length > 0) { |
3 |
$(this).addClass("parent"); |
4 |
}; |
5 |
}) |
6 |
if (ww < 800) { |
7 |
$(".toggleMenu").css("display", "inline-block"); |
8 |
$(".nav li a.parent").click(function(e) { |
9 |
e.preventDefault(); |
10 |
$(this).parent("li").toggleClass('hover'); |
11 |
}); |
12 |
} else { |
13 |
... } |
Paso 6: Conmutar el Menú
Nuestro menú se ve muy bien en los dispositivos móviles ahora, pero se está tomando una gran cantidad espacio valioso de pantalla. Un nuevo enfoque popular ha sido alternar listas de navegación utilizando un botón, normalmente con la palabra "Menú" o un icono de menú. Vamos a obtener nuestro enlace de alternancia que muestre nuestra lista de navegación sólo cuando se hace clic sobre él.
1 |
$(".toggleMenu").click(function(e) { |
2 |
e.preventDefault(); |
3 |
$(".nav").toggle(); |
4 |
}); |
5 |
|
6 |
if (ww < 800) { |
7 |
$(".toggleMenu").css("display", "inline-block"); |
8 |
$(".nav").hide(); |
9 |
} else { |
10 |
... |
11 |
} |

Paso 7: Un Poco Más de Estilo
Ya que ahora tenemos nuestros elementos de lista padre seleccionados con una clase, ¿por qué no añadir una pequeña de flecha hacia abajo para que nuestros usuarios sepan qué elementos de la lista tienen hijos?
1 |
.nav > li > .parent { |
2 |
background-position: 95% 50%; |
3 |
}
|
4 |
.nav li li .parent { |
5 |
background-image: url("images/downArrow.png"); |
6 |
background-repeat: no-repeat; |
7 |
background-position: 95% 50%; |
8 |
}
|
9 |
@media screen and (max-width: 800px) { |
10 |
.nav > li > .parent { |
11 |
background-position: 95% 50%; |
12 |
}
|
13 |
.nav li li .parent { |
14 |
background-image: url("images/downArrow.png"); |
15 |
background-repeat: no-repeat; |
16 |
background-position: 95% 50%; |
17 |
}
|
18 |
}
|



Bono: Presumiendo
Listo, para propósitos prácticos este menú funciona bastante bien. Si lo abre en un navegador móvil, obtendrá una lista de acordeón vertical muy útil, si lo abre en su navegador de escritorio, obtendrá una agradable lista desplegable horizontal. Pero si usted cambia el tamaño de tu navegador de escritorio a anchos de móvil, nuestra navegación sólo funciona con el posicionamiento del cursor y el menú no se oculta con la característica de alternancia. Esto está bien para la mayoría de las aplicaciones. Después de todo, el visitante promedio de su sitio web no selecciona el borde de su nakvegador y comienza a arrastrar de una manera alocada hacia adentro y hacia afuera.
Sin embargo, si usted está tratando de impresionar a sus compañeros profesionales de la web, esto simplemente no será suficiente. Tendremos que configurar nuestro script para que responda evento resize
y ejecutar nuestro código condicional cuando el navegador se redimensione por debajo del breakpoint. Expandiendo el código demostrado en el excelente tutorial de Creación de Un Diseño Adaptivo Mobile-First, vamos a establecer algunas variables para realizar un seguimiento y actualizar el ancho de nuestro navegador.
Paso 8: Eventos de Ventana
Puesto que queremos comprobar el ancho del navegador tanto en la carga de la página como cuando el navegador se redimensiona, comencemos moviendo todo el código condicional fuera del evento $(document).ready()
y envolviéndolo en una función adjustMenu
.
1 |
var ww = document.body.clientWidth; |
2 |
$(document).ready(function() { |
3 |
$(".toggleMenu").click(function(e) { |
4 |
e.preventDefault(); |
5 |
$(".nav").toggle(); |
6 |
}); |
7 |
$(".nav li a").each(function() { |
8 |
if ($(this).next().length > 0) { |
9 |
$(this).addClass("parent"); |
10 |
}; |
11 |
}) |
12 |
adjustMenu(); |
13 |
}); |
14 |
function adjustMenu() { |
15 |
if (ww < 800) { |
16 |
$(".toggleMenu").css("display", "inline-block"); |
17 |
$(".nav").hide(); |
18 |
$(".nav li a.parent").click(function(e) { |
19 |
e.preventDefault(); |
20 |
$(this).parent("li").toggleClass('hover'); |
21 |
}); |
22 |
} else { |
23 |
$(".toggleMenu").css("display", "none"); |
24 |
$(".nav li").hover(function() { |
25 |
$(this).addClass('hover'); |
26 |
}, function() { |
27 |
$(this).removeClass('hover'); |
28 |
}); |
29 |
} |
30 |
} |
Para hacer un llamado a la función cuando se redimensiona el navegador, vamos a amarrarlo bind
a los eventos de ventana resize
y orientationchange
. Dentro de este evento vamos a redefinir la variable ww
para ajustarse al nuevo ancho del navegador.
1 |
$(window).bind('resize orientationchange', function() { |
2 |
ww = document.body.clientWidth; |
3 |
adjustMenu(); |
4 |
}); |
En este punto, hemos introducido más problemas: aunque esto parece funcionar inicialmente (el menú horizontal se colapsa dentro del botón "menú" que abre el menú), se hace rápidamente evidente que tenemos dos grandes problemas:
- Todo el menú desaparece si cambiamos el tamaño de la ventana de ancho móvil de vuelta más allá del breakpoint.
- El evento hover se sigue disparando en la versión móvil.
Paso 9: Mostrando y Ocultando
Nuestro menú de navegación faltante parece una solución fácil: sólo adicione $("nav").show()
después de la condición mayor-que del breakpoint. Esta solución parece funcionar, pero trae consigo algunos casos particulares difíciles. Dado que el código se reevalúa cada vez que se redimensiona el navegador, cada vez que redimensionamos con el menú abierto, este se cierra automáticamente.
Esto puede parecer un caso extremo poco probable, pero los navegadores móviles son extraños: por ejemplo, en mi Galaxy S, el desplazamiento hacia abajo y luego hacia la parte superior de una página desencadena el evento
resize
. ¡No está bien!
Para solucionar esto, necesitamos tener alguna forma de comprobar si se ha hecho clic en el menú de alternancia. Voy a utilizar una clase añadida en el botón de conmutación de menú porque podría ser útil para el estilo (¿Tal vez queremos una flecha hacia abajo más adelante bajo la línea?) Además de alternar la visualización del menú de navegación, el botón de alternancia de menú ahora alternará su propia clase .active
. De vuelta en nuestra condición de breakpoint menor-que, actualizaremos el código para ocultar nuestro menú de navegación sólo si el menú de alternancia no tiene una clase .active
.
1 |
$(document).ready(function() { |
2 |
$(".toggleMenu").click(function(e) { |
3 |
e.preventDefault(); |
4 |
$(this).toggleClass("active"); |
5 |
$(".nav").toggle(); |
6 |
}); |
7 |
}); |
8 |
if (ww < 800) { |
9 |
$(".toggleMenu").css("display", "inline-block"); |
10 |
if (!$(".toggleMenu").hasClass("active")) { |
11 |
$(".nav").hide(); |
12 |
} else { |
13 |
$(".nav").show(); |
14 |
} |
15 |
$(".nav li a.parent").click(function(e) { |
16 |
e.preventDefault(); |
17 |
$(this).parent("li").toggleClass('hover'); |
18 |
}); |
19 |
} ... |
Paso 10 Desvinculación de Eventos Hover
Para resolver nuestro problema del menú de navegación de tamaño móvil que responde a eventos al pasar el cursor, sólo tenemos que desvincular unbind()
el evento hover de nuestros elementos de lista dentro de la condición de breakpoint menor-que.
1 |
$(".nav li").unbind('mouseenter mouseleave'); |
Sin embargo, esto ilumina un nuevo problema: nuestros eventos click
no funcionan si usted cambia el tamaño del navegador de grande a pequeño. Algo de depuración revela que el evento click
ha estado vinculado al enlace varias veces, por lo que tan pronto como haga clic, la clase .hover
se activa y luego se desactiva inmediatamente. Esto sucede porque toda la función se dispara repetidamente a medida que usted cambia el tamaño de la ventana. Para asegurarnos de que empezamos a alternar desde el lugar correcto, necesitamos desvincular unbind
el evento click antes de volverlo a enlazar.
Pero al redimensionar el navegador de pequeño a grande una vez más, no encontramos nuestro evento hover
, porque lo desvinculamos cuando el navegador era pequeño, y nuestro evento click
aún está ahí, así que vamos a desvincularlo antes de a su vez vincular nuestra declaración hover. También vamos a eliminar los elementos de la lista con una clase de .hover
antes de agregarlos de nuevo en el evento hover, para evitar que los menús se mantengan extrañamente abiertos al ampliar el navegador.
Estoy reescribiendo los eventos .click()
y .hover()
utilizando .bind()
por razones de claridad. Significa exactamente lo mismo.
1 |
if (ww < 800) { |
2 |
$(".toggleMenu").css("display", "inline-block"); |
3 |
if (!$(".toggleMenu").hasClass("active")) { |
4 |
$(".nav").hide(); |
5 |
} else { |
6 |
$(".nav").show(); |
7 |
} |
8 |
$(".nav li").unbind('mouseenter mouseleave'); |
9 |
$(".nav li a.parent").unbind("click").bind("click", function(e){ |
10 |
e.preventDefault(); |
11 |
$(this).parent("li").toggleClass('hover'); |
12 |
}); |
13 |
} else { |
14 |
$(".toggleMenu").css("display", "none"); |
15 |
$(".nav").show(); |
16 |
$(".nav li").removeClass("hover"); |
17 |
$(".nav li a").unbind("click"); |
18 |
$(".nav li").unbind('mouseenter mouseleave').bind('mouseenter mouseleave', function() { |
19 |
$(this).toggleClass('hover'); |
20 |
}); |
21 |
} |
¡Hurra! Todo parece funcionar como debería.
Paso 11: Haga que IE se Comporte
No sería una fiesta si IE7 no llegara a estropear todo ¿o sí? Tenemos un error extraño aquí donde desaparecen nuestros sub-menús cuando se muestran sobre otro contenido (en nuestro ejemplo, algún texto lorem ipsum). Una vez que el cursor alcanza el elemento de párrafo, *bam* no más menús. Estoy bastante seguro de que esto se debe a alguna rareza en la forma en que IE7 se ocupa de la position: relative;
y el problema se resuelve fácilmente al activar hasLayout
en el elemento .nav a
.
1 |
.nav a { |
2 |
*zoom: 1; |
3 |
}
|
Consideraciones Adicionales
Como siempre, usted tendrá que hacer su propia llamada de juicio acerca del navegador y el soporte de características, pero herramientas como Modernizr y respond.js pueden aliviar el dolor de respaldar navegadores antiguos.
He probado este menú en Mobile Safari y todos los navegadores de Android 2.3 sobre los que pude poner mis manos y parece que funciona bastante bien. Sin embargo, esta técnica depende mucho de JavaScript y ya que algunos navegadores móviles (generalmente Blackberry) tienen un soporte muy pobre para JavaScript, es posible que dejemos a algunos usuarios con un menú de navegación inutilizable.
Afortunadamente, hay una serie de métodos que puede utilizar para proporcionar diseños simplificados a dispositivos sin JavaScript. La buena y vieja técnica de agregar una clase .no-js
a la etiqueta body y de removerla en su JavaScript viene a la mente, pero también podría proporcionar atributos href
a los elementos de navegación de nivel superior, enviando a los usuarios al listado general de categoría "zapatos" por ejemplo, y apoyarse en preventDefault
para evitar este comportamiento en dispositivos habilitados para JavaScript.
Por supuesto, las media queries no funcionarán en las versiones anteriores de IE, por lo que tendrá que decidir si vale la pena incluir un polyfill como respond.js para llenar este vacío.
Por último, pero no menos importante, está ese molesto error en iOS que hace que el nivel de zoom cambie cuando se gira el dispositivo. Eche un vistazo al script iOS Orientationchange Fix para aplastar este error.
Lecturas Adicionales
A pesar de que esta técnica puede ser adecuada para ciertas situaciones y estructuras de menú, todavía hay muchas otras opciones disponibles para domar la navegación en dispositivos móviles. Por ejemplo:
- El reciente tuorial de Ryan DeBeasi para una inteligente solución de menús de navegación de un nivel.
- Navegue a través de los compendios de Brad Frost Patrones de Navegación Adaptiva..
- ...y Patrones Complejos de Navegación para Diseño Adaptivo.
- Para una solución orientada a móviles primero, eche un vistazo a los menús de navegación de HTML5 Rocks, Creación de un Diseño Móvil Primero Adaptivo.
¡Siéntase libre de reusar, clonar o bifurcar el repositorio de GitHub, y gracias por leer!