Widget reference
<script type="module" src="https://unpkg.com/shipdot@0.1"></script><ship-dot src="/shipdot.json"></ship-dot>Importing shipdot registers the <ship-dot> custom element. The import is safe during server-side rendering (it does nothing there). The package has zero runtime dependencies and weighs about 6.5 KB min+gzip; CI fails above 10 KB.
Attributes
Section titled “Attributes”| Attribute | Default | Description |
|---|---|---|
src |
— (required) | URL of shipdot.json, relative to the page. It is the only request the widget makes. |
mode |
popover |
popover, drawer or modal. |
locale |
navigator.languages |
Language to show. See i18n. |
theme |
auto |
light, dark or auto (follows prefers-color-scheme). |
hotspots |
on | off disables hotspots. |
max-hotspots |
3 |
Maximum hotspots visible at once. |
first-visit |
mark-read |
What a first-time user sees. mark-read: everything already published counts as read. show-latest: the newest entry stays unread. |
storage-key |
default |
Read state is stored in localStorage under shipdot:<storage-key>. Use different keys for widgets with different feeds. |
The element reflects the unread count in data-unread.
Properties and methods
Section titled “Properties and methods”| Member | Description |
|---|---|
entries |
All entries of the loaded feed, newest first (read-only). |
unread |
Unread entries, newest first (read-only). |
open() |
Opens the panel and marks every entry as read. |
close() |
Closes the panel and returns focus to the trigger. |
markAllRead() |
Marks every entry as read, clearing the badge and the hotspots. |
markRead(ids) |
Marks the entries with these IDs as read. Works before the feed loads. See Syncing read state across devices. |
replayLatest() |
Marks the newest entry as unread again, bringing back its badge and hotspot. Handy for a “What’s new?” link. |
refresh() |
Fetches src again and re-renders. Returns a promise. |
const widget = document.querySelector('ship-dot')document.querySelector('#whats-new-link').addEventListener('click', () => widget.open())Events
Section titled “Events”All events are CustomEvents that bubble and cross shadow roots (composed), so you can listen on document.
| Event | detail |
When |
|---|---|---|
shipdot:ready |
{ entries, unread, locale } |
The feed loaded (after every refresh()). |
shipdot:open |
{ mode } |
The panel opened. |
shipdot:close |
{ mode } |
The panel closed. |
shipdot:read |
{ ids } |
Entries were marked as read. |
shipdot:hotspot-click |
{ id } |
A hotspot was clicked or activated with the keyboard. |
shipdot:error |
{ error } |
The feed could not be loaded or a target selector is invalid. |
document.addEventListener('shipdot:read', (event) => { console.log('read', event.detail.ids)})Slot, parts and custom properties
Section titled “Slot, parts and custom properties”- The default slot replaces the bell with your own trigger. See Theming.
- Parts:
trigger,badge,panel,header,close,list,entry,entry-title,tag,image,body,link, and on the hotspot layer (.shipdot-layer)hotspot,tooltip,got-it. - Custom properties:
--shipdot-accent,--shipdot-accent-contrast,--shipdot-hotspot,--shipdot-bg,--shipdot-fg,--shipdot-muted,--shipdot-border,--shipdot-radius,--shipdot-shadow,--shipdot-font,--shipdot-panel-width,--shipdot-trigger-size,--shipdot-z. Defaults in Theming.
Read state and privacy
Section titled “Read state and privacy”The IDs of read entries are stored in localStorage (only IDs still present in the feed, at most 500). Every access is wrapped in try/catch: in private mode, with storage disabled, or in a sandboxed iframe, the widget works the same and keeps read state in memory for the session. Nothing is sent anywhere.
Syncing read state across devices
Section titled “Syncing read state across devices”Read state lives on one device. If your app has user accounts, it can carry read state between devices on its own: save the IDs from shipdot:read and hand them back with markRead().
<script type="module"> await customElements.whenDefined('ship-dot') const widget = document.querySelector('ship-dot')
// Save what the user reads on this device. widget.addEventListener('shipdot:read', (event) => { fetch('/api/me/shipdot-read', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(event.detail.ids), }) })
// Mark what they read on other devices. This can run before the feed loads. const res = await fetch('/api/me/shipdot-read') if (res.ok) widget.markRead(await res.json())</script>/api/me/shipdot-read stands for an endpoint of your own app that adds the posted IDs to a set per user and returns that set. Read state only grows, so merging devices is a union and never conflicts. shipdot:read also fires for the IDs you pass to markRead() that this device had not read yet; a set ignores the ones it already has. replayLatest() only affects the current device.
shipdot itself still sends nothing: the requests above are your app’s, to your server.
Accessibility
Section titled “Accessibility”- The trigger is a
<button>whose accessible name includes the unread count (“What’s new (2 unread)”), witharia-expandedandaria-haspopup="dialog". - The panel is a labeled dialog. Drawer and modal use a native modal
<dialog>: focus is trapped inside, Esc closes it, and focus returns to the trigger. The popover closes on Esc or an outside click. - Unread entries are announced with visually hidden text, not only by color.
- Hotspots are keyboard-reachable buttons; tooltips are labeled dialogs.
- Both themes meet WCAG 2.1 AA contrast. axe runs on every panel mode and theme in CI.
prefers-reduced-motiondisables the pulse and slide animations.
Browser support
Section titled “Browser support”Chrome and Edge 123+, Firefox 125+, Safari 17.5+. The widget relies on the Popover API, <dialog>, Shadow DOM, light-dark() and color-mix().
TypeScript and frameworks
Section titled “TypeScript and frameworks”The package ships types for the element (ShipDot, ShipDotAttributes, ShipDotEventMap, Feed, FeedEntry) and registers ship-dot in HTMLElementTagNameMap. JSX and template typings are opt-in per framework:
import type {} from 'shipdot/react' // React 19+import type {} from 'shipdot/vue' // Vue 3import type {} from 'shipdot/svelte' // Svelte 5import type {} from 'shipdot/astro' // Astro