ساخت مگامنوی چند سطحی واکنش‌گرا با CSS — از صفر تا صد

۹۵۹ بازدید
آخرین به‌روزرسانی: ۱۳ شهریور ۱۴۰۲
زمان مطالعه: ۱۶ دقیقه
ساخت مگامنوی چند سطحی واکنش‌گرا با CSS — از صفر تا صد

در این راهنما با مراحل ساخت مگامنوی چند سطحی واکنش‌گرا صرفاً با CSS آشنا می‌شویم. این نوع کامپوننت‌ها معمولاً با کمک گرفتن از جاوا اسکریپت ساخته می‌شوند، اما ما در این راهنما روشی را به شما نشان می‌دهیم که این کار صرفاً با CSS انجام می‌یابد.

مگامنوی چند سطحی واکنش‌گرا

شاید از خود بپرسید چرا باید خودمان را محدود به استفاده از CSS بکنیم؟

  • در این حالت نیازی به دستکاری DOM وجود ندارد.
  • در این حالت محدود به فریمورک نیستیم، یعنی چه از ری‌اکت، چه انگولار یا حتی از HTML و CSS ساده استفاده کنید، مگامنوی شما عمل خواهد کرد.
  • در این حالت عملکرد خوبی داریم، ‌چون دیگر نیازی به بارگذاری و اجرای جاوا اسکریپت وجود ندارد.

محدودیت‌های رویکرد صرفاً CSS

این رویکرد که معرفی می‌کنیم علاوه بر مزایای خود برخی محدودیت‌ها نیز دارد که به شرح زیر هستند.

دسترس‌پذیری: استفاده از یک markup معتبر و خوش‌ساخت به افزایش دسترس‌پذیری کمک می‌کند. اما برای امور زیر به جاوا اسکریپت نیاز داریم:

افزودن پشتیبانی از کیبورد: فعال و غیر فعال کردن خصوصیت‌هایی از قبیل aria-expanded.

پشتیبانی مرورگر: مرورگرهای مختلف حالت‌های ‎:focus را به طرز متفاوتی اعمال می‌کنند. برای نمونه Safari حالت ‎:focus را در زمان کلیک اعمال نمی‌کند. همچنین پشتیبانی داخلی ‎:focus محدود است. این بدان معنی است که این راه‌حل در همه مرورگرها کار نخواهد کرد.

UX: این راهکار صرفاً به این منظور ارائه می‌شود که نشان دهیم تنها با استفاده از CSS چه کارهایی می‌توان انجام داد، ‌اما این به آن معنی نیست که این رویکرد بهترین تجربه کاربری را فراهم می‌سازد. جاوا اسکریپت گزینه‌های بسیار بیشتری برای تنظیم تعامل‌ها در اختیار ما قرار می‌دهد.

راه‌اندازی

از آنجا که ما در این راهنما صرفاً از CSS (یعنی LESS)‌ و HTML استفاده می‌کنیم، ‌مورد خاصی وجود ندارد که بخواهیم راه‌اندازی کنیم. کافی است یک صفحه HTML باز کنید و یک فایل CSS داشته باشید تا کار را آغاز کنید. همچنین می‌توانید به وب‌سایت CodePen بروید و مراحل کار را با LESS پیگیری کنید. در این راهنما فرض کرده‌ایم که شما درک مناسبی از CSS و یا Less دارید. برخی از خصوصیت‌های CSS که در این راهنما مورد استفاده قرار خواهیم داد، به شرح زیر هستند:

  • حالت‌های مختلف از قبیل ‎:focus و ‎:hover و ‏‏‎:focus-within
  • سلکتورهای هم‌نیای (sibling)‌ CSS
  • مشخصه pointer-events
  • انیمیشن و گذارهای CSS
  • مشخصه transform

Markup

ما قصد داریم مثال خود را ساده ارائه کنیم. بدین ترتیب تنها چیزی که نیاز داریم که لیست تودرتوی مناسب سئو است:

