Skip to content

Vanilla DOM Adapter

The AlapUI class binds Alap menus to vanilla HTML. No framework required.

Setup

typescript
import { AlapUI } from 'alap';

const ui = new AlapUI(config, {
  selector: '.alap',
  menuTimeout: 5000,
  onItemHover: (detail) => { ... },
  onItemContext: (detail) => { ... },
});

Constructor options

OptionTypeDefaultDescription
selectorstring'.alap'CSS selector for trigger elements
menuTimeoutnumberfrom configAuto-dismiss timeout in ms
onTriggerHoverfunctionCallback when mouse enters trigger
onTriggerContextfunctionCallback on right-click of trigger
onItemHoverfunctionCallback when mouse enters menu item
onItemContextfunctionCallback on right-click/ArrowRight of menu item

See Events for callback detail types.

Methods

MethodDescription
refresh()Re-scan DOM for new trigger elements
updateConfig(config)Replace config and re-scan
destroy()Remove all event listeners and menu container

HTML attributes

AttributeOnDescription
class="alap"TriggerDefault selector (configurable via selector)
data-alap-linkitemsTriggerExpression to evaluate
data-alap-existingTriggerPer-anchor existingUrl override: "prepend", "append", "ignore"
data-alap-placementTriggerPer-anchor placement override: "N", "NE", "E", "SE", "S", "SW", "W", "NW", "C"
html
<a class="alap" data-alap-linkitems=".coffee">cafes</a>
<a class="alap" data-alap-linkitems=".nyc + .bridge">NYC bridges</a>
<a class="alap" data-alap-linkitems="@favorites" id="favorites">my picks</a>

ARIA (set automatically)

AttributeOnValue
roleTrigger"button"
aria-haspopupTrigger"true"
aria-expandedTrigger"true" when open, "false" when closed
tabindexTrigger"0" (if not already focusable)
roleMenu"menu"
aria-labelledbyMenuTrigger's id (if present)
role<li>"none"
role<a>"menuitem"

Generated markup

html
<div id="alapelem" class="alapelem alap_mylink" role="menu" aria-labelledby="mylink">
  <ul>
    <li class="alapListElem" role="none">
      <a href="..." role="menuitem" tabindex="-1"
         data-alap-hooks="item-hover item-context"
         data-alap-guid="a1b2c3d4..."
         data-alap-thumbnail="https://...">
        Link Label
      </a>
    </li>
  </ul>
</div>

Styling

The DOM adapter creates a single shared menu container. No shadow boundary — the full CSS cascade applies.

Selectors

SelectorElementDescription
#alapelem<div>Menu container (shared, repositioned per trigger)
.alap_${anchorId}<div>Per-anchor class (when trigger has an id)
.alapListElem<li>Every list item

Per-item cssClass

The cssClass field on an AlapLink is applied to the <li>:

json
{ "label": "Special", "url": "...", "cssClass": "featured" }
html
<li class="alapListElem featured" role="none">...</li>

Basic styling

css
#alapelem {
  background: white;
  border: 1px solid #ddd;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  padding: 4px 0;
}

#alapelem a {
  display: block;
  padding: 8px 16px;
  color: #333;
  text-decoration: none;
}

#alapelem a:hover, #alapelem a:focus {
  background: #f0f7ff;
  color: #2563eb;
}

Positioning

Alap uses a compass-based placement engine. The menu is positioned relative to the trigger, with automatic fallback when the preferred position doesn't fit in the viewport.

Placement directions

     NW    N    NE
      ┌────┬────┐
   W  │ trigger │  E
      └────┴────┘
     SW    S    SE
         (C = centered over trigger)

Default: SE (below, left-aligned). Set globally via settings.placement or per-trigger via data-alap-placement:

html
<a class="alap" data-alap-linkitems=".coffee" data-alap-placement="N">above me</a>
<a class="alap" data-alap-linkitems=".coffee" data-alap-placement="E">beside me</a>
<a class="alap" data-alap-linkitems=".coffee" data-alap-placement="C">over me</a>

Viewport containment

When viewportAdjust is true (default):

  • If the preferred placement overflows the viewport, Alap tries the opposite side, then adjacent positions
  • If no placement fits fully, the menu is clamped to the available space with vertical scrolling
  • The menu never causes the page to scroll — uses overflow: clip to prevent layout shift
  • placementGap controls the pixel gap between trigger and menu (default: 4)
  • viewportPadding controls the minimum distance from viewport edges (default: 8)

Image triggers

Image triggers use a point rect at the click coordinates. The placement engine positions the menu relative to the click point using the same compass logic and fallback behavior.

Static positioning

Set viewportAdjust: false to disable the placement engine. The menu is positioned below the trigger with no viewport awareness — the legacy behavior.

Examples

Hover preview panel:

typescript
const preview = document.getElementById('preview');

const ui = new AlapUI(config, {
  onItemHover: ({ link }) => {
    preview.innerHTML = `
      <img src="${link.thumbnail}" />
      <p>${link.description}</p>
    `;
    preview.hidden = false;
  },
});

Dynamically added content:

typescript
ui.refresh();                    // after AJAX loads new .alap links
ui.updateConfig(updatedConfig);  // after config changes
ui.destroy();                    // cleanup on SPA route change

Existing URL preservation:

html
<!-- Original href becomes the first menu item (default: prepend) -->
<a class="alap" href="https://example.com" data-alap-linkitems=".coffee">
  example.com and coffee
</a>

<!-- Override per-link -->
<a class="alap" href="https://example.com" data-alap-linkitems=".coffee" data-alap-existing="append">
  coffee then example.com
</a>