Overlays

HoverCard

A card of details on hover or focus, after a short wait, that stays under the pointer; Escape hides it.

#overlay#popover#user#no-js

Demo

Preview

Reviewed by @adaAda LovelaceEngineering · LondonWrote the parser and the type checker. 214 reviews this year.See her profile and merged on Oct 27 into quillolang/componentsFifty UI components for Quillo, one file each. MIT.★ 1.2k · 48 forks · updated today.

Pro planUnlimited projects, custom domains and daily backups, for $12 a month.

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.qwe
main.qwe
import 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.

demos/hover_card.qwe
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

ui/hover_card.qwe
// 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>}
Thread
56Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.