1<nav>
2    <a href="javascript:void(0);" class="mobile-menu-trigger">Open mobile menu</a>
3    <ul class="menu menu-bar">
4        <li>
5            <a href="javascript:void(0);" class="menu-link menu-bar-link" aria-haspopup="true">1. Multilevel mega menu</a>
6            <ul class="mega-menu mega-menu--multiLevel">
7                <li>
8                    <a href="javascript:void(0);" class="menu-link mega-menu-link" aria-haspopup="true">1.1 Flyout link</a>
9                    <ul class="menu menu-list">
10                        <li>
11                            <a href="/page" class="menu-link menu-list-link">1.1.1 Page link</a>
12                        </li>
13                        <li>
14                            <a href="javascript:void(0);" class="menu-link menu-list-link" aria-haspopup="true">1.1.2 Flyout link</a>
15                            <ul class="menu menu-list">
16                                <li>
17                                    <a href="/page" class="menu-link menu-list-link">1.1.2.1 Page link</a>
18                                </li>
19                                <li>
20                                    <a href="/page" class="menu-link menu-list-link">1.1.2.2 Page link</a>
21                                </li>
22                            </ul>
23                        </li>
24                        <li>
25                            <a href="/page" class="menu-link menu-list-link">1.1.3 Page link</a>
26                        </li>
27                    </ul>
28                </li>
29                <li>
30                    <a href="javascript:void(0);" class="menu-link mega-menu-link" aria-haspopup="true">1.2 Flyout link</a>
31                    <ul class="menu menu-list">
32                        <li>
33                            <a href="/page" class="menu-link menu-list-link">1.2.1 Page link</a>
34                        </li>
35                        <li>
36                            <a href="/page" class="menu-link menu-list-link">1.2.2 Page link</a>
37                        </li>
38                    </ul>
39                </li>
40                <li>
41                    <a href="javascript:void(0);" class="menu-link mega-menu-link" aria-haspopup="true">1.3 Flyout link</a>
42                    <ul class="menu menu-list">
43                        <li>
44                            <a href="/page" class="menu-link menu-list-link">1.3.1 Page link</a>
45                        </li>
46                        <li>
47                            <a href="/page" class="menu-link menu-list-link">1.3.2 Page link</a>
48                        </li>
49                    </ul>
50                </li>
51                <li>
52                    <a href="/page" class="menu-link mega-menu-link">1.4 Page link</a>
53                </li>
54                <li class="mobile-menu-back-item">
55                    <a href="javascript:void(0);" class="menu-link mobile-menu-back-link">Back</a>
56                </li>
57            </ul>
58        </li>
59
60        <li>
61            <a href="javascript:void(0);" class="menu-link menu-bar-link" aria-haspopup="true">2. Flat mega menu (3 cols)</a>
62            <ul class="mega-menu mega-menu--flat">
63                <li>
64                    <a href="/page" class="menu-link mega-menu-link mega-menu-header">2.1 Page link header</a>
65                    <ul class="menu menu-list">
66                        <li>
67                            <a href="/page" class="menu-link menu-list-link">2.1.1 Page link</a>
68                        </li>
69                        <li>
70                            <a href="/page" class="menu-link menu-list-link">2.1.2 Page link</a>
71                        </li>
72                        <li>
73                            <a href="/page" class="menu-link menu-list-link">2.1.3 Page link</a>
74                        </li>
75                    </ul>
76                </li>
77                <li>
78                    <a href="/page" class="menu-link mega-menu-link mega-menu-header">2.2 Page link header</a>
79                    <ul class="menu menu-list">
80                        <li>
81                            <a href="/page" class="menu-link menu-list-link">2.2.1 Page link</a>
82                        </li>
83                        <li>
84                            <a href="/page" class="menu-link menu-list-link">2.2.2 Page link</a>
85                        </li>
86                        <li>
87                            <a href="/page" class="menu-link menu-list-link">2.2.3 Page link</a>
88                        </li>
89                    </ul>
90                </li>
91                <li>
92                    <a href="/page" class="menu-link mega-menu-link mega-menu-header">2.3 Page link header</a>
93                    <ul class="menu menu-list">
94                        <li>
95                            <a href="/page" class="menu-link menu-list-link">2.2.1 Page link</a>
96                        </li>
97                        <li>
98                            <a href="/page" class="menu-link menu-list-link">2.2.2 Page link</a>
99                        </li>
100                    </ul>
101                </li>
102                <li class="mobile-menu-back-item">
103                    <a href="javascript:void(0);" class="menu-link mobile-menu-back-link">Back</a>
104                </li>
105            </ul>
106        </li>
107
108        <li>
109            <a href="javascript:void(0);" class="menu-link menu-bar-link" aria-haspopup="true">3. Flat mega menu (2 cols)</a>
110            <ul class="mega-menu mega-menu--flat">
111                <li>
112                    <a href="#" class="menu-link mega-menu-link mega-menu-header">3.1 Page link header</a>
113                    <ul class="menu menu-list">
114                        <li>
115                            <a href="/page" class="menu-link menu-list-link">
116                                3.1.1 Page link<br />
117                                <small>Short decription of link</small>
118                            </a>
119                        </li>
120                        <li>
121                            <a href="/page" class="menu-link menu-list-link">
122                                3.1.2 Page link<br />
123                                <small>Short decription of link</small>
124                            </a>
125                        </li>
126                        <li>
127                            <a href="/page" class="menu-link menu-list-link">
128                                3.1.2 Page link<br />
129                                <small>Short decription of link</small>
130                            </a>
131                        </li>
132                    </ul>
133                </li>
134                <li class="mega-menu-content">
135                    <p class="mega-menu-header">3.2 Page link header</p>
136                    <p>This is just static content. You can add anything here. Images, text, buttons, your grandma's secrect recipe.</p>
137                </li>
138                <li class="mobile-menu-back-item">
139                    <a href="javascript:void(0);" class="menu-link mobile-menu-back-link">Back</a>
140                </li>
141            </ul>
142        </li>
143
144        <li>
145            <a href="/page" class="menu-link menu-bar-link">Static link</a>
146        </li>
147
148        <li class="mobile-menu-header">
149            <a href="/home" class="">
150                <span>Home</span>
151            </a>
152        </li>
153    </ul>
154</nav>

اینک که هیچ نوع استایل‌بندی روی این لیست اعمال نکرده‌ایم، ظاهر آن به صورت زیر است:

مگامنوی چند سطحی واکنش‌گرا

  • هدر لینک Home موبایل و لینک‌های Back موبایل باید در لیست‌های متناظر خود در بخش انتهایی ظاهر شوند. در ادامه با استفاده از flex-order آن‌ها را به صورت بصری در ابتدای لیست قرار می‌دهیم.
  • لینکی که دارای فلش باز کردن مگامنو یا باز شدن شناور است باید خصوصیت aria-haspopup آن به صورت true تعیین شده باشد. همچنین باید از رفتار پیش‌فرض مرورگر جلوگیری کنیم. این کار از نظر فنی با استفاده از جاوا اسکریپت انجام می‌یابد، اما می‌توان از <button> به جای یک <a> استفاده کرد.
  • مقدار زیادی از CSS به برخی معناشناسی‌های خاص مرتبط هستند، از این رو مهم است که از Markup صحیحی استفاده کنیم.

