Overlays
Things that float above the page — modal, dropdown, tooltip, drawer. Zero JavaScript, except the one honest line the modal and drawer need.
Modal
The native dialog is keel's modal —
there is no .k-modal class because none is needed.
The base layer styles the bare element: surface, radius, shadow,
padding, and a backdrop that fades in via
@starting-style (and degrades to a plain show in
browsers without it). The platform does the hard parts for free:
focus is trapped inside the dialog, Esc closes it, and a button
in a form method="dialog" closes it with no script
at all. Honestly: opening it is the one line of JavaScript
below — showModal(). For a modal that docks to the
screen edge instead of the middle, see the
Drawer — same element, one class.
Markup
<button class="k-btn" type="button"
onclick="document.getElementById('confirm').showModal()">Open modal</button>
<dialog id="confirm" aria-labelledby="confirm-title">
<h3 id="confirm-title">Delete this draft?</h3>
<p>The draft and its two attachments are removed.</p>
<form method="dialog" class="k-cluster">
<button class="k-btn" value="confirm">Delete draft</button>
<button class="k-btn k-btn--ghost" value="cancel">Cancel</button>
</form>
</dialog>
Dropdown
.k-dropdown is a details element
wearing a .k-btn--ghost styled summary,
with a .k-menu list that floats below when open.
It works with zero JavaScript. The honest trade-off: it closes
when you click the summary again or toggle it from the keyboard,
but it does not light-dismiss on an outside click —
that's what the missing script would buy you. Menu items are
links or real buttons; an hr between
items draws a separator.
Options ▾
Markup
<details class="k-dropdown">
<summary class="k-btn k-btn--ghost">Options ▾</summary>
<ul class="k-menu">
<li><a href="rename.html">Rename</a></li>
<li><a href="duplicate.html">Duplicate</a></li>
<li><hr></li>
<li><button type="button">Delete</button></li>
</ul>
</details>
Menu items
A .k-menu item is more than a link. A
.k-menu__label is a small section header;
.k-menu__hint pushes a shortcut or a submenu
chevron to the trailing edge. A leading .k-icon
lines up like a letter. Mark the chosen item
aria-current="true" for a single choice or
aria-checked="true" for a checkable one — both tint
it in the accent, so a menu of aria-checked options
is a multi-select picker. .is-danger reddens a
destructive item; a real [disabled] button (or
aria-disabled) dims and blocks it. As ever, keel
styles the states — your JS toggles the attributes.
Markup
<ul class="k-menu">
<li class="k-menu__label">Actions</li>
<li><button type="button">
<span class="k-icon k-icon--edit" aria-hidden="true"></span> Edit
<span class="k-menu__hint">⌘E</span>
</button></li>
<li><button type="button" aria-checked="true">
<span class="k-icon k-icon--check" aria-hidden="true"></span> Pinned
</button></li>
<li><button type="button" disabled>… Share (offline)</button></li>
<li><hr></li>
<li><button type="button" class="is-danger">
<span class="k-icon k-icon--trash" aria-hidden="true"></span> Delete
<span class="k-menu__hint">⌫</span>
</button></li>
</ul>
Tooltip
Put text in a data-k-tip attribute and it shows
as a small dark bubble on hover and on
:focus-visible, so keyboard users see it too. It's
pure CSS — which also means the attribute content is not exposed
to assistive tech. If the tip carries meaning the visible text
doesn't, repeat it in an aria-label. For anything
richer than one short line, use a popover instead.
Markup
<!-- visible text already says what it does; the tip adds detail -->
<button class="k-btn k-btn--ghost"
data-k-tip="Copies a share link to the clipboard">Copy link</button>
<!-- icon-only: the tip is the label, so mirror it in aria-label -->
<button class="k-btn k-btn--ghost"
data-k-tip="Download as PDF"
aria-label="Download as PDF">⤓</button>
Popover
.k-popover is the tooltip's richer cousin: a
styled floating surface — border, radius, shadow, padding — built
on the native popover API. Where the
Tooltip is one line of hover text, a
popover holds real content: a heading, a paragraph, links. A
trigger button carries popovertarget and the panel
carries the popover attribute, so opening it,
light-dismiss on an outside click, and Esc all come from the
platform — zero JavaScript. Honestly: keel ships the panel look,
not the tether. An un-anchored popover is centred in the top
layer by the browser; to pin it to its trigger, use CSS anchor
positioning where it's supported, or the one line of JS that does
the same. That placement is the trade-off you accept for the free
dismiss behaviour.
Downloads a self-contained HTML file. Styles are inlined, so it opens anywhere with no server.
Markup
<button class="k-btn k-btn--ghost" popovertarget="pop">Details</button>
<div id="pop" popover class="k-popover">
<strong>Export format</strong>
<p>Downloads a self-contained HTML file.</p>
<p><a href="exports.html">Read more about exports</a></p>
</div>
Drawer
.k-drawer is a dialog that slides
in from the edge instead of appearing in the middle — offcanvas
navigation, a settings panel, a cart. --start slides
it from the start edge instead of the end. Honestly: opening it
is the one line of JavaScript below —
showModal() — keel ships the look and the motion.
Because it's a real modal dialog, Esc closes it, focus is
trapped, and the backdrop comes free; a
form method="dialog" button closes it with no
script at all.
Markup
<button class="k-btn k-btn--ghost" type="button"
onclick="document.getElementById('drawer').showModal()">Open drawer</button>
<dialog class="k-drawer" id="drawer" aria-label="Menu">
<h4>Menu</h4>
<ul class="k-menu">
<li><a href="docs.html">Docs</a></li>
<li><a href="pricing.html">Pricing</a></li>
</ul>
<form method="dialog">
<button class="k-btn k-btn--ghost">Close</button>
</form>
</dialog>
<!-- slide from the other edge -->
<dialog class="k-drawer k-drawer--start">…</dialog>