← Back to UI screens

Live: try it with the mouse and the keyboard (Tab, arrows, Escape).

ScreenshotPopover component — mobile, monochrome style
Theme

Popover component — mobile, monochrome style

Popover UI component for websites and web apps, shown in Patchbay, a fictional developer product. Shows a share button that opens a non-modal popover with a short form, a disabled trigger, and every state (default, hover, focus-visible, open, disabled). Monochrome style: pure greyscale, Geist, black primary actions and flat surfaces. Comes with copyable HTML, CSS and a few lines of plain JavaScript (keyboard support and ARIA per the WAI-ARIA Authoring Practices), a live demo, and a DESIGN.md with light and dark colour tokens.

Open live HTML ↗ Download DESIGN.md
Pattern
Popover
Device
Mobile
Style
Monochrome
Product (fictional)
Patchbay
Category
Developer

Spec

Colours (light)
background #ffffff · surface #fafafa · text #111111 · muted #666666 · primary #111111 · accent #444444
Colours (dark)
background #0a0a0a · surface #161616 · text #f2f2f2 · muted #a0a0a0 · primary #f2f2f2 · accent #c8c8c8
Fonts
Geist (headings and body), JetBrains Mono (code) · Google Fonts, under their own licences
Light and dark
Both included; follows the system setting
ARIA pattern
Dialog (non-modal disclosure) (WAI-ARIA Authoring Practices)
Keyboard
Enter or Space opens; Escape closes and returns focus
States
default, hover, focus-visible, open, disabled
popovermobiledeveloperbuttonform

Code

Paste the HTML where the component goes, the CSS into your stylesheet and the script before </body>. The tokens block holds the light and dark colours; swap in your own.

HTML
<div class="mgd-pop-row">
  <div class="mgd-pop">
    <button class="mgd-btn" type="button" aria-expanded="false" aria-controls="share-pop"><svg class="i" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="6" r="2.5"/><circle cx="18" cy="18" r="2.5"/><path d="M8.2 10.8l7.6-3.6M8.2 13.2l7.6 3.6"/></svg> Share</button>
    <div class="mgd-pop__panel" id="share-pop" role="dialog" aria-labelledby="share-title" tabindex="-1" hidden>
    <h3 class="mgd-pop__title" id="share-title">Share “API latency”</h3>
    <p class="mgd-pop__hint">People you invite can view this dashboard.</p>
    <label class="mgd-pop__label" for="share-email">Email</label>
    <input class="mgd-pop__input" id="share-email" type="email" autocomplete="email" placeholder="name@company.com">
    <div class="mgd-pop__actions"><button type="button" class="mgd-btn" data-pop-close>Cancel</button><button type="button" class="mgd-btn mgd-btn--primary">Send invite</button></div>
    </div>
  </div>
  <button class="mgd-btn" type="button" disabled><svg class="i" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4v12M7 11l5 5 5-5M4 20h16"/></svg> Export (Pro)</button>
