The porthole

One script tag, and it looks like your app

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.

The whole install

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.

Try it on this page

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.

The current configuration

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' })

Two layouts, four corners

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.

Popup display: 'popup'
The default. A floating card anchored above the launcher, sized to its content.
Slide-out display: 'slideout'
A full-height panel pinned to whichever side `position` chooses. It overlays the page rather than pushing it, so your layout does not reflow.

Feedback-only mode

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
})

Every colour is yours

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>

All the tokens

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

What reaches your board, and who sees it

The first two of these are project settings rather than init options, because they should hold however many places you've embedded the widget.

Connect votes to your users

Your backend can exchange a stable user ID for a widget token with POST /api/v1/widget-identities. Send external_id in the JSON body and authenticate with your project's API key. Derive the ID from your signed-in session; keep the API key on your server.

Pass the returned token as userToken. Votes then follow the user across devices, and people on the same network can vote independently. Tokens expire after one hour. Refresh through your backend and call ShipLog.identify with the new token. Calling it clears the current draft and refreshes vote state; call it with null on logout. Without a token, voting still uses an anonymous IP identifier.

// Your backend issues this token for the current signed-in user.
ShipLog.init({
  projectSlug: 'yourapp',
  userToken: widgetIdentity.token,
  onVoteChanged: function(result) {
    console.log(result.feedbackId, result.voted);
  }
})

onVoteChanged receives feedbackId and voted after a successful add or removal. Failed requests do not call it. Callback errors leave the saved vote intact. It runs in the browser and does not provide durable delivery.

From your backend, use GET /api/v1/votes?external_id=your-user-id with the same API key to retrieve that user's current votes. Results are paginated with page and per_page (up to 100). Existing anonymous votes remain separate. Email and name supplied through identify label submissions; they do not verify a voter's identity.

After someone submits feedback

Drafts survive errors, tab changes, and list refreshes while the widget stays on the page. A successful submission clears the form and highlights the new item; moderated submissions show a receipt while they wait for approval.

Use onFeedbackSubmitted to run your own UI after the server accepts a submission. It receives the created feedback and a pending boolean. It runs once for each successful submission response, including submissions awaiting moderation. Callback errors do not change the saved result. This callback runs in the browser; it is not a durable webhook.

ShipLog.init({
  projectSlug: 'yourapp',
  onFeedbackSubmitted: function(result) {
    console.log(result.feedback.id, result.pending);
  }
})

Hold submissions for review

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.

Hide what you've finished

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.

Remove the ShipLog footer

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.

Attribute to a signed-in user

Anonymous voting uses a hashed IP and requires no account. If your app knows the submitter's email and name, you can attach them to feedback with this call. These details are unverified labels. Use a signed widget token to connect votes to an authenticated user. When also using a token, include it in the same identify call; identify replaces the current identity.

ShipLog.identify({ email: 'user@example.com', name: 'Ada L.' })

Questions

Will the widget break my site's CSS?
No, in both directions. The panel renders inside a shadow root, so your stylesheet cannot reach into it and its styles cannot leak out onto your page. That is why the widget looks the same on a Tailwind app, a Bootstrap app and a hand-rolled one. It injects no stylesheet of its own into your document and adds one element to the body.
How big is it, and does it need a build step?
Under 20KB gzipped, and no. It is one file of vanilla JavaScript with no dependencies and no framework — you add a script tag and call ShipLog.init. There is nothing to install, nothing to bundle and no npm package, so it cannot pull a transitive dependency into your app.
Can I make it match a dark theme?
Yes — every colour in the panel is themeable, sixteen of them, passed as a theme object to ShipLog.init. Any colour you leave out keeps its default, so a dark theme is usually eight or nine values. Status badges are the one exception: they carry meaning, so they keep their semantic colours.
Can I use it as a feedback box without the changelog?
Yes. Pass feedbackOnly: true and the What's New tab is hidden, so the widget is a feedback box alone. It also skips the changelog request entirely in that mode rather than fetching data nothing will render.
Can I remove the "Powered by ShipLog" footer?
On the Captain plan ($9/mo per project), by passing hideBranding: true. The server decides the entitlement and tells the widget, so passing it on the free plan does nothing — the free plan keeps the footer. That is the only difference between the plans besides the 50-item soft cap.
Do people need an account to leave feedback?
No. Submitting and voting work without a ShipLog account. Anonymous votes use a hashed IP. Your app can optionally connect votes to its signed-in users with a verified widget token issued through your backend. Email and name passed to ShipLog.identify label submissions but do not authenticate voters.
Can I review submissions before they appear publicly?
Yes. Turn on moderation in project settings and anything submitted through the widget or the public board waits as pending until you approve it. Rejecting is reversible — nothing is destroyed — and the submitter is told their feedback arrived either way. Writes you make yourself through the API or MCP are not held.

Put it in your app

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.