Configuration
Getting Started: Installation · Quick Start · This Page
Every Alap instance starts with a config object. At minimum you need allLinks — a dictionary of your links. Everything else is optional.
Live version with interactive examples: https://docs.alap.info/getting-started/configuration
Config shape
interface AlapConfig {
// --- Data layer (pure JSON, serializable) ---
allLinks: Record<string, AlapLink>; // required
settings?: AlapSettings;
macros?: Record<string, AlapMacro>;
searchPatterns?: Record<string, AlapSearchPattern | string>;
// --- Code layer (JavaScript only) ---
protocols?: Record<string, AlapProtocol>;
}The config has two layers:
- Data —
allLinks,macros,settings,searchPatterns. Pure JSON. This is what editors produce, what gets saved to a database, and what can be loaded from a<script type="application/json">block or a remote endpoint. - Code —
protocols. Custom protocol handlers are functions, so they can only be defined in JavaScript/TypeScript. They are part of your application code, not your stored data.
You can define everything at the code layer — write your entire config in a .ts file, skip JSON entirely, and be done. But you can't go the other direction: a pure JSON config gives you links, tags, macros, expressions, and access to Alap's built-in protocols (:web:, :atproto:, :json:, etc.), but no custom protocols. Custom protocols require code.
Most projects land somewhere in between: data lives in JSON (portable, editor-friendly, storable), and the code layer wraps it with any protocols the project needs:
// alap-config.ts
import linkData from './links.json'; // data layer — from editor, DB, API
export const config: AlapConfig = {
...linkData, // allLinks, macros, settings
protocols: { // code layer — defined in your app
price: {
handler: (segments, link) => { /* ... */ },
},
},
};allLinks — your link library
The traditional web model: each link knows its destination. One <a> tag, one href.
Alap flips this. You build a library of links — a single collection of everything you might want to link to — and then your links query into it.
allLinks: {
golden_gate: {
url: 'https://en.wikipedia.org/wiki/Golden_Gate_Bridge',
label: 'Golden Gate Bridge',
tags: ['bridge', 'sf', 'landmark'],
},
brooklyn: {
url: 'https://en.wikipedia.org/wiki/Brooklyn_Bridge',
label: 'Brooklyn Bridge',
tags: ['bridge', 'nyc', 'landmark'],
},
bluebottle: {
url: 'https://bluebottlecoffee.com',
label: 'Blue Bottle Coffee',
tags: ['coffee', 'sf'],
},
}Each entry has an ID (the key), a URL, a label, and tags. Both the ID and the tags are queryable:
- By ID —
golden_gatein an expression picks that one specific link. Good for hand-curated picks where you know exactly which item you want. - By tag —
.coffeepicks every link carrying thecoffeetag. Tags are the connective tissue — they describe what a link is about without deciding where it should appear. Add a new coffee shop to your library tomorrow, and every.coffeequery picks it up automatically.
Mix them freely: golden_gate, .coffee + .sf means "the Golden Gate link, plus every SF coffee spot." IDs for precision, tags for breadth.
Link fields
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Destination URL |
label | string | No | Display text (required unless image is set) |
tags | string[] | No | Tags for .tag queries |
cssClass | string | No | CSS class applied to the menu item |
image | string | No | Image URL rendered instead of text |
altText | string | No | Alt text for image |
targetWindow | string | No | _self, _blank, etc. Default: "fromAlap" |
description | string | No | Used by search patterns and hooks |
thumbnail | string | No | Preview image for hover/context events |
hooks | string[] | No | Event hooks this item participates in |
guid | string | No | Permanent UUID that survives renames |
createdAt | string | number | No | ISO 8601 or Unix ms. Used by age filters |
meta | Record<string, unknown> | No | Arbitrary metadata for protocol queries |
settings — global defaults
Settings control menu behavior. All fields are optional — omit settings entirely to use the defaults.
settings: {
listType: 'ul',
menuTimeout: 3000,
maxVisibleItems: 6,
existingUrl: 'prepend',
placement: 'S',
placementGap: 8,
hooks: ['item-hover'],
}| Field | Type | Default | Description |
|---|---|---|---|
listType | 'ul' | 'ol' | 'ul' | Menu list element type |
menuTimeout | number | 5000 | Auto-dismiss timeout (ms) after mouse leaves |
maxVisibleItems | number | 10 | Items before the menu scrolls. 0 = no limit |
existingUrl | 'prepend' | 'append' | 'ignore' | 'prepend' | How to handle an existing href on the trigger |
placement | string | 'SE' | Comma-separated placement string: compass direction + strategy (e.g. 'SE', 'SE, clamp') |
placementGap | number | 4 | Pixel gap between trigger edge and menu edge |
viewportPadding | number | 8 | Minimum distance the menu keeps from viewport edges |
viewportAdjust | boolean | true | Enable smart placement with viewport containment |
preventFocusScroll | boolean | true | Prevent viewport scrolling when focus moves to menu items on keyboard open |
hooks | string[] | — | Default hooks for all items (per-link hooks overrides) |
Placement
The placement setting controls where the menu appears relative to the trigger. Think of it as a compass:
NW N NE
┌────┬────┐
W │ trigger │ E
└────┴────┘
SW S SE
(C = centered over trigger)The default is SE — below the trigger, left edge aligned with the trigger's left edge. The default strategy is flip — if the preferred direction doesn't fit, the engine tries fallback positions.
Add a strategy to control how hard the engine tries: "SE" (flip, default), "SE, clamp" (constrain to viewport), or "SE, place" (pinned, no fallback). When no placement is set at all, the engine doesn't run — CSS positions the menu.
Per-element override: data-alap-placement="N, clamp" (DOM mode) or placement="N, clamp" (web component / framework adapters).
See Placement for the full guide on placement, strategies, and CSS styling.
macros — reusable expressions
Macros name a query so you can reference it with @:
macros: {
nyc_bridges: { linkItems: '.nyc + .bridge' },
sf_coffee: { linkItems: '.coffee + .sf' },
all_bridges: { linkItems: '@nyc_bridges | @sf_bridges' },
}<alap-link query="@nyc_bridges">NYC bridges</alap-link>Macros can reference other macros. See Macros for nesting rules and cycle protection.
searchPatterns — named regex searches
Define regex queries you can reference with /name/ syntax:
searchPatterns: {
bridges: 'bridge|viaduct',
recent_coffee: {
pattern: 'coffee|cafe',
options: { fields: 'lt', age: '30d', sort: 'newest', limit: 10 },
},
}A plain string is shorthand for { pattern: "..." } with default options. See Search Patterns for the full options reference.
protocols — dimensional queries
Protocol expressions extend the query language with domain-specific filtering — time, location, or any custom dimension. Because protocol handlers are functions, they can't live in a JSON config — they must be defined in a JS/TS module (e.g. alap-config.ts), alongside allLinks, macros, and settings:
const config: AlapConfig = {
allLinks: {
bluebottle: {
label: 'Blue Bottle',
url: 'https://bluebottlecoffee.com',
tags: ['coffee', 'sf'],
meta: { price: 5 },
},
stumptown: {
label: 'Stumptown',
url: 'https://stumptowncoffee.com',
tags: ['coffee', 'portland'],
meta: { price: 4 },
},
},
protocols: {
price: {
handler: (segments, link) => {
if (!link.meta?.price) return false;
const min = parseFloat(segments[0]);
const max = parseFloat(segments[1]);
return link.meta.price >= min && link.meta.price <= max;
},
},
},
};The :price:0:10: syntax in an expression invokes the price protocol handler, filtering items by their meta.price value:
<alap-link query=".coffee + :price:0:10:">affordable cafes</alap-link>See Protocols for handler contracts and source chains.
Inline configuration
Alap's config is content — labels, URLs, tags, relationships. It can live in the document itself as a <script type="application/json"> block:
<script type="application/json" id="alap-config">
{
"allLinks": {
"golden": { "url": "https://...", "label": "Golden Gate", "tags": ["bridge", "sf"] },
"brooklyn": { "url": "https://...", "label": "Brooklyn Bridge", "tags": ["bridge", "nyc"] }
}
}
</script>
<script>
const el = document.getElementById('alap-config');
const config = JSON.parse(el.textContent);
Alap.registerConfig(config);
</script>The browser won't execute it, but anything that reads the page — human or machine — can parse it. One artifact, two purposes: runtime configuration and a readable description of the page's link structure.
Complete example
const config: AlapConfig = {
settings: {
listType: 'ul',
menuTimeout: 5000,
maxVisibleItems: 8,
existingUrl: 'prepend',
hooks: ['item-hover'],
},
macros: {
nyc_bridges: { linkItems: '.nyc + .bridge' },
sf_outdoors: { linkItems: '(.sf + .park) | (.sf + .beach)' },
staff_picks: { linkItems: 'golden_gate, bluebottle, highline' },
},
searchPatterns: {
bridges: { pattern: 'bridge', options: { fields: 'lt', sort: 'alpha' } },
recent_items: { pattern: '.', options: { age: '7d', sort: 'newest', limit: 5 } },
},
allLinks: {
golden_gate: {
url: 'https://en.wikipedia.org/wiki/Golden_Gate_Bridge',
label: 'Golden Gate Bridge',
tags: ['bridge', 'sf', 'landmark'],
description: 'Iconic suspension bridge spanning the Golden Gate strait.',
createdAt: '2026-01-15T10:30:00Z',
},
brooklyn: {
url: 'https://en.wikipedia.org/wiki/Brooklyn_Bridge',
label: 'Brooklyn Bridge',
tags: ['bridge', 'nyc', 'landmark'],
createdAt: '2026-01-20T09:00:00Z',
},
bluebottle: {
url: 'https://bluebottlecoffee.com',
label: 'Blue Bottle Coffee',
tags: ['coffee', 'sf'],
createdAt: '2026-02-10T14:00:00Z',
},
highline: {
url: 'https://www.thehighline.org',
label: 'The High Line',
tags: ['park', 'nyc', 'landmark'],
createdAt: '2026-03-01T11:00:00Z',
},
},
};With this config:
<alap-link query="@nyc_bridges">NYC bridges</alap-link>
<alap-link query="@staff_picks">our picks</alap-link>
<alap-link query="/recent_items/">this week</alap-link>
<alap-link query=".landmark - .nyc">non-NYC landmarks</alap-link>Next steps
- Expressions — the query language that makes this work
- Types — full TypeScript interface definitions