</div>
CSS
/* Tokens (light + dark) */
:root{--bg:#ffffff;--surface:#fafafa;--border:#e4e4e4;--text:#111111;--muted:#666666;--primary:#111111;--on-primary:#ffffff;--accent:#444444;--font-h:"Geist",Georgia,serif;--font-b:"Geist",system-ui,sans-serif;--hw:600;
--c-bw:1px;--c-line:var(--border);--c-r:6px;--c-pill:6px;--c-sh:none;--c-pop:0 10px 28px rgb(0 0 0 / .16);--c-tt:none;--c-ls:0;--c-fw:500;--c-fs:14px;--c-ring:2px solid var(--text);--c-ind:2px;color-scheme:light}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#0a0a0a;--surface:#161616;--border:#2c2c2c;--text:#f2f2f2;--muted:#a0a0a0;--primary:#f2f2f2;--on-primary:#0a0a0a;--accent:#c8c8c8;color-scheme:dark}
}
:root[data-theme="dark"]{--bg:#0a0a0a;--surface:#161616;--border:#2c2c2c;--text:#f2f2f2;--muted:#a0a0a0;--primary:#f2f2f2;--on-primary:#0a0a0a;--accent:#c8c8c8;color-scheme:dark}

/* Component */
.mgd-sr{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
.mgd-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:0 16px;border:var(--c-bw) solid var(--c-line);border-radius:var(--c-pill);background:var(--surface);color:var(--text);font:var(--c-fw) var(--c-fs)/1 var(--font-b);text-transform:var(--c-tt);letter-spacing:var(--c-ls);box-shadow:var(--c-sh);cursor:pointer;white-space:nowrap;transition:background-color .12s,border-color .12s}
.mgd-btn:hover,.mgd-btn.is-hover{background:color-mix(in oklab,var(--primary) 9%,var(--surface))}
.mgd-btn:focus-visible,.mgd-btn.is-focus{outline:var(--c-ring);outline-offset:2px}
.mgd-btn[aria-expanded="true"],.mgd-btn.is-open{background:color-mix(in oklab,var(--primary) 15%,var(--surface))}
.mgd-btn--primary{background:var(--primary);border-color:var(--primary);color:var(--on-primary)}
.mgd-btn--primary:hover,.mgd-btn--primary.is-hover{background:color-mix(in oklab,var(--primary) 84%,var(--text))}
.mgd-btn:disabled,.mgd-btn[aria-disabled="true"],.mgd-btn.is-disabled{opacity:.5;cursor:not-allowed;box-shadow:none}
.mgd-btn svg{flex:none}
.mgd-pop{position:relative;display:inline-block}
.mgd-pop__panel{position:absolute;z-index:10;top:calc(100% + 10px);left:0;width:min(340px,calc(100vw - 40px));padding:18px;background:var(--surface);color:var(--text);border:var(--c-bw) solid var(--c-line);border-radius:var(--c-r);box-shadow:var(--c-pop)}
.mgd-pop__panel[hidden]{display:none}
.mgd-pop__panel:focus-visible{outline:var(--c-ring);outline-offset:2px}
.mgd-pop__title{margin:0 0 4px;font:var(--hw) 17px/1.25 var(--font-h)}
.mgd-pop__hint{margin:0 0 14px;color:var(--muted);font-size:13px}
.mgd-pop__label{display:block;margin:0 0 6px;font:600 13px var(--font-b)}
.mgd-pop__input{display:flex;align-items:center;width:100%;height:42px;padding:0 12px;border:1px solid var(--muted);border-radius:var(--c-r);background:var(--bg);color:var(--text);font:15px var(--font-b)}
.mgd-pop__input::placeholder{color:var(--muted)}
.mgd-pop__input:focus-visible{outline:var(--c-ring);outline-offset:1px}
.mgd-pop__actions{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}
.mgd-pop-row{display:flex;flex-wrap:wrap;gap:12px;align-items:center}
@media (prefers-reduced-motion:reduce){.mgd-pop,.mgd-pop *{transition:none!important;animation-duration:2.4s!important}}
JavaScript
document.querySelectorAll(".mgd-pop").forEach(function (wrap) {
  var btn = wrap.querySelector("[aria-controls]"), panel = document.getElementById(btn.getAttribute("aria-controls"));
  function open() { panel.hidden = false; btn.setAttribute("aria-expanded", "true"); (panel.querySelector("input,select,textarea,button") || panel).focus(); }
  function close(back) { if (panel.hidden) return; panel.hidden = true; btn.setAttribute("aria-expanded", "false"); if (back) btn.focus(); }
  btn.addEventListener("click", function () { panel.hidden ? open() : close(true); });
  panel.addEventListener("click", function (e) { if (e.target.closest("[data-pop-close]")) close(true); });
  wrap.addEventListener("keydown", function (e) { if (e.key === "Escape" && !panel.hidden) { e.stopPropagation(); close(true); } });
  document.addEventListener("click", function (e) { if (!wrap.contains(e.target)) close(false); });
  wrap.addEventListener("focusout", function (e) { if (e.relatedTarget && !wrap.contains(e.relatedTarget)) close(false); });
});

DESIGN.md

Component section (states, ARIA, keyboard)
## Component: Popover

A small non-modal panel anchored to a button, for a short form or details. Escape or a click outside closes it.

- **Pattern:** WAI-ARIA Authoring Practices, Dialog (non-modal disclosure) (https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)
- **Roles and attributes:** The trigger is a button with aria-expanded and aria-controls; the panel has role="dialog" and aria-labelledby, is not modal, and takes focus on open. Escape closes it and returns focus to the trigger.
- **States drawn:** default, hover, focus-visible, open, disabled
- **Focus:** a visible `:focus-visible` outline (2px solid text), never removed
- **Style knobs (monochrome):** border 1px, corners 6px, shadow none, labels sentence case
- **Motion:** transitions are short (≤160ms) and switched off under `prefers-reduced-motion: reduce`
- **Code:** HTML + CSS + a few lines of plain JavaScript (no libraries); colours come only from the tokens above, so light and dark both work

### Keyboard
- **Enter / Space:** open or close from the trigger
- **Tab:** move through the panel's controls
- **Escape:** close and return focus to the trigger

### Licence
Original Made Good Designs code: free to use and adapt in any project, including commercial ones, with no credit needed. Fonts are Google Fonts under their own licences.
AI

Build this with your AI

Fonts: Geist (Google Fonts)

Writing about this component?

More popover components

Popover component — web, monochrome (Patchbay)
web · monochrome
Popover component — mobile, minimal (Patchbay)
mobile · minimal
Popover component — mobile, editorial (Patchbay)
mobile · editorial
Popover component — mobile, brutalist (Patchbay)
mobile · brutalist
Popover component — mobile, luxury (Patchbay)
mobile · luxury
Popover component — mobile, playful (Patchbay)
mobile · playful
Popover component — mobile, corporate (Patchbay)
mobile · corporate
Popover component — mobile, retro (Patchbay)
mobile · retro
Building with an AI coding agent? Every screen here is also in the free Made Good Designs MCP server: ask Claude, Cursor or ChatGPT to search_ui_screens (e.g. “billing settings, editorial style”), get_ui_flow for a whole journey, and get_ui_screen_code to pull the HTML and DESIGN.md straight into your project.

Save to board