Navigation

MegaMenu

A top bar whose items open wide panels of grouped links under the whole bar, on hover and from the keyboard, with no script.

#navigation#menu#link#marketing#no-js

Demo

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/mega_menu.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/mega_menu.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/mega_menu.qwe
let mega_menu_demo_items = [    { label: "Products", columns: [        { title: "Build", links: [            { label: "Editor", href: "#editor", description: "Write, check and preview as you type.", icon: "M4 20h4L18.5 9.5a2.1 2.1 0 0 0-3-3L5 17z" },            { label: "Components", href: "#mega_menu", description: "A kit of views and components.", icon: "M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z" },            { label: "CLI", href: "#cli", description: "Run, check, test and deploy.", icon: "M4 17l6-5-6-5M12 19h8" }        ] },        { title: "Run", links: [            { label: "Hosting", href: "#hosting", description: "One command to a URL." },            { label: "Databases", href: "#db", description: "SQLite, Postgres, MySQL." },            { label: "Live data", href: "#live", description: "Pages that follow the server." }        ] }    ], featured: { title: "What's new in 0.5", text: "Styles that travel with each component, and a faster check.", href: "#new", cta: "Read the notes →" } },    { label: "Resources", columns: [        { title: "Learn", links: [{ label: "Docs", href: "#docs" }, { label: "Guides", href: "#guides" }, { label: "Examples", href: "#examples" }] },        { title: "Community", links: [{ label: "Forum", href: "#forum" }, { label: "Discord", href: "#discord" }] }    ] },    { label: "Pricing", href: "#pricing" },    { label: "Blog", href: "#blog" }]view MegaMenuDemo() =    <div.demo-stack>        <div style="position: relative; height: 340px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); overflow: hidden">            <MegaMenu id="mega-menu-demo" brand="Acme" items={mega_menu_demo_items} current="#mega_menu">                <Button size="sm" variant="ghost" href="#mega_menu">Sign in</Button>                <Button size="sm" href="#mega_menu">Start free</Button>            </MegaMenu>            <p.demo-note style="padding: 16px 20px">Hover Products or Resources, or Tab to them: the panel opens under the whole bar.</p>        </div>    </div>

Source

ui/mega_menu.qwe
// MegaMenu: the bar along the top of a large site, whose items open wide panels of// grouped links under the whole bar (Products: three columns of them, with a line// each; Resources; Company). A panel opens when its item is hovered, and when the// keyboard reaches it or anything in it (Tab from the item's button goes through its// links), so it needs no script; an item with an href alone is a plain link.////   <MegaMenu brand="Acme" current="/pricing" items={[//       { label: "Products", columns: [//           { title: "Build", links: [{ label: "Editor", href: "/editor", description: "Write and preview." }] },//           { title: "Run", links: [{ label: "Hosting", href: "/hosting" }] }//       ] },//       { label: "Pricing", href: "/pricing" }//   ]}>//       <Button size="sm" href="/login">Sign in</Button>//   </MegaMenu>//// items: { label, href?, columns?: [{ title?, links: [{ label, href, description?, icon? }] }],// featured?: { title, text, href, cta? } }. An item with columns opens a panel of them;// featured is a highlighted box at the panel's right. icon: a character or an emoji,// or an SVG path (24x24, stroked) when it starts with "M". current: the href of this// page, marked aria-current="page" on the item or link that goes there.// brand, brand_href ("/"): the site's name at the left. children: what goes at the// right end (buttons). label: what screen readers call it ("Main").import ui/theme.qwe use ui_clslet mega_menu_css = css`.ui-megamenu { position: relative; z-index: 10; border-bottom: 1px solid var(--ui-border); background: var(--ui-bg); color: var(--ui-fg); font: 15px/1.5 var(--ui-font); }.ui-megamenu-in { display: flex; align-items: center; gap: 20px; height: 60px; padding: 0 20px; }.ui-megamenu-brand { flex: none; color: var(--ui-fg); font-weight: 650; font-size: 16px; text-decoration: none; border-radius: var(--ui-radius-sm); }.ui-megamenu-brand:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-megamenu-bar { display: flex; gap: 2px; margin: 0; padding: 0; list-style: none; }.ui-megamenu-item { position: static; }.ui-megamenu-trigger {  display: inline-flex; align-items: center; gap: 5px; height: 38px; padding: 0 12px; border: 0; border-radius: var(--ui-radius-sm);  background: transparent; color: var(--ui-muted); font: 500 14px/1 var(--ui-font); text-decoration: none; white-space: nowrap; cursor: pointer;}.ui-megamenu-trigger:hover, .ui-megamenu-item:focus-within > .ui-megamenu-trigger, .ui-megamenu-item:hover > .ui-megamenu-trigger { color: var(--ui-fg); background: var(--ui-subtle); }.ui-megamenu-trigger:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-megamenu-trigger[aria-current="page"] { color: var(--ui-fg); box-shadow: inset 0 -2px 0 var(--ui-accent); border-radius: var(--ui-radius-sm) var(--ui-radius-sm) 0 0; }.ui-megamenu-chev { width: 14px; height: 14px; transition: transform .15s; }.ui-megamenu-item:hover .ui-megamenu-chev, .ui-megamenu-item:focus-within .ui-megamenu-chev { transform: rotate(180deg); }.ui-megamenu-panel {  position: absolute; left: 0; right: 0; top: 100%; display: none; padding: 24px 20px 28px; border-bottom: 1px solid var(--ui-border);  background: var(--ui-bg); box-shadow: var(--ui-shadow-lg);}.ui-megamenu-panel::before { content: ""; position: absolute; left: 0; right: 0; top: -12px; height: 12px; }.ui-megamenu-item:hover > .ui-megamenu-panel, .ui-megamenu-item:focus-within > .ui-megamenu-panel { display: block; animation: ui-megamenu-in .14s ease-out; }@keyframes ui-megamenu-in { from { opacity: 0; transform: translateY(-4px); } }.ui-megamenu-cols { display: grid; grid-template-columns: repeat(var(--ui-megamenu-cols, 3), minmax(0, 1fr)); gap: 28px; max-width: 1100px; margin: 0 auto; }.ui-megamenu-title { margin: 0 0 8px; color: var(--ui-muted); font-size: 12px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }.ui-megamenu-cols ul { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; }.ui-megamenu-link {  display: flex; align-items: flex-start; gap: 10px; margin: 0 -8px; padding: 7px 8px; border-radius: var(--ui-radius-sm);  color: var(--ui-fg); text-decoration: none; font-size: 14px;}.ui-megamenu-link:hover { background: var(--ui-subtle); }.ui-megamenu-link:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-megamenu-link[aria-current="page"] .ui-megamenu-name { color: var(--ui-accent); }.ui-megamenu-icon { display: inline-grid; place-items: center; flex: none; width: 22px; height: 22px; margin-top: 1px; font-size: 17px; line-height: 1; color: var(--ui-accent); }.ui-megamenu-icon svg { width: 18px; height: 18px; }.ui-megamenu-name { display: block; font-weight: 600; }.ui-megamenu-desc { display: block; color: var(--ui-muted); font-size: 13px; }.ui-megamenu-featured {  display: block; padding: 16px; border-radius: var(--ui-radius); background: var(--ui-accent-soft); color: var(--ui-fg);  text-decoration: none; font-size: 14px;}.ui-megamenu-featured:hover .ui-megamenu-cta { text-decoration: underline; }.ui-megamenu-featured:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-megamenu-featured .ui-megamenu-name { font-size: 15px; }.ui-megamenu-cta { display: inline-block; margin-top: 10px; color: var(--ui-accent); font-weight: 600; }.ui-megamenu-end { display: flex; align-items: center; gap: 8px; margin-left: auto; }@media (max-width: 760px) {  .ui-megamenu-cols { grid-template-columns: 1fr 1fr; }  .ui-megamenu-panel { max-height: 70vh; overflow: auto; }}@media (prefers-reduced-motion: reduce) { .ui-megamenu-chev { transition: none; } .ui-megamenu-item:hover > .ui-megamenu-panel, .ui-megamenu-item:focus-within > .ui-megamenu-panel { animation: none; } }`fn mega_menu_holds(it, current: str?) -> bool = current != null && (get(it, "href") == current    || any(get(it, "columns") ?? [], (c) => any(c.links, (l) => l.href == current))    || get(get(it, "featured"), "href") == current)view MegaMenu(items: list = [], current: str? = null, brand: str = "", brand_href: str = "/", label: str = "Main",              id: str? = null, class: str? = null, children) {    style global mega_menu_css    <nav class={ui_cls(["ui-megamenu", class])} id={id} aria-label={label}>        <div.ui-megamenu-in>            {brand != "" && <a.ui-megamenu-brand href={brand_href}>{brand}</a>}            <ul.ui-megamenu-bar>                {for [i, it] in enumerate(items) => {                    let cols = get(it, "columns") ?? []                    let pid = `${id ?? "ui-megamenu"}-p${i}`                    <li.ui-megamenu-item>                        {len(cols) == 0                            ? <a.ui-megamenu-trigger href={it.href} aria-current={current != null && it.href == current ? "page" : null}>{it.label}</a>                            : [<button.ui-megamenu-trigger type="button" aria-haspopup="true" aria-controls={pid}                                       aria-current={mega_menu_holds(it, current) ? "page" : null}>                                   {it.label}                                   <svg.ui-megamenu-chev viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"                                        stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>                               </button>,                               <div.ui-megamenu-panel id={pid} aria-label={it.label}>                                   <div.ui-megamenu-cols style={`--ui-megamenu-cols: ${len(cols) + (get(it, "featured") != null ? 1 : 0)}`}>                                       {for c in cols => <div.ui-megamenu-col>                                           {get(c, "title") != null && <p.ui-megamenu-title>{c.title}</p>}                                           <ul aria-label={get(c, "title")}>                                               {for l in c.links => <li><a.ui-megamenu-link href={l.href}                                                       aria-current={current != null && l.href == current ? "page" : null}>                                                   {get(l, "icon") != null && <span.ui-megamenu-icon aria-hidden="true">{starts_with(l.icon, "M") && len(l.icon) > 2                                                       ? <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"                                                              stroke-linejoin="round"><path d={l.icon}/></svg>                                                       : l.icon}</span>}                                                   <span>                                                       <span.ui-megamenu-name>{l.label}</span>                                                       {get(l, "description") != null && <span.ui-megamenu-desc>{l.description}</span>}                                                   </span>                                               </a></li>}                                           </ul>                                       </div>}                                       {get(it, "featured") != null && <a.ui-megamenu-featured href={it.featured.href}>                                           <span.ui-megamenu-name>{it.featured.title}</span>                                           <span.ui-megamenu-desc>{it.featured.text}</span>                                           <span.ui-megamenu-cta>{get(it.featured, "cta") ?? "Learn more →"}</span>                                       </a>}                                   </div>                               </div>]}                    </li>                }}            </ul>            {children != null && <div.ui-megamenu-end>{children}</div>}        </div>    </nav>}
Thread
57Useful? Like it, or add to its thread below.

Thread 0 posts

On MegaMenu: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.