Try it
The URL of an OTE feed.json document.
Paste an OTE feed object, an event array, or one OTE event object.
Optional maximum number of events to show. Default: no limit.
Optional. Cards use a built-in placeholder when an event has no image.
What happens when a visitor selects an event. Default: modal.
Native actions. The default placement is the event detail view.
Adds a custom action with an icon that shows an alert for the selected event.
Optional CSS font-family for the widget.
Optional base font-size for the widget.
Which optional pieces of each event to show. Ignored by the calendar layout.
Language of the widget's own UI text (loading/empty/error messages). "auto" follows the visitor's browser language.
Include events whose start date has already passed. Default: on.
Live preview
Copy-paste snippet
This is the exact markup for the configuration above.
The snippet uses a fixed widget version. Use
/embed/latest/ only when you explicitly want automatic
updates.
Versioning
Production embeds should use a fixed version URL so future widget changes do not alter existing sites without an explicit upgrade.
- Recommended
https://tools.opentechevents.org/embed/v0.3.1/ote-events.js- Automatic updates
https://tools.opentechevents.org/embed/latest/ote-events.js- Legacy alias
https://tools.opentechevents.org/embed/ote-events.js
The widget follows semantic versioning: patch releases are compatible fixes, minor releases add compatible features, and major releases may require migration. Check the changelog before changing the version used by a production page.
Component versions and OTE Spec versions are independent. The component version tells you which widget bundle you are loading; the OTE Spec version tells you which event/feed shape that bundle is designed to consume.
Host app integration
Apps such as OTE Reader can pass already-filtered events in memory and
keep subscriptions, folders, read state, and persistence outside the
widget. Use sort="none" when the host app owns ordering.
await customElements.whenDefined("ote-events");
const widget = document.querySelector("ote-events");
widget.setAttribute("sort", "none");
widget.setAttribute("event-actions", "none");
widget.events = filteredEvents.map((event) => ({
...event,
_feedUrl: event.feedUrl,
_feedTitle: event.feedTitle,
}));
widget.eventActions = (context) => [{
id: "save",
label: saved.has(context.originalEvent.id) ? "Saved" : "Save",
icon: saved.has(context.originalEvent.id) ? "bookmark" : "star",
pressed: saved.has(context.originalEvent.id),
placement: "preview",
onClick(_previewEvent, actionContext) {
saveEvent(actionContext.originalEvent.id, actionContext.feed?.url);
},
}];
widget.eventClassName = (context) =>
read.has(context.originalEvent.id) ? "is-read" : "is-unread";
widget.eventBadges = (context) =>
context.originalEvent.cfpOpen ? [{ label: "CFP", icon: "plus" }] : [];
| API | Use |
|---|---|
el.events |
Render an in-memory array of OTE events without fetching. |
sort="none" |
Keep the host application's ordering instead of widget date sorting. |
empty-message |
Customize the empty state shown after host-side filters. |
eventActions(context) |
Return per-event actions with dynamic label, icon, placement, and pressed state. |
eventClassName(context) |
Add host-owned state classes such as read, unread, saved, or canceled. |
eventBadges(context) |
Add host-owned badges such as folder, collection, CFP, or saved state. |
The ote-event-action event includes action,
previewEvent, originalEvent, index,
feed, and source. The widget renders only; it
does not store favorites, folders, read state, or filters.
Using AI agents?
The embed package includes an agent-facing guide with the typed
eventActions contract, placement rules, and the tests that
act as living documentation.