HoverCard
A card of details on hover or focus, after a short wait, that stays under the pointer; Escape hides it.
Demo
Reviewed by Ada LovelaceEngineering · LondonWrote the parser and the type checker. 214 reviews this year.See her profile
Rest the pointer on a name, or Tab to it; Escape hides the card.
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/hover_card.qweimport ui/theme.qwe use theme_cssuse ui/hover_card.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
let hover_card_demo_css = css`.hc-demo-who { display: flex !important; align-items: center; gap: 10px; }.hc-demo-who img { width: 40px; height: 40px; border-radius: 50%; }.hc-demo-who b { display: block; }.hc-demo-muted { color: var(--ui-muted); font-size: 13px; }`view HoverCardDemo() = <div.demo-stack> <style>{hover_card_demo_css}</style> <p style="margin: 0"> Reviewed by{" "} <HoverCard id="hc-ada" trigger="@ada" href="#hover_card"> <span.hc-demo-who> <img src={`data:image/svg+xml,${url_encode("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'><rect width='40' height='40' fill='#5b4bdb'/><text x='20' y='26' font-family='sans-serif' font-size='16' fill='#fff' text-anchor='middle'>AL</text></svg>")}`} alt=""/> <span><b>Ada Lovelace</b><span.hc-demo-muted>Engineering · London</span></span> </span> <span>Wrote the parser and the type checker. 214 reviews this year.</span> <span><a href="#avatar">See her profile</a></span> </HoverCard> {" "}and merged on Oct 27 into{" "} <HoverCard id="hc-repo" trigger="quillolang/components" align="center" width={280}> <b>quillolang/components</b> <span.hc-demo-muted>Fifty UI components for Quillo, one file each. MIT.</span> <span>★ 1.2k · 48 forks · updated today</span> </HoverCard>. </p> <p style="margin: 0"> <HoverCard id="hc-plan" trigger="What is in Pro?" placement="top" delay={200}> <b>Pro plan</b> <span>Unlimited projects, custom domains and daily backups, for $12 a month.</span> </HoverCard> </p> <p.demo-note>Rest the pointer on a name, or Tab to it; Escape hides the card.</p> </div>Source
// HoverCard: a card of details shown when the pointer rests on a link or button (after// a short wait) or it is focused, which stays while the pointer is on the card, so its// own links can be clicked. Escape hides it; a tap on a button shows it on touch// screens. The trigger is described by the card (aria-describedby), so screen readers// read it too.//// <p>Reviewed by <HoverCard id="hc-ada" trigger="@ada" href="/people/ada">// <b>Ada Lovelace</b><p>Engineering, London. 214 reviews.</p>// </HoverCard></p>// <HoverCard id="hc-plan" trigger={<span><Badge>Pro</Badge> plan</span>}> ... </HoverCard>//// id (required): the card's id, unique on the page. trigger: what is hovered: text, or// HTML when the HoverCard is in your own view or component. href: makes the trigger a// link; without one it is a button. children: the card. It is a <span>, so it can sit// in a sentence: give it <span>s (each is a line of its own), <b>, <a>, <img>, not <p>// or <div>, which HTML does not allow inside a paragraph.// placement: "bottom" (default) or "top". align: "start" (default), "center" or// "end". width: the card's width in pixels (300). delay: how long the pointer rests// before it shows, in ms (400).// HoverCardHost is the part that runs in the browser; HoverCard puts it together.import ui/theme.qwe use ui_clslet hover_card_css = css`.ui-hc { position: relative; display: inline-block; }.ui-hc-trigger { display: inline; padding: 0; border: 0; background: none; color: var(--ui-accent); font: inherit; font-weight: 500; text-decoration: underline dotted; text-underline-offset: 3px; cursor: pointer;}a.ui-hc-trigger { text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--ui-accent) 40%, transparent); }.ui-hc-trigger:hover { text-decoration-color: currentColor; }.ui-hc-trigger:focus-visible { outline: none; border-radius: 3px; box-shadow: var(--ui-focus); }.ui-hc-card { position: absolute; z-index: 60; top: calc(100% + 8px); left: 0; box-sizing: border-box; width: min(var(--w, 300px), calc(100vw - 32px)); padding: 14px 16px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); box-shadow: var(--ui-shadow-lg); font: 14px/1.5 var(--ui-font); text-align: left; white-space: normal; display: none;}.ui-hc-card::before { content: ""; position: absolute; inset: -10px 0; z-index: -1; }.ui-hc-card > span { display: block; }.ui-hc-card a { color: var(--ui-accent); }.ui-hc-card a:focus-visible { outline: none; border-radius: 3px; box-shadow: var(--ui-focus); }.ui-hc-card > * + * { margin-top: 6px; }.ui-hc-top .ui-hc-card { top: auto; bottom: calc(100% + 8px); }.ui-hc-center .ui-hc-card { left: 50%; margin-left: calc(min(var(--w, 300px), calc(100vw - 32px)) / -2); }.ui-hc-end .ui-hc-card { left: auto; right: 0; }/* Shown at once but seen only after the delay, so a pointer passing by does not open it. */.ui-hc:not(.ui-hc-off):is(:hover, :focus-within, .ui-hc-open) .ui-hc-card { display: block; visibility: visible; opacity: 1; translate: 0 0; transition: visibility 0s, opacity .15s, translate .15s; transition-delay: var(--delay, 400ms);}@starting-style { .ui-hc:not(.ui-hc-off):is(:hover, :focus-within, .ui-hc-open) .ui-hc-card { visibility: hidden; opacity: 0; translate: 0 4px; }}.ui-hc:not(.ui-hc-off).ui-hc-pinned .ui-hc-card { transition-delay: 0s; }@supports (position-area: bottom) { .ui-hc-card { position: fixed; inset: auto; margin: 8px 0; position-anchor: var(--a); position-area: bottom span-right; position-try-fallbacks: bottom span-left, flip-block, top span-left; } .ui-hc-top .ui-hc-card { inset: auto; position-area: top span-right; position-try-fallbacks: top span-left, flip-block, bottom span-left; } .ui-hc-center .ui-hc-card { margin-left: 0; position-area: bottom; position-try-fallbacks: bottom span-right, bottom span-left, flip-block; } .ui-hc-top.ui-hc-center .ui-hc-card { position-area: top; position-try-fallbacks: top span-right, top span-left, flip-block; } .ui-hc-end .ui-hc-card { position-area: bottom span-left; position-try-fallbacks: bottom span-right, flip-block, top span-right; } .ui-hc-top.ui-hc-end .ui-hc-card { position-area: top span-left; position-try-fallbacks: top span-right, flip-block, bottom span-right; } @media (max-width: 520px) { .ui-hc .ui-hc-card { margin: 8px 16px; position-area: bottom span-all; justify-self: center; position-try-fallbacks: flip-block; } .ui-hc-top .ui-hc-card { position-area: top span-all; } }}@media (hover: none) { .ui-hc:not(.ui-hc-open):not(:focus-within) .ui-hc-card { display: none; } }@media (prefers-reduced-motion: reduce) { .ui-hc .ui-hc-card { translate: none !important; } }`component HoverCardHost(class: str? = null, style: str? = null, children) { style global hover_card_css state hover = false state pinned = false state off = false let calm = () => { off = false pinned = false } <span class={ui_cls(["ui-hc", (hover || pinned) && "ui-hc-open", pinned && "ui-hc-pinned", off && "ui-hc-off", class])} style={style} @pointerenter={(e) => { if e.pointerType != "touch" { hover = true } }} @pointerleave={(e) => { hover = false if !e.target.matches(":focus-within") { calm() } }} @focusout={(e) => { let host = e.target.closest(".ui-hc") if !hover && (e.relatedTarget == null || !host.contains(e.relatedTarget)) { calm() } }} @click={(e) => { if e.target.closest("button.ui-hc-trigger") != null { pinned = !pinned || off off = false } }} @keydown={(e) => { if e.key == "Escape" && !off { e.preventDefault() off = true pinned = false } }}>{children}</span>}view HoverCard(id: str, trigger, href: str? = null, placement: str = "bottom", align: str = "start", width: int = 300, delay: int = 400, class: str? = null, children) { style global hover_card_css <HoverCardHost class={ui_cls([placement == "top" && "ui-hc-top", align != "start" && `ui-hc-${align}`, class])} style={`--w: ${width}px; --delay: ${delay}ms; --a: --${id}`}> {href != null ? <a.ui-hc-trigger href={href} aria-describedby={id} style={`anchor-name: --${id}`}>{trigger}</a> : <button.ui-hc-trigger type="button" aria-describedby={id} style={`anchor-name: --${id}`}>{trigger}</button>} <span.ui-hc-card id={id}>{children}</span> </HoverCardHost>}Related
Thread 0 posts
On HoverCard: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.