Skip to content

Widget

<rollout-feed> is the ready-made feed: a bell with an unread badge that opens a panel listing announcements. Opening an announcement marks it read. The panel closes with Escape, a click outside it, or its close button.

import { defineFeedElement } from '@rollout/js/ui'
defineFeedElement() // registers <rollout-feed>; safe to call more than once

defineFeedElement('my-feed') registers it under another tag name.

Attribute Description
project-key Publishable key (pk_live_…).
api-url Base URL of the rollout API, e.g. https://api.rollout.so.
variant popover (default): bell plus floating panel. inline: the panel in place, e.g. a page.
for Id of your own button that opens the popover, instead of the built-in bell.
theme light (default), dark or auto, optionally plus simple: theme="auto simple".
page-size Announcements per page (default 20, at most 50).
nonce CSP nonce for the <style> elements the widget adds.
Property Description
token Signed identity token, or a function returning a fresh one. It is called again after a 401.
user Unverified identity { id, org?, createdAt?, attrs? }. Only works if the project allows it.
client A headless client from init() to share, instead of project-key, api-url and token.
appearance Styling; see Appearance.
localization Texts; see Localization.
refresh When to reload the feed; see Headless client → Options.
fetch A custom fetch implementation.
rollout Read-only: the headless client behind the widget, or null until it is configured.

Changes made in the same tick are batched, so setting several properties re-renders once.

  • open(), close(), toggle() control the popover.
  • The openchange event fires when it opens or closes; event.detail.open is the new state.
feed.addEventListener('openchange', (event) => {
if (event.detail.open) analytics.track('Opened what’s new')
})

To open the feed from a button in your navigation, give the button an id and point for at it:

<button id="whats-new">What's new</button>
<rollout-feed
for="whats-new"
project-key="pk_live_…"
api-url="https://api.rollout.so"
></rollout-feed>

To show your own unread badge, share one client between the widget and your code:

import { init } from '@rollout/js'
const rollout = init({ projectKey: 'pk_live_…', apiUrl: 'https://api.rollout.so', token })
feed.client = rollout
rollout.subscribe(({ unreadCount }) => (badge.textContent = unreadCount || ''))

mountFeed(target, options) from @rollout/js/ui renders the widget into any element. It takes the headless client’s options plus variant, trigger (an element), appearance, localization, onOpenChange and nonce, or an existing client. It returns { client, open, close, toggle, update, destroy }. update() changes the presentation in place; destroy() unmounts it and also destroys the client unless you passed one in.

The appearance property is modeled after Clerk’s. Everything is optional, and the parts apply in order: theme, then variables, then elements.

feed.appearance = {
theme: 'dark', // 'light' | 'dark' | 'auto' | 'simple', a shared appearance object, or an array
variables: {
colorPrimary: '#7c3aed',
colorBackground: '#fffdf7',
fontFamily: 'Inter, sans-serif',
borderRadius: '1rem',
},
elements: {
panel: 'shadow-2xl ring-1 ring-black/5', // class names, e.g. Tailwind…
item: { '&[data-unread]': { backgroundColor: '#f5f3ff' } }, // …or CSS objects
tag: { borderRadius: '999px', textTransform: 'none' },
},
layout: { placement: 'bottom-start', showImages: false },
}
Preset Effect
light Light colors (default).
dark Dark colors.
auto Follows the operating system.
simple Drops decorative styles (colors, borders, shadows, radii) and keeps the layout.

A theme can also be an appearance object, so several widgets can share one design. With an array, later entries win.

Each variable becomes a CSS custom property on the widget root, colorPrimary → --rollout-color-primary, so you can also set them in your own CSS.

Variable Used for
colorPrimary Accent: links, focus rings, “Improved” tags; the unread color by default.
colorPrimaryForeground Text on top of colorPrimary (the unread badge).
colorBackground Panel background.
colorForeground Text.
colorMutedForeground Secondary text, such as dates.
colorMuted Subtle surfaces: hover states, skeletons. Derived by default.
colorBorder Borders.
colorRing Focus rings.
colorShadow Shadows.
colorUnread Unread dot and badge.
colorDanger Errors.
colorSuccess “New” tags.
colorWarning “Fixed” tags.
colorCodeKeyword Code highlighting: keywords (const, return).
colorCodeString Code highlighting: strings.
colorCodeComment Code highlighting: comments.
colorCodeNumber Code highlighting: numbers, attributes, variables.
colorCodeFunction Code highlighting: function and class names.
colorCodeType Code highlighting: types, built-ins, tags.
fontFamily Font.
fontSize Base font size; everything else scales from it.
borderRadius Base radius of the panel; smaller parts derive from it.
spacing Base spacing unit; paddings and gaps derive from it.
panelWidth Width of the popover panel.

Every part renders with a stable class rollout-<key> (for example .rollout-itemTitle), and appearance.elements accepts the same keys. A string is added as a class name. An object becomes a rule scoped to this widget; it supports nested & selectors ('&:hover', '&[data-unread]') and @media queries. Numbers get px, except unitless properties like opacity or zIndex.

Area Keys
Trigger root, trigger, triggerIcon, triggerBadge
Panel panel, header, headerTitle, markAllReadButton, backButton, closeButton, body
List list, item, itemImage, itemContent, itemTags, tag, itemTitle, itemDate, unreadDot, loadMoreButton
States skeleton, empty, emptyIcon, emptyTitle, emptyText, error, retryButton
Detail detail, detailImage, detailDate, detailTitle, content

State is exposed as data attributes you can style:

  • data-unread on unread items and on the trigger;
  • data-tag="new|improved|fixed" on tags;
  • data-open on the root while the popover is open;
  • data-view="list|detail" and data-side="top|bottom" on the panel.

The announcement body inside .rollout-content keeps rollout’s rendered markup: .rollout-intro, .rollout-snippet[data-tag], .rollout-hero and .rollout-tag.rollout-tag-new (or -improved, -fixed).

Option Default Description
placement bottom-end Where the popover opens: bottom, bottom-start, bottom-end, top, top-start, top-end.
offset 8 Gap between trigger and popover, in px.
showImages true Thumbnails in the list.
showTags true Category tags in the list.
showDates true Dates in the list and detail view.

The popover flips to the other side when there isn’t enough room, stays inside the viewport and caps its height to the space available.

The widget renders into the light DOM, not a shadow root, so your stylesheets and utility classes reach it.

  • Its base styles are inserted first in <head>, and every selector has the specificity of a single class. Any class of yours wins.
  • Every element starts from all: revert, so your global element rules (h2 {}, button {}) don’t leak in.
  • theme: 'simple' drops the decorative styles for fully custom designs.

Every text can be replaced; pass any subset.

feed.localization = {
locale: 'de',
title: 'Neuigkeiten',
markAllRead: 'Alle als gelesen markieren',
tags: { new: 'Neu', improved: 'Verbessert', fixed: 'Behoben' },
}
Key Default
locale The browser’s locale (used for dates).
title What’s new
trigger What’s new (accessible label of the bell)
unreadCount (count) => \${count} unread`` (badge label)
unread Unread (label of an item’s unread dot)
markAllRead Mark all as read
back Back
close Close
loadMore Load more
emptyTitle No updates yet
emptyText New features and fixes will show up here.
error Couldn’t load updates.
retry Try again
tags { new: 'New', improved: 'Improved', fixed: 'Fixed' }

Dates show as “Sep 12”, with the year added for other years.