پیش از آغاز کار فهرست الزامات منوی مورد نظر را با هم مرور می‌کنیم:

  • مگامنوی چند سطحی با منوهای شناور
  • مگامنوهای مسطح با لی‌آوت ستونی منعطف
  • توانایی افزودن محتوای استاتیک
  • واکنش‌گرایی کامل که روی موبایل به صورت منوی لغزشی ثابت ظاهر می‌شود.

ایجاد منوی دسکتاپ

در این بخش ابتدا با مراحل ساخت منو برای نمایشگرهای دسکتاپ آشنا می‌شویم.

استایل‌بندی و لی‌آوت مقدماتی

ابتدا باید برخی استایل‌بندی و لی‌آوت‌ها را به بخش‌های اصلی منو از قبیل نوار منو، لینک‌های منو و فلش باز کردن مگامنو اضافه کنیم:

1@color-accent: #247BA0;
2@color-light: #ffffff;
3@menu-link-padding: 15px 20px;
4
5// General styling
6nav {
7   ul, li {
8     list-style: none;
9     padding: 0;
10     margin: 0;
11  } 
12  a {
13    display: block;
14    text-decoration: none;
15    &:hover, &:visited {
16      text-decoration: none;
17    }
18  }
19}
20
21// Shared styles for all links
22.menu-link {
23  padding: @menu-link-padding;
24  background: @color-light;
25  color: @color-accent;
26  transition: background .2s, color .2s;
27  &:hover {
28    background: @color-accent;
29    color: @color-light;
30    .mega-menu & {
31      background: tint(@color-accent, 85%);
32      color: darken(@color-accent, 5%);
33    }
34  }
35  &[aria-haspopup="true"] {
36    ~ ul {
37      display: none;
38    }
39  }
40}
41
42// Basic styling for the menu bar
43.menu-bar {
44  position: relative;
45  display: flex;
46  background: @color-light;
47}
48
49// Basic styling for the mega menu dropdown
50.mega-menu {
51  position: absolute;
52  top: 100%;
53  left: 0;
54  width: 100%;
55  background: @color-light;
56  overflow: hidden;
57}
58
59// Hide mobile specific elements
60.mobile-menu-trigger, .mobile-menu-trigger, .mobile-menu-back-item {
61  display: none;
62}

به موارد زیر توجه کنید.

  • نوار منو یک کانتینر flexbox است و دارای موقعیت relative است.
  • فلش باز کردن مگامنو یک کانتینر flexbox است و دارای موقعیت‌یابی absolute است و از این رو زیر نوار منو نمایش خواهد یافت.
  • لینک Dropdown/flyouts باید دارای خصوصیت aria-haspopup=true باشد و لیست‌های هم‌نیای آن پنهان خواهند شد.

کارکرد باز کردن و شناوری

برای ایجاد این کارکرد به طور معمول از جاوا اسکریپت استفاده می‌شود، ا‌ما ما از ترکیبی از ظرفیت‌های CSS استفاده می‌کنیم.

فلش باز کردن مگامنو

نخستین کارکردی که به منوی خود اضافه می‌کنیم، ‌قابلیت باز کردن منوی باز شونده مگامنو است. برای این که مطمئن شویم این کارکرد مستحکم است، باید آن را با استفاده از ترکیبی از رویکردها اجرا کنیم:

  • روی focus: مربوط به لینک
  • روی focus-within: مربوط به آیتم لیست
  • روی hover: مربوط به خود مگامنو. به این ترتیب مطمئن می‌شویم که در صورتی که لینک focus را از دست بدهد همچنان بازمی‌ماند و همچنین در حالتی که مرورگر از focus-within پشتیبانی نکند باز خواهد ماند.
1.menu-bar {
2  > li {
3    > [aria-haspopup="true"] {
4      &:focus {
5        ~ ul {
6          display: block;
7        }
8      }
9    }
10    &:focus-within {
11      > [aria-haspopup="true"] {
12        ~ ul {
13          display: block;
14        }
15      }
16    }
17  }
18  .mega-menu {
19    &:hover {
20      display: block;
21    }
22  }
23}

متأسفانه این کد در حال حاضر چندان جالبی نیست، ‌بنابراین کمی انیمیشن به آن اضافه می‌کنیم. می‌دانیم که شبیه‌سازی انیمیشن لغزش به پایین (slide-down) بسیار دشوار است، مگر این که ارتفاع ثابتی داشته باشید، ‌بنابراین به جای آن از مشخصه scale مربوط به transform استفاده می‌کنیم. با تنظیم transform-origin روی بخش فوقانی و انیمیت صرفاً روی محور Y می‌توانیم یک انیمیشن لغزش به پایین را شبیه‌سازی کنیم.

نکته خوب این است که استفاده از transform و به خصوص scale برای انیمیشن ازنظر مرورگر هزینه پایینی دارد که به معنی داشتن عملکرد خوب است.

