← Back to UI screens

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

ScreenshotPopover component — web, brutalist style
Theme

Popover component — web, brutalist 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). Brutalist style: high contrast, Space Grotesk, uppercase headings, thick black borders and hard offset shadows. 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
Web
Style
Brutalist
Product (fictional)
Patchbay
Category
Developer

Spec

Colours (light)
background #faf9f6 · surface #ffffff · text #201d15 · muted #676357 · primary #736000 · accent #506fdb
Colours (dark)
background #11100b · surface #211f17 · text #f0f0ed · muted #b4b1a7 · primary #c2a41c · accent #9fb9ff
Fonts
Space Grotesk (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
popoverwebdeveloperbuttonform

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:#faf9f6;--surface:#ffffff;--border:#dbd8ce;--text:#201d15;--muted:#676357;--primary:#736000;--on-primary:#ffffff;--accent:#506fdb;--font-h:"Space Grotesk",Georgia,serif;--font-b:"Space Grotesk",system-ui,sans-serif;--hw:700;
--c-bw:2px;--c-line:var(--text);--c-r:0;--c-pill:0;--c-sh:3px 3px 0 var(--text);--c-pop:6px 6px 0 var(--text);--c-tt:uppercase;--c-ls:.02em;--c-fw:700;--c-fs:13px;--c-ring:3px solid var(--text);--c-ind:4px;color-scheme:light}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#11100b;--surface:#211f17;--border:#37352d;--text:#f0f0ed;--muted:#b4b1a7;--primary:#c2a41c;--on-primary:#111111;--accent:#9fb9ff;color-scheme:dark}
}
:root[data-theme="dark"]{--bg:#11100b;--surface:#211f17;--border:#37352d;--text:#f0f0ed;--muted:#b4b1a7;--primary:#c2a41c;--on-primary:#111111;--accent:#9fb9ff;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(--text);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 (3px solid text), never removed
- **Style knobs (brutalist):** border 2px, corners 0, shadow `3px 3px 0 var(--text)`, labels uppercase, tracked .02em
- **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: Space Grotesk (Google Fonts)

Writing about this component?

More popover components

Popover component — web, minimal (Patchbay)
web · minimal
Popover component — web, editorial (Patchbay)
web · editorial
Popover component — web, luxury (Patchbay)
web · luxury
Popover component — web, playful (Patchbay)
web · playful
Popover component — web, corporate (Patchbay)
web · corporate
Popover component — web, retro (Patchbay)
web · retro
Popover component — web, monochrome (Patchbay)
web · monochrome
Popover component — mobile, brutalist (Patchbay)
mobile · brutalist
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