The porthole
The ShipLog widget is a single file of vanilla JavaScript — under 20KB gzipped, no dependencies, no build step and no npm package. It renders in a shadow root, so your CSS cannot leak in and its styles cannot leak out. Every colour is themeable, it can be a popup or a full-height slide-out, and it is on every plan including the free one.
Two tags, anywhere in your page. Replace yourapp with your project slug, which you'll find in Settings once you have an account.
<script defer src="https://app.shiplog.ca/widget/shiplog-widget.js"></script>
<script>ShipLog.init({ projectSlug: 'yourapp' })</script>
That is the entire integration. There is no account to connect, no domain to allowlist and no key to keep secret — the slug is public, the same way your board URL is.
The launcher in the corner is the real widget, not a screenshot — the same file your embed would load. Switch its configuration and it tears down and re-initialises in place, exactly as it would with those options in your own init call.
Submissions here are real and land on a throwaway demo project, so feel free to post something. It is not ShipLog's own board.
Snippets further down this page have a Run button beside Copy — it applies that snippet to the widget above so you can see it without pasting anything into your own app. Those snippets read projectSlug: 'yourapp' because that is the part you replace; Run substitutes this demo project's slug so there is something real to talk to.
This updates as you switch above or hit Run below. Copy it and you get exactly what you're looking at, slug and all.
ShipLog.init({ projectSlug: 'widget-demo-gst2dq' })
display chooses the shape and position chooses the side. A value the widget doesn't recognise falls back to the popup rather than to no panel at all, so a typo degrades quietly instead of breaking your page.
display: 'popup'
display: 'slideout'
If you publish your changelog somewhere else — or not at all — pass feedbackOnly: true and the What's New tab disappears. The widget then skips the changelog request entirely rather than fetching data nothing will render, so it costs your visitors one round trip instead of two.
ShipLog.init({
projectSlug: 'yourapp',
feedbackOnly: true
})
16 colours, passed as a theme object. Anything you leave out keeps its default, so a dark panel is usually eight or nine values rather than all 16. A theme passed to init overrides the brand colour configured in your project settings, which is what you want when one project is embedded in two differently-skinned places.
<script defer src="https://app.shiplog.ca/widget/shiplog-widget.js"></script>
<script>
ShipLog.init({
projectSlug: 'yourapp',
theme: {
primaryColor: '#8b5cf6',
background: '#0f1420',
surface: '#171d2b',
text: '#f3f4f6',
textMuted: '#a6adbb',
border: '#252c3b',
inputBorder: '#333c4e',
divider: '#1e2432'
}
})
</script>
Use six-digit hex. Status badges — open, planned, in progress, shipped — keep their own semantic colours and are deliberately not themeable, because a reader who has learned what green means on one board should not have to relearn it on yours.
| Token | Default | Paints |
|---|---|---|
| primaryColor |
#0047B6
|
buttons, active tab, links |
| onPrimary |
#fff
|
text sitting on those buttons |
| background |
#fff
|
panel background |
| surface |
#fff
|
vote buttons and input fields |
| text |
#111827
|
titles and text you type |
| textStrong |
#374151
|
tab hover, bold changelog text |
| textMuted |
#6b7280
|
descriptions, inactive tabs |
| textSubtle |
#9ca3af
|
dates, loading and empty states |
| textFaint |
#d1d5db
|
the footer credit |
| border |
#e5e7eb
|
panel and vote button borders |
| inputBorder |
#d1d5db
|
input and textarea borders |
| divider |
#f3f4f6
|
lines between rows |
| codeBg |
#f3f4f6
|
inline code in changelog entries |
| accent |
#ef4444
|
the unread dot |
| dangerBg |
#fef2f2
|
error message background |
| dangerText |
#dc2626
|
error message text |
The first two of these are project settings rather than init options, because they should hold however many places you've embedded the widget.
Turn on moderation and anything submitted through the widget or your public board waits as pending until you approve it — nothing appears publicly in the meantime, and it isn't counted in vote totals. Rejecting is reversible: the submission is kept, so a mistaken rejection costs you a click rather than the content. The submitter is told their feedback arrived either way, so a held item doesn't read as a failure to them. Writes you make yourself through the API or MCP are not held.
Completed and closed items can be kept out of the widget's list while staying on your public board. Worth turning on once you've shipped a lot: the widget is a small panel, and a long tail of finished requests crowds out the open ones people are there to vote on.
On the Captain plan, pass hideBranding: true and the "Powered by ShipLog" credit goes away. The server decides the entitlement and tells the widget, so passing it on the free plan does nothing rather than half-working — the free plan keeps the footer. Alongside the 50-item soft cap, this is the only difference between the two plans.
Submitting and voting are anonymous by default — votes are deduplicated on a hashed IP, not a login, so nobody has to make an account to be heard. If your app already knows who someone is, tell the widget and their submissions carry it:
ShipLog.identify({ email: 'user@example.com', name: 'Ada L.' })
Free plan, every feature, 50 feedback items, no credit card. You'll have a slug to paste into that script tag in about a minute.