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 oncedefineFeedElement('my-feed') registers it under another tag name.
Attributes
Section titled “Attributes”| 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. |
Properties
Section titled “Properties”| 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.
Methods and events
Section titled “Methods and events”open(),close(),toggle()control the popover.- The
openchangeevent fires when it opens or closes;event.detail.openis the new state.
feed.addEventListener('openchange', (event) => { if (event.detail.open) analytics.track('Opened what’s new')})Your own trigger
Section titled “Your own trigger”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 = rolloutrollout.subscribe(({ unreadCount }) => (badge.textContent = unreadCount || ''))Without custom elements
Section titled “Without custom elements”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.
Appearance
Section titled “Appearance”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 },}Themes
Section titled “Themes”| 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.
Variables
Section titled “Variables”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. |
Elements
Section titled “Elements”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-unreadon unread items and on the trigger;data-tag="new|improved|fixed"on tags;data-openon the root while the popover is open;data-view="list|detail"anddata-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).
Layout
Section titled “Layout”| 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.
How styles apply
Section titled “How styles apply”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.
Localization
Section titled “Localization”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.