1.menu-bar {
2  > li {
3    > [aria-haspopup="true"] {
4      &:focus {
5        ~ ul {
6          display: block;
7          transform-origin: top;
8          animation: dropdown .2s ease-out;
9        }
10      }
11    }
12    ...
13}
14
15@keyframes dropdown {
16  0% {
17    opacity: 0;
18    transform: scaleY(0);
19  }
20  40% {
21    opacity: 1;
22  }
23  100% {
24    transform: scaleY(1);
25  }
26}

این انیمیشن تنها به حالت ‎:focus روی لینک اضافه می‌شود، ‌چون بقیه بخش‌ها صرفاً برای باز نگه داشتن مگامنو استفاده می‌شوند.

در نهایت باید حالت‌های active را به لینک‌های نوار منوی خود اضافه کنیم. برای این که مطمئن شویم حالت‌های فعال زمانی که منو باز است، ‌به صورت چسبان می‌مانند، ‌از ترکیبی از رویکردها به صورت زیر استفاده کرده‌ایم:

  • روی hover: و focus-within: روی آیتم لیست
  • روی focus: روی لینک

با این که از حالت hover روی آیتم لیست استفاده می‌کنیم، اما استایل‌بندی همچنان روی خود لینک اعمال خواهد شد.

1.menu-bar {
2  > li {
3    &:hover {
4      > [aria-haspopup="true"] {
5        background: @color-accent;
6        color: @color-light;
7      }
8    }
9    > [aria-haspopup="true"] {
10      &:focus {
11        background: @color-accent;
12        color: @color-light;
13      }
14    }
15    &:focus-within {
16      > [aria-haspopup="true"] {
17        background: @color-accent;
18        color: @color-light;
19      }
20    }
21  }
22}

اکنون منوی ما باید چیزی مانند زیر باشد:

مگامنوی چند سطحی واکنش‌گرا

Flyout

گام بعدی مربوط به ساختن کارکرد Flyouts در مگامنوی چند سطحی است. در گام پیشین همه هم‌نیاهای لینک‌ها را با Flyout پنهان کردیم.

ابتدا باید لی‌آوت مورد نیاز را که شامل 3 ستون است اضافه کنیم. مقیاس‌بندی این مورد ساده است و امکان داشتن تعداد متفاوتی از ستون‌ها را فراهم می‌سازد، ‌اما به منظور حفظ سادگی مثال از لی‌آوت سه ستونی استفاده می‌کنیم.

1@mega-menu-multiLevel-colWidth: 100/3 + 0%;
2
3.mega-menu--multiLevel {
4  flex-direction: column;
5  [aria-haspopup="true"] {
6    ~ ul {
7       position: absolute;
8       top: 0;
9       height: 100%;
10    }
11  }
12  > li {
13    width: @mega-menu-multiLevel-colWidth;
14    > [aria-haspopup="true"] {
15      ~ ul {
16        left: @mega-menu-multiLevel-colWidth;
17        width: @mega-menu-multiLevel-colWidth;
18        ul {
19          width: 100%;
20          left: 100%;
21        }
22      }
23    }
24  }
25}

برای ایجاد این کارکرد جهت باز کردن flyout-های تودرتو از یک رویکرد مشابه به باز شدن مگامنو کمک می‌گیریم. Flyout-ها در زمان hover باز می‌شوند، ‌اما ترجیح بر این است که این کار با focus انجام شود.

1.mega-menu--multiLevel {
2  li {
3    &:hover {
4      [aria-haspopup="true"] {
5        ~ ul {
6          display: flex;
7        }
8      }
9    }
10  }
11  [aria-haspopup="true"] {
12    ~ ul {
13      &:hover {
14        display: flex;
15      }
16    }
17  }
18  li {
19    &:focus-within {
20      > [aria-haspopup="true"] {
21        ~ ul {
22          display: flex;
23        }
24      }
25    }
26  }
27}

این کد نیز کمی ناخوشایند به نظر می‌رسد، بنابراین مقداری انیمیشن به آن اضافه می‌کنیم. همان مشکل قبلی این بار نیز وجود دارد چون نمی‌توانیم عرض را بدون هیچ مقدار ثابت انیمیت کنیم. بنابراین در این جا نیز از transform scale استفاده می‌کنیم. از آنجا که flyout-ها به صورت جانبی به سمت بیرون می‌لغزند، ‌ما مبدأ را به سمت چپ جابجا و محور X را انیمیت می‌کنیم.

توجه کنید که این انیمیت روی ‎:hover مربوط به list-item اعمال می‌شود و بدین ترتیب مطمئن می‌شویم که انیمیشن در زمانی که کرسر از flyout باز شده به لینک بازمی‌گردد، ‌ری‌استارت نخواهد شد.

1.mega-menu--multiLevel {
2  li {
3    &:hover {
4      > [aria-haspopup="true"] {
5        ~ ul {
6          display: flex;
7          transform-origin: left;
8          animation: flyout .2s ease-out;
9        }
10      }
11    }
12  }
13  ...
14}
15
16@keyframes flyout {
17  0% {
18    opacity: 0;
19    transform: scaleX(0);
20  }
21  100% {
22    opacity: 1;
23    transform: scaleX(1);
24  }
25}

پیش از آن که ظاهر آن را بررسی کنیم، برخی حالت‌های active دیگر به آن اضافه می‌کنیم.

1.mega-menu--multiLevel {
2  li {
3    &:hover, &:focus-within {
4      > [aria-haspopup="true"] {
5        background: tint(@color-accent, 85%);
6        color: darken(@color-accent, 5%);
7      }
8    }
9  }
10}

اینک به صورت زیر در آمده است:

مگامنوی چند سطحی واکنش‌گرا

استایل‌بندی مگامنوی مسطح

از آنجا که مگامنوی ما هم اینک یک flexbox است، تنها چیزی که نیاز داریم این است که مطمئن شویم همه فرزندان مگامنوی مسطح ما فضای یکسانی اشغال می‌کنند. همچنین مقداری استایل‌بندی به لینک‌های هدر اضافه می‌کنیم:

1.mega-menu--flat {
2  > * {
3    flex: 1;
4  }
5}

نتیجه دسکتاپ

بدین ترتیب کار پیاده‌سازی کارکردهای مگامنوی دسکتاپ ما به پایان می‌رسد. در این بخش چند استایل دیگر به آن اضافه می‌کنیم:

  • فلش‌های رو به پایین برای لینک‌های بازشدنی نوار منو
  • فلش‌های سمت راست برای لینک‌های flyout مگا منو
  • حاشیه‌هایی بین ستون‌های مگامنو
1.menu-link {
2  &[aria-haspopup="true"] {
3    &:after {
4      content: "\203A";
5      display: inline-block;
6      font-size: 1.5em;
7      float: right;
8      line-height: 1;
9      margin-top: -3px; // Font offset
10      .menu-bar > li > & {
11        margin-top: 0;
12        margin-left: 30px;
13        transform-origin: center;
14        transform: rotate(90deg);
15      }
16    }
17  }
18}
19
20.mega-menu--multiLevel {
21  [aria-haspopup="true"] {
22    ~ ul {
23      border-left: 1px solid #f0f0f0;
24    }
25  }
26}

به این ترتیب منوی ما اینک ظاهر زیبا و جذابی یافته است:

مگامنوی چند سطحی واکنش‌گرا

ایجاد منوی موبایل

پیش از آغاز ایجاد منوی موبایل، ‌باید کمی پاکسازی انجام دهیم. همه کدهای بخش قبلی را انتخاب کرده و آن را منسجم‌تر می‌سازیم. استایل‌های خاص دسکتاپ را به درون یک کوئری مدیا می‌بریم تا مجبور به بازنویسی این استایل‌ها برای نسخه موبایل نباشیم.

نقطه آغاز گام بعدی به صورت زیر است:

1@color-accent: #177E89;
2@color-light: #ffffff;
3@menu-link-padding: 20px 25px;
4@breakpoint: 950px;
5@mega-menu-multiLevel-colWidth: 100/3 + 0%;
6
7// ------------------ SHARED STYLES
8
9nav {
10   ul, li {
11     list-style: none;
12     padding: 0;
13     margin: 0;
14  } 
15  a {
16    display: block;
17    text-decoration: none;
18    &:hover, &:visited {
19      text-decoration: none;
20    }
21    &[aria-haspopup="true"] {
22      ~ ul {
23        display: none;
24      }
25    }
26  }
27}
28
29.menu-bar {
30  background: @color-light;
31}
32
33.menu-link {
34  padding: @menu-link-padding;
35  background: @color-light;
36  color: @color-accent;
37  transition: background .2s, color .2s;
38  &[aria-haspopup="true"] {
39    &:after {
40      content: "\203A";
41      display: inline-block;
42      font-size: 1.5em;
43      float: right;
44      line-height: 1;
45      margin-top: -3px; // Font offset
46    }
47  }
48}
49
50.mega-menu-header {
51  font-size: 1.2em;
52  text-transform: uppercase;
53  font-weight: bold;
54  color: darken(@color-accent, 5%);
55}
56
57.mega-menu {
58  background: @color-light;
59}
60
61.mega-menu--multiLevel {
62  flex-direction: column;
63}
64
65
66// ------------------ MEDIA QUERIES
67
68@media all and (min-width: @breakpoint + 1px) {
69  // Desktop only
70  
71  .menu-bar {
72    position: relative;
73    display: flex;
74    > li {
75      > [aria-haspopup="true"] {
76        // STYLING: Down arrow on desktop
77        &:after {
78          margin-top: 0;
79          margin-left: 30px;
80          transform-origin: center;
81          transform: rotate(90deg);
82        }
83        
84        // FUNCTIONALITY: Open mega menu
85        &:focus {
86          ~ ul {
87            display: flex;
88            transform-origin: top;
89            animation: dropdown .2s ease-out;
90          }
91        }
92      }
93      
94      // FUNCTIONALITY: Keep mega menu open
95      &:focus-within {
96        > [aria-haspopup="true"] {
97          ~ ul {
98            display: flex;
99          }
100        }
101      }
102      
103      // STYLING: Active state
104      > [aria-haspopup="true"]:focus,
105      &:focus-within > [aria-haspopup="true"],
106      &:hover > [aria-haspopup="true"] {
107        background: @color-accent;
108        color: @color-light;
109      }
110    }
111  }
112  
113  .mega-menu {
114    // LAYOUT: Mega menu
115    position: absolute;
116    top: 100%;
117    left: 0;
118    width: 100%;
119    // FUNCTIONALITY: Keep mega menu open
120    &:hover {
121      display: flex;
122    }
123  }
124 
125  
126  .mega-menu--multiLevel {
127    // LAYOUT: Multi level columns
128    > li {
129      width: @mega-menu-multiLevel-colWidth;
130      > [aria-haspopup="true"] {
131        ~ ul {
132          left: @mega-menu-multiLevel-colWidth;
133          width: @mega-menu-multiLevel-colWidth;
134          ul {
135            width: 100%;
136            left: 100%;
137          }
138        }
139      }
140    }
141    
142    li {
143      // FUNCTIONALITY: Opening flyouts
144      &:hover {
145        > [aria-haspopup="true"] {
146          ~ ul {
147            display: block;
148            transform-origin: left;
149            animation: flyout .2s ease-out;
150          }
151        }
152      }
153      
154      // FUNCTIONALITY: Keeping flyouts open
155      &:focus-within {
156        > [aria-haspopup="true"] {
157          ~ ul {
158            display: block;
159          }
160        }
161      }
162      
163      // STYLING: Flyout link active states
164      &:hover, &:focus-within {
165        > [aria-haspopup="true"] {
166          background: tint(@color-accent, 85%);
167          color: darken(@color-accent, 5%);
168        }
169      }
170    }
171    [aria-haspopup="true"] {
172      ~ ul, & {
173        border-left: 1px solid #f0f0f0;
174        // FUNCTIONALITY: Keeping flyouts open
175        &:hover {
176          display: block;
177        }
178      }
179      // LAYOUT: Flyouts
180      ~ ul {
181        position: absolute;
182        top: 0;
183        height: 100%;
184      }
185    }
186  }
187  
188  // STYLING: Flat mega menu columns
189  .mega-menu--flat {
190    > * {
191      flex: 1;
192    }
193  }
194  
195  // Hide mobile specific elements
196  .mobile-menu-trigger, .mobile-menu-trigger, .mobile-menu-back-item {
197    display: none;
198  }
199
200}
201
202@media all and (max-width: @breakpoint) {
203  // Mobile only
204
205  
206}
207
208
209// ------------------ ANIMATIONS
210
211@keyframes dropdown {
212  0% {
213    opacity: 0;
214    transform: scaleY(0);
215  }
216  100% {
217    opacity: 1;
218    transform: scaleY(1);
219  }
220}
221
222@keyframes flyout {
223  0% {
224    opacity: 0;
225    transform: scaleX(0);
226  }
227  100% {
228    opacity: 1;
229    transform: scaleX(1);
230  }
231}

استایل‌بندی و لی‌آوت‌های مقدماتی

کار خود را با راه‌اندازی منوی موبایل از طریق افزودن برخی استایل‌ها و لی‌آوت‌های مقدماتی به عناصر مختلف آغاز می‌کنیم. در بخش موبایل، منوی ما به صورت یک منوی با جهت‌گیری سمت چپ ثابت است که وقتی روی دکمه‌ای می‌زنیم به سمت بیرون می‌لغزد. منوهای این مگامنوی چند سطحی به صورت آکاردئون نمایش می‌یابند.

1@menu-mobile-width: 350px;
2
3.mobile-menu-trigger, .mobile-menu-header, .mobile-menu-back-item {
4  display: block;
5}
6
7.mobile-menu-trigger {
8  background: @color-accent;
9  color: @color-light;
10  border: 0;
11  padding: 10px;
12  font-size: 1.2em;
13  border-radius: 4px;
14}
15
16.mobile-menu-header {
17  order: -1;
18  background: grey;
19  a {
20    padding: @menu-link-padding;
21    color: @color-light;
22  }
23}
24
25.menu-bar {
26  flex-direction: column;
27  position: fixed;
28  top: 0;
29  left: -100%;
30  height: 100vh;
31  width: @menu-mobile-width;
32  max-width: @menu-mobile-width;
33  max-width: 90%;
34  overflow-x: hidden;
35  transition: left .3s;
36  box-shadow: 1px 0px 2px 0px rgba(0,0,0,0.25);
37  > li {
38    > [aria-haspopup="true"] {
39      ~ ul {
40        display: flex;
41        flex-direction: column;
42        background: @color-light;
43        position: absolute;
44        left: 100%;
45        top: 0;
46        height: 100vh;
47        width: 100%;
48        transition: left .3s;
49        [aria-haspopup="true"] {
50          .mega-menu-header;
51          color: @color-dark;
52          &:after {
53            display: none;
54          }
55        }
56      }
57    }
58  }
59}

تنها چیزی که اینک روی صفحه دیده می‌شود، دکمه باز کردن منوی موبایل است. خود منو خارج از صفحه قرار دارد و مگامنو خارج از منوی موبایل قرار گرفته است. ما در واقع موقعیت چپ منو را انیمیت می‌کنیم و به این منظور از transform استفاده نکرده‌ایم. به نظر می‌رسد که در iOS اگر از transform برای آفست کردن منو استفاده کنیم، کلیک کردن روی دکمه باز کردن منوی موبایل موجب ثبت یک کلیک روی لینک Home درون منو نیز می‌شود.

نکته‌ای در خصوص focus: روی موبایل

دستگاه‌های موبایل عملاً از حالت‌های ‎:focus پشتیبانی نمی‌کنند، اما می‌توان از hover: برای شبیه‌سازی همان کارکرد استفاده کرد. این بدان معنی است که نمی‌توانیم از کد مربوط به منوی دسکتاپ روی منوی موبایل استفاده کنیم و منوی موبایل صرفاً روی دستگاه‌های لمسی استفاده خواهد شد. حالت اول یعنی استفاده از منوی دسکتاپ روی دستگاه‌های موبایل احتمالاً یک مورد استثنایی است، ‌اما برای این که همه حالت‌ها را پوشش دهیم این مورد را نیز پشتیبانی خواهیم کرد.

برای این که صرفاً دستگاه‌های لمسی را هدف‌گیری کنیم، می‌توانیم از خصوصیت مدیای hover استفاده کنیم که بررسی می‌کند آیا سازوکار ورودی اصلی کاربر می‌تواند روی عناصر hover کند یا نه.

باز کردن منوی موبایل

به این منظور منو باید هم‌نیای دکمه منوی موبایل باشد. همان‌طور که احتمالاً حدس می‌زنید، ما از hover: برای تحریک تغییر موقعیت منو در زمان ضربه زدن روی دکمه استفاده می‌کنیم، ‌اما برای این که از دستگاه‌های غیر لمسی نیز پشتیبانی کنیم از focus: هم استفاده خواهیم کرد.

برای جلوگیری از این که منو به محض آغاز تعامل با آن بسته شود، ‌باید از hover: و focus-within: روی خود منو استفاده کنیم تا همچنان باز بماند.

1// ------------------------ ALL DEVICES
2.mobile-menu-trigger {
3  &:focus {
4    ~ ul {
5      left: 0;
6    }
7  }
8}
9
10.menu-bar {
11  &:hover, &:focus-within {
12    left: 0;
13  }
14}
15
16// ------------------------ TOUCH DEVICES
17@media (hover: none) {
18  .mobile-menu-trigger {
19    &:hover {
20      ~ ul {
21        left: 0;
22      }
23    }
24  }
25}

اکنون کارکرد مقدماتی برای باز کردن منوی موبایل را داریم.

مگامنوی چند سطحی واکنش‌گرا

باز کردن مگامنوها

از همین رویکرد برای باز کردن منوهای مگا نیز استفاده می‌کنیم.

1// ------------------------ ALL DEVICES
2  .menu-bar {
3    > li {
4      > [aria-haspopup="true"] {
5        &:focus {
6          ~ ul {
7            left: 0;
8          }
9        }
10      }
11      ~ ul {
12        &:hover, &:focus-within {
13          left: 0;
14        }
15      }
16    }
17  }
18
19// ------------------------ TOUCH DEVICES
20@media (hover: none) {
21  .menu-bar {
22    > li {
23      > [aria-haspopup="true"] {
24        &:hover {
25          ~ ul {
26            left: 0;
27          }
28        }
29        ~ ul {
30          &:hover {
31            left: 0;
32          }
33        }
34      }
35    }
36  }
37}

دکمه‌های بازگشت

این کار یکی از دشوارترین کارها برای اجرا بدون جاوا اسکریپت محسوب می‌شود. به خاطر داشته باشید که تنها روش باز کردن منوهای ما استفاده از ‎:focus یا ‎:hover روی یک هم‌نیا یا یک والد منو است. دکمه‌های بازگشت بخشی از خود منو هستند و به جهت طرز کار CSS تنها می‌توانیم والد دکمه بازگشت را برای تغییر موقعیت منو هدف‌گیری کنیم.

بنابراین چطور می‌توانیم مطمئن باشیم که منوی شامل دکمه بازگشت که کلیک شده است، ‌focus خود را از دست می‌دهد؟

ابتدا باید دکمه بازگشت را از خود لیست حذف کنیم. به این منظور باید یک ارتفاع ثابت روی دکمه تعیین کنیم و سپس از همان مقدار آفست منفی به صورت عمودی استفاده کنیم و مگامنو را به مقدار مثبت آفست کنیم. همچنین مقداری استایل‌بندی مقدماتی به دکمه بازگشت اضافه می‌کنیم.

1@mobile-menu-back-height:           ~"calc(1.4em + 40px)";
2@mobile-menu-back-offset:           ~"calc(0px - (1.4em + 40px))";
3
4.mobile-menu-back-item {
5  order: -1;
6  a {
7    background: tint(grey, 70%);
8    color: @color-dark;
9    max-height: @mobile-menu-back-height;
10    margin-top: @mobile-menu-back-offset;
11    pointer-events: none;
12    &:before {
13      content: "\2039";
14      margin-right: 10px;
15    }
16  }
17}
18
19.menu-bar {
20  > li {
21    > [aria-haspopup="true"] {
22      ~ ul {
23        margin-top: @mobile-menu-back-height;
24      }
25    }
26  }
27}

اکنون ظاهر آن همانند قبل است، ‌اما دکمه بازگشت در عمل خارج از خود لیست قرارگرفته است. توجه کنید که کد زیر از همه کلیک‌ها و حالت‌های روی یک عنصر جلوگیری می‌کند:

pointer-events: none;

این بدان معنی است که به طور نظری زمانی که روی دکمه بازگشت کلیک کنیم، ‌در عمل روی آنچه زیر آن است کلیک کرده‌ایم که در این مورد منوی قبلی است. همچنین به آن معنی است که وقتی کلیک می‌کنیم، مگامنوی باز شده فوکوس خود را از دست می‌دهد.

این امر موجب می‌شود که مشکل دیگری پدید آید. از آنجا که ما روی آنچه زیر دکمه قرار دارد کلیک می‌کنیم در عمل روی لینک Home در سطح نخست منو کلیک کرده‌ایم که موجب آغاز رفتار پیش‌فرض آن لینک می‌شود و ما را به یک صفحه می‌برد.

برای جلوگیری از این رفتار، ‌باید نمایانی لینک Home را در زمانی که مگامنو باز است، ‌ پنهان کنیم در زمانی که روی دکمه بازگشت کلیک می‌کنیم، ‌لینک پنهان است، اما به محض این که منو باز می‌شود، لینک دوباره نمایان می‌شود.

1.mobile-menu-header {
2  a {
3    visibility: visible;
4  }
5}
6
7// ------------------------ ALL DEVICES
8.menu-bar {
9  > li {
10    &:focus-within ~ .mobile-menu-header a {
11      visibility: hidden;
12    }
13  }
14}
15
16// ------------------------ TOUCH DEVICES
17@media (hover: none) {
18  .menu-bar {
19    > li {
20      &:hover ~ .mobile-menu-header {
21        a {
22          visibility: hidden;
23        }
24      }
25    }
26  }
27}

باز کردن Flyout-ها

در این بخش کارکرد باز کردن Flyout-های بعدی را ایجاد می‌کنیم. ابتدا مقداری استایل‌بندی اضافه می‌کنیم:

1  .menu-bar {
2    > li {
3      > [aria-haspopup="true"] {
4        ~ ul {
5          [aria-haspopup="true"] {
6            color: @color-dark;
7            &:after {
8              content: "+";
9              background: none;
10              font-size: 1em;
11              font-weight: normal;
12              height: 20px;
13              line-height: 1;
14            }
15            ~ ul {
16              max-height: 0px;
17              transform-origin: top;
18              transform: scaleY(0);
19              transition: max-height .1s steps(1);
20            }
21          }
22        }
23      }
24    }
25  }

با استفاده از ترکیبی از گذارها و انیمیشن‌ها، یک کارکرد آکاردئون-مانند تودرتو اضافه خواهیم کرد. در ادامه این کارکرد را پیش از ادامه اضافه می‌کنیم:

1// ------------------------ ALL DEVICES
2.menu-bar {
3  > li {
4    > [aria-haspopup="true"] {
5      ~ ul {
6        [aria-haspopup="true"] {
7          &:focus {
8            ~ ul {
9              max-height: 500px;
10              animation: dropdown .3s forwards;
11            }
12          }
13        }
14        li {
15          &:focus-within {
16            > [aria-haspopup="true"] {
17              ~ ul {
18                max-height: 500px;
19                transform: scaleY(1);
20              }
21            }
22          }
23        }
24      }
25    }
26  }
27}
28
29
30// ------------------------ TOUCH DEVICES
31
32@media (hover: none) {
33  .menu-bar {
34    > li {
35      > [aria-haspopup="true"] {
36        ~ ul {
37          [aria-haspopup="true"] {
38            &:hover {
39              ~ ul {
40                max-height: 500px;
41                animation: dropdown .3s forwards;
42              }
43            }
44            ~ ul {
45              &:hover {
46                max-height: 500px;
47                transform: scaleY(1);
48              }
49            }
50          }
51        }
52      }
53    }
54  }
55}

از انیمیشن dropdown که در مورد مگامنوی دسکتاپ استفاده کردیم در این جا نیز استفاده مجدد می‌کنیم. مشکلی که قبلاً در زمان تلاش برای استفاده از transform برای باز کردن منوی موبایل داشتیم و با کلیک روی دکمه موجب آغاز کلیک روی لینک درون منو می‌شدیم،‌ در این جا نیز مطرح است. اگر صرفاً مجبور به استفاده از transform باشیم، برای نمونه ‌زمانی که روی باز شدن آیتم دوم کلیک کنیم، ‌در عمل موجب کلیک روی آیتم سوم می‌شود.

در این جا باید از max-height استفاده کنیم. این حالت شبیه نوعی هک است، ‌اما کار می‌کند. از آنجا که از یک max-height به عنوان گذار استفاده می‌کنیم، یک تأخیر اندک در حدود 0.1 ثانیه برای بستن آکاردئون باز قبلی ظاهر می‌شود. بدین ترتیب از کلیک کردن روی چند آیتم باز جلوگیری می‌کنیم.

مگامنوی چند سطحی واکنش‌گرا

نتیجه نهایی

بدین ترتیب کار ما به پایان رسیده است و موفق شده‌ایم یک مگامنوی واکنش‌گرای چند سطحی صرفاً با CSS بسازیم.

مگامنوی چند سطحی واکنش‌گرا

سخن پایانی

کد کامل این پروژه را می‌توانید در این صفحه (+) مشاهده کنید. CSS بسیار قدرتمند است و در اغلب موارد این قدرت آن دست کم گرفته می‌شود. ما به طور سنتی عادت کرده‌ایم که برای پیاده‌سازی کارکردها و تعامل‌پذیری از جاوا اسکریپت استفاده کنیم، ‌اما این پروژه نشان داد که اگر خلاقیت به خرج بدهیم، ‌ظرفیت‌های CSS می‌توانند کاملاً ما را شگفت‌زده کنند.

اما باید توجه داشته باشید که ساخت نسخه صرفاً CSS این مگامنو صرفاً یک آزمون است تا ببینیم بدون جاوا اسکریپت چه کارهایی می‌توانیم انجام دهیم. برای ایجاد کامپوننت آماده پروداکشن از این مگامنو باید مقداری جاوا اسکریپت اضافه کنیم تا مطمئن شویم که این کارکرد روی همه مرورگرها ارائه می‌شود و تجربه روان‌تری برای کاربر رقم بزنیم و برخی از مشکلات ناشی از رویکرد صرفاً CSS را حذف کنیم.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۵ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
swlh
۲ دیدگاه برای «ساخت مگامنوی چند سطحی واکنش‌گرا با CSS — از صفر تا صد»

سلام ،ممنون از اموزش کاربردیتون.
من میخواهم هر موقع زیر منویی را انتخاب میکنم ارتفاع ul قبلی به اندازه این زیر منو تغییر کند . به عنوان مثال من لینک 1.1 را هاور کردم و زیر منوهای ان که 10 تا هستتد نمایش داده شده میخواهم ارتفاع ul اولی که لینک 1.1 در ان است هم به اندازه همان 10 تا تغییر کند.
چطور این را میتوانم انجام دهم؟
با تشکر

خیلی ممنون از آموزش کامل شما

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *