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.
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.qwemain.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>}Related
CollapsibleNavSide navigation in groups that fold under their titles, the browser remembering which are open.6 views · 7 downloadsFooterNavColumns of grouped links for a site's footer, each under its title, wrapping on narrow screens.5 views · 7 downloadsMenuA vertical list of links and actions in titled sections, with icons, shortcuts, counts, disabled items and the active one marked.6 views · 6 downloadsAlphabetIndexA to Z links into a list sorted by name; letters with nothing under them stay in place, disabled.4 views · 6 downloadsBreadcrumbsWhere this page sits: a trail of links up to the home page, the last one marked as this page.5 views · 6 downloadsDirectoryNavA long list of names grouped under sticky letter headers, with a row of letters at the top that jumps to each group.5 views · 5 downloads
Thread
Thread 0 posts
On MegaMenu: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.