@sesamy/sesamy-components
v2.27.5
Published
Shareable web components library built with Vite + Svelte
Keywords
Readme
🌐 sesamy-components
A shareable web components library using Vite, Svelte, Storybook and TypeScript.
This library provides typed web components that can be used with plain HTML or within any major frameworks, such as React, Angular, Vue or Svelte (see compatibility).
Table of Contents
- Installation
- Interaction tracking
- Components
- Internationalization
- Development
- Testing
- Building
- Create a New Component
Installation
You can install the package with:
npm install @sesamy/sesamy-components
# or
yarn add @sesamy/sesamy-componentsCDN Usage
You can also use the components directly via CDN:
<script type="module" src="https://unpkg.com/@sesamy/sesamy-components"></script>Per-element events
Each top-level component dispatches per-element CustomEvents (bubbles: true, composed: true) that publishers can subscribe to directly on the element — no need to poll or listen on window. TypeScript consumers get typed detail via HTMLElementEventMap augmentation exported from the package:
import '@sesamy/sesamy-components'; // ambient augmentation
const el = document.querySelector('sesamy-login')!;
el.addEventListener('sesamy:login-success', (e) => {
// e.detail is typed as { userinfo: { sub: string; email?: string; … } }
console.log(e.detail.userinfo.sub);
});The full event map and detail interfaces are exported as named types:
import type {
SesamyElementEventMap,
SesamyLoginSuccessDetail,
SesamyPaywallShownDetail,
SesamyAccessGrantedDetail,
SesamyContentUnlockedDetail
} from '@sesamy/sesamy-components';Interaction tracking
In addition to the DOM events above, the components emit first-party interactions through sesamy-js (window.sesamy.analytics.track), so they arrive with the sesamy-js context (anonymous id, user id, vendor, page) attached. This is additive: the DOM events keep firing exactly as before, and page views stay sesamy-js's responsibility — the components never emit them.
| Event | Emitted by | When | Properties |
| --------------------------- | -------------------------- | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| viewArticle | sesamy-content-container | Once per element, as soon as the container has resolved an article and knows its access state | itemSrc, publisherContentId, state (public/logged-in/unlocked/locked) |
| content_unlocked | sesamy-content-container | Alongside the sesamy:content-unlocked DOM event | itemSrc, publisherContentId, contentName |
| content_access_unresolved | sesamy-content-container | Once per element, the first time an access check fails or runs past its timeout | itemSrc, publisherContentId, reason (timeout, or the error) |
| content_access_recovered | sesamy-content-container | Once per element, when a definite answer arrives after an unresolved check | itemSrc, publisherContentId, state (granted/denied), attempts, elapsedMs |
| addToCart | sesamy-paywall | When the user picks a product and continues to checkout | itemSrc, publisherContentId, sku, purchaseOptionId, price, currency, paywallId |
Event and property names match the ones @sesamy/web-components produced, so consumers of the interactions index keep working unchanged — only context.library differs (@sesamy/sesamy-js instead of @sesamy/web-components). Each event also repeats its own name in properties.name, as the legacy library did.
Opting out
Tracking is on by default. A page opts out with the same meta tag the legacy components honoured:
<meta name="sesamy:analytics" content="false" />or at runtime:
import { disableInteractions, enableInteractions } from '@sesamy/sesamy-components';
disableInteractions(); // stop emitting; DOM events keep firing
enableInteractions(); // opt back in, overriding the meta tagresetInteractions() drops an explicit call and returns to the meta-tag default, and interactionsEnabled() reports the current state. Turning sesamy-js analytics off (analytics.enabled: false in its config) suppresses these events too, since they go through its pipeline.
Components
sesamy-login
A web component that provides authentication functionality, displaying a login button for unauthenticated users and an avatar with a dropdown menu for authenticated users.
Props/Attributes:
buttonText: Text to display on the login buttonloading: Boolean to show loading stateloggedIn: Boolean indicating if user is logged inuserAvatar: URL to the user's avatar imagelang: Language setting for the componentvariant: Appearance variant ('text', 'picture', or 'link')class: CSS classes to apply to the component
Events:
All per-element events are CustomEvents dispatched on the <sesamy-login> element with bubbles: true, composed: true, so they cross shadow DOM boundaries and can be caught via event delegation.
sesamy:login-success: Fired when the authenticated state transitions from logged-out to logged-in.detail: { userinfo: { sub: string; email?: string; name?: string; [key: string]: unknown } }.sesamy:login-error: Fired when a login attempt fails (popup closed, token exchange rejected, network error).detail: { error: { code: string; message: string; cause?: unknown } }.sesamy:logout: Fired when the authenticated state transitions from logged-in to logged-out.detail: {}.login: Legacy event, dispatched when the login action is triggered.
const el = document.querySelector('sesamy-login');
el.addEventListener('sesamy:login-success', (e) => {
console.log('welcome', e.detail.userinfo.sub);
});
el.addEventListener('sesamy:login-error', (e) => {
console.warn('login failed', e.detail.error.code, e.detail.error.message);
});
el.addEventListener('sesamy:logout', () => {
console.log('user logged out');
});See also the window-level Events enum emitted by @sesamy/sesamy-js (AUTHENTICATED, LOGOUT, …) for cross-page coordination.
Basic Usage Example:
<!-- Simple login button -->
<sesamy-login></sesamy-login>
<!-- Customized login button -->
<sesamy-login button-text="Sign In Now"></sesamy-login>Design tokens:
<sesamy-login
style="
--sesamy-font-family: Georgia; /* Sets font family, default Helvetica */
--sesamy-base-font-size: 18px; /* Base size (in px) that all component font sizes and spacing scale from, default 16px. Components never use rem, so they are unaffected by e.g. html { font-size: 62.5% } */
--sesamy-login-button-background-color: blue; /* Sets background color of the login button, default transparent */
--sesamy-login-button-text-color: green; /* Sets text color of the login button, default black */
--sesamy-login-button-border-color: pink; /* Sets border color of the login button, default black */
--sesamy-login-button-border-width: 5px; /* Sets border width of the login button, default 1px */
--sesamy-login-button-border-radius: 20px; /* Sets border radius of the login button, default 6px at a 16px base size */
--sesamy-login-button-font-weight: 100; /* Sets font weight of the login button, default 700 */
--sesamy-login-popup-width: 400px; /* Sets width of the login popup, default 288px */
--sesamy-login-popup-bgcolor: green; /* Sets background color of the login popup, default white */
--sesamy-login-popup-textcolor: pink; /* Sets text color of the login popup, default black */
--sesamy-login-popup-border-color: red; /* Sets border color of the login popup, default #e5e7eb */
--sesamy-login-popup-border-width: 5px; /* Sets border width of the login popup, default 1px */
--sesamy-login-popup-border-radius: 20px; /* Sets border radius of the login popup, default 2px */
--sesamy-login-popup-zindex: 100; /* Sets z-index of the login popup, default 10 */
"
></sesamy-login>Slots
The sesamy-login component provides several slots for customizing its appearance and behavior:
button-text
- Purpose: Replaces the default login button text.
- Behavior: Content in this slot will be rendered as the main text of the login button, replacing the default (localized) "login" text.
- Example:
<sesamy-login> <span slot="button-text">Sign in with Email</span> </sesamy-login>
button-text-prefix
- Purpose: Inserts content before the login button text.
- Behavior: Content in this slot will appear before the main button text, useful for adding icons or labels.
- Example:
<sesamy-login> <span slot="button-text-prefix">🔒</span> </sesamy-login>
button-text-suffix
- Purpose: Inserts content after the login button text.
- Behavior: Content in this slot will appear after the main button text, useful for adding icons or additional info.
- Example:
<sesamy-login> <span slot="button-text-suffix">→</span> </sesamy-login>
avatar
- Purpose: Replaces the default avatar shown when logged in.
- Behavior: Content in this slot will be rendered instead of the default avatar image/button when the user is authenticated.
- Example:
<sesamy-login> <img slot="avatar" src="/my-avatar.png" alt="User avatar" style="width:32px;height:32px;border-radius:50%" /> </sesamy-login>
popup-menu
- Purpose: Replaces the default popup menu shown when clicking the avatar.
- Behavior: Content in this slot will be rendered instead of the default menu (email/account/logout) when the user is authenticated and opens the menu.
- Example:
<sesamy-login> <div slot="popup-menu"> <a href="/profile">Profile</a> <a href="/logout">Logout</a> </div> </sesamy-login>
Note:
- All slots are optional. If not provided, the component will render its default content for each area.
sesamy-content-container
A web component that controls access to content based on user authentication and entitlements, with support for different content locking mechanisms.
Props/Attributes:
item-src: URL of the content itempass: Semicolon-separated list of pass IDs that grant accessaccess-level: Access level required ('public', 'logged-in', or 'entitlement')publisher-content-id: ID of the content from the publisherlock-mode: Content locking mechanism ('embed', 'encode', 'signedUrl', 'event', or 'proxy')locked-content-selector: CSS selector for locked content when using signed URLs
Events:
All per-element events bubble and are composed (cross shadow roots).
sesamy:content-unlocked: Fired when gated content is decrypted and rendered into the element.detail: { contentName: string }— matches the element'sdata-dca-content-nameattribute when present, otherwise falls back to the resolvedpublisher-content-id.sesamyUnlocked: Legacy event, still dispatched alongsidesesamy:content-unlocked.detail: { publisherContentId, itemSrc }.
The container also emits the viewArticle and content_unlocked interactions through sesamy-js — see Interaction tracking.
When access cannot be resolved:
The container only removes the content slot on a definite denial. If the access check fails (no token, a network error) or does not answer within 10 seconds, the state is unknown and the preview slot is shown, with the article left in place. The container then checks again by itself after 1s, 3s and 10s, then every 30s, and straight away when the browser comes back online or the tab becomes visible again, until it gets an answer. A slow answer that arrives after its check timed out is still used. Once an article is shown, a later check that cannot be resolved does not hide it. content_access_unresolved and content_access_recovered report how often this happens.
const el = document.querySelector('sesamy-content-container');
el.addEventListener('sesamy:content-unlocked', (e) => {
analytics.track('content_unlocked', { name: e.detail.contentName });
});Basic Usage Example:
<!-- Basic content container with preview and locked content -->
<sesamy-content-container item-src="https://example.com/article.html">
<div slot="preview">This is a preview visible to everyone</div>
<div slot="content">This is the full content for authorized users</div>
</sesamy-content-container>
<!-- Content visible only to logged-in users -->
<sesamy-content-container access-level="logged-in">
<div slot="preview">Please log in to view this content</div>
<div slot="content">This content is for logged-in users only</div>
</sesamy-content-container>sesamy-paywall
A web component that displays a paywall for content, loading paywall settings from a remote URL and supporting different templates (Article, Boxes, Login).
Props/Attributes:
settings-url: URL to fetch paywall settings (required)item-src: URL of the content itemprice: Price of the contentcurrency: Currency code for the priceredirect-url: URL to redirect after purchaseutm-source,utm-medium,utm-campaign,utm-term,utm-content: UTM parameters for trackingpass: Pass ID for access
Post-purchase redirect:
Where the visitor lands after buying is resolved when they go to checkout, first usable value winning:
redirectUrlon the chosen subscription option (from the paywall settings)settings.redirectUrlon the paywall (the paywall-wide default)- the
redirect-urlattribute on the element - the page the visitor is on
Only an absolute URL counts as usable. Empty, whitespace-only and unparseable
values fall through to the next level (the last with a console.warn) instead
of sending a broken redirect-url to checkout. Single purchases have no
per-option redirect, so they start at level 2; subscription options of type URL
link straight out and never reach checkout at all.
Events:
Per-element events dispatched directly on the <sesamy-paywall> element (bubble, composed):
sesamy:paywall-shown: Fired once per visible mount when the paywall becomes visible to the user.detail: { reason: 'unauthenticated' | 'no-entitlement' | 'consent-required' | string }.sesamy:paywall-dismissed: Fired when the user dismisses the paywall without purchasing (element is removed while it was shown and access was never granted).detail: {}.sesamy:access-granted: Fired when the paywall confirms the user has access and hides itself.detail: { scopes: string[] }— the entitlement scopes / passes that granted access.
const el = document.querySelector('sesamy-paywall');
el.addEventListener('sesamy:paywall-shown', (e) => {
console.log('paywall visible; reason:', e.detail.reason);
});
el.addEventListener('sesamy:access-granted', (e) => {
console.log('granted scopes:', e.detail.scopes);
});Legacy bus events (emitted on window via api.events.emit, unchanged):
sesamyPaywallAccessChecked: Emitted after access check, with{ hasAccess, paywallId, articleUrl, passes }indetail.sesamyPaywallProductSelected: Emitted when a product/subscription is selected and the continue button is pressed, with{ product, checkoutId, paywallId }indetail.sesamyPaywallCheckoutRedirect: Emitted before redirecting to checkout, with{ checkout, paywallId, paymentMethod }indetail.
The paywall also emits the addToCart interaction through sesamy-js when the user continues to checkout — see Interaction tracking.
Slots:
headline: Replaces the headline from the paywall settings (e.g., placement-specific copy)below-headline: Content rendered below the paywall headline (e.g., additional info, custom elements)features: Content rendered in the features section of the paywall (e.g., feature list, benefits)login-button-text: Replaces the text of the "already subscribing" login button
Basic Usage Example:
<!-- Article paywall -->
<sesamy-paywall
settings-url="https://api.example.com/paywall/settings"
item-src="https://example.com/article"
price="99"
currency="USD"
>
<div slot="features">✔️ Unlimited access<br />✔️ Cancel anytime</div>
</sesamy-paywall>
<!-- Login paywall with below-headline slot -->
<sesamy-paywall settings-url="https://api.example.com/paywall/login-settings">
<div slot="below-headline">Additional content below headline</div>
</sesamy-paywall>Slots
The sesamy-paywall component provides four slots for customization:
headline
- Purpose: Replaces the headline configured in the paywall settings.
- Behavior: When you provide content in the
headlineslot, it is rendered in place of the stored headline, with the headline's own styling (size, weight, max width). When you leave it empty, the headline from the paywall settings is shown. Use this to vary the copy per placement (e.g., article vs. front page) while reusing one paywall configuration. - Availability: All templates (
ARTICLE,BOXES,LOGIN). - Example:
<sesamy-paywall settings-url="https://api.example.com/paywall/settings"> <span slot="headline">Read the full investigation</span> </sesamy-paywall>
below-headline
- Purpose: Inserts custom content directly below the paywall headline.
- Behavior: The content you provide in this slot will be rendered in addition to the default paywall content, immediately below the headline. Use this for adding extra information, banners, or custom elements.
- Example:
<sesamy-paywall settings-url="https://api.example.com/paywall/login-settings"> <div slot="below-headline">Special offer for new users!</div> </sesamy-paywall>
features
- Purpose: Replaces the default features section of the paywall.
- Behavior: When you provide content in the
featuresslot, it will completely replace the built-in features list or section. Use this slot to fully customize the list of benefits, features, or selling points shown to the user. Slotted content is static: it does not switch when the visitor selects a different subscription option the way the built-in list does. - Availability: Only the
ARTICLEtemplate renders a paywall-wide features list. TheBOXEStemplate shows features inside each box, so the slot has no effect there. - Example:
<sesamy-paywall settings-url="https://api.example.com/paywall/settings"> <div slot="features"> <ul> <li>✔️ Unlimited access</li> <li>✔️ Cancel anytime</li> <li>✔️ Exclusive articles</li> </ul> </div> </sesamy-paywall>
login-button-text
- Purpose: Replaces the text of the "already subscribing" login button the paywall renders above the headline.
- Behavior: The paywall embeds a nested
sesamy-logincomponent and forwards this slot into that component's ownbutton-textslot. When you leave it empty, the default translated text (Already subscribing? Login) is used. - Availability: Only the
ARTICLEtemplate renders this login button. TheBOXEStemplate hides it, and theLOGINtemplate doesn't embed asesamy-loginat all, so the slot has no effect there. - Styling: Inside the paywall this button is rendered borderless and underlined (4px offset), at the base font size (16px by default) rather than the 14px a standalone
sesamy-loginuses, and it fades to 80% opacity on hover. Set--sesamy-login-button-text-sizeon the paywall to override the size; the other--sesamy-login-button-*variables apply as usual. - Example:
<sesamy-paywall settings-url="https://api.example.com/paywall/settings"> <span slot="login-button-text">Already a subscriber? Sign in</span> </sesamy-paywall>
Note:
- The
below-headlineslot adds to the paywall, while theheadline,featuresandlogin-button-textslots replace the default content entirely.
sesamy-visibility
A simple web component that conditionally renders content based on user authentication status.
Basic Usage Example:
<sesamy-visibility>
<div slot="logged-in">This content is only visible when logged in</div>
<div slot="not-logged-in">This content is only visible when not logged in</div>
</sesamy-visibility>sesamy-avatar
A web component that displays a user avatar image with configurable size and loading state.
Props/Attributes:
src: URL of the avatar imagealt: Alt text for the imagesize: Size of the avatar ('sm', 'md', or 'lg')loading: Boolean to show loading state
Basic Usage Example:
<!-- Default avatar -->
<sesamy-avatar src="https://example.com/user.jpg" alt="User avatar"></sesamy-avatar>
<!-- Large avatar with loading state -->
<sesamy-avatar src="https://example.com/user.jpg" size="lg" loading></sesamy-avatar>sesamy-button
A customizable button web component with multiple variants and sizes.
Props/Attributes:
variant: Button style variant ('primary', 'secondary', or 'tertiary')size: Button size ('sm', 'md', or 'lg')loading: Boolean to show loading spinnerdisabled: Boolean to disable the buttonhref: URL if the button should act as a linktype: Button type ('button', 'submit', etc.)class: Additional CSS classes
Basic Usage Example:
<!-- Primary button -->
<sesamy-button variant="primary">Subscribe</sesamy-button>
<!-- Secondary button with loading state -->
<sesamy-button variant="secondary" loading>Processing...</sesamy-button>
<!-- Button as a link -->
<sesamy-button href="/checkout" variant="primary">Go to Checkout</sesamy-button>sesamy-login-menu-item
A web component for individual menu items in the login dropdown menu. Can be used to customize the logged-in user menu.
Props/Attributes:
type: Type of menu item ('EMAIL', 'ACCOUNT', 'LOGOUT', or 'LINK')href: URL for link type itemstarget: Link target attribute (e.g., '_blank')text: Custom text for the menu item
Basic Usage Example:
<!-- Account link -->
<sesamy-login-menu-item type="ACCOUNT"></sesamy-login-menu-item>
<!-- Custom link -->
<sesamy-login-menu-item
type="LINK"
href="https://example.com/settings"
text="Settings"
></sesamy-login-menu-item>
<!-- Logout button -->
<sesamy-login-menu-item type="LOGOUT"></sesamy-login-menu-item>Internationalization
The components support multiple languages out of the box. Supported languages:
- 🇬🇧 English (en)
- 🇸🇪 Swedish (sv)
- 🇳🇴 Norwegian Bokmål (nb)
- 🇩🇰 Danish (da)
- 🇫🇮 Finnish (fi)
- 🇮🇹 Italian (it)
- 🇵🇱 Polish (pl)
- 🇨🇿 Czech (cs)
Set the language using the lang attribute on supported components:
<sesamy-login lang="sv"></sesamy-login>Development
Your components source code lives in packages/lib/ folder. Only components with the .wc.svelte extension will be exported as web components and available in your library. This means that you can also use regular Svelte components with the .svelte extension as child components for your implementation details.
You can add additional components by adding them to the packages/lib/src folder and editing packages/lib/index.ts.
Available Scripts
| Command | Description |
| ------------------------ | ----------------------------------------- |
| yarn dev | Start the development server |
| yarn build | Build both library and demo |
| yarn build:lib | Build the library only |
| yarn storybook | Start Storybook for component development |
| yarn build:storybook | Build Storybook for deployment |
| yarn test | Run Playwright tests |
| yarn check | Run Svelte type checking |
| yarn pull-translations | Pull latest translations from i18nexus |
Testing your components
You can start a development server with:
yarn devThen open your browser to localhost:5173.
This will build the demo application located in the packages/demo/ folder, in which you can use and test your web components during development.
Storybook
For component development and visual testing, use Storybook:
yarn storybookThen open your browser to localhost:6006.
End-to-End Tests
Run Playwright tests with:
yarn testFor running E2E tests in Docker (ensuring consistent snapshots):
yarn e2eTo update snapshots:
yarn e2e:snapshotsUsing the built web components with the demo app
The demo application is provided for development and testing of your components, that's why it imports the .svelte files from the packages/lib/ folder directly by default.
If you prefer, you can import the built web components from the dist/ folder instead, by editing packages/demo/src/App.svelte and replacing the import statement with import '../../../dist/lib'; if you have the bundleComponents option enabled.
You'll also have to make sure to run the yarn build script to generate the dist/lib/ folder first.
Building the library
The command yarn build will create the web components library in the dist/lib/ folder. It creates both an ES module (dist/lib/<your-lib>.js) suitable for bundler (non-minified), a minified ES module (dist/lib/<your-lib>.min.js) and a regular UMD script (dist/lib/<your-lib>.umd.js).
The build is automatically called when executing yarn publish to distribute your library, thanks to the prepublishOnly script entry in package.json.
Notes and limitations
This template does not provide any web components polyfills for older browsers support. It's usually best to leave that task to the host application, hence why they're left out.
Props
Props on a .wc.svelte component are exposed both as properties of the DOM element and, where possible, as attributes. The attribute name defaults to the prop name lowercased, so a camelCase prop like buttonText is only settable from markup as buttontext — name each prop exactly as the attribute should be written instead: lowercase for a single word, kebab-case for several. Rename the kebab-case keys to camelCase locals in the $props() destructure so the component body stays readable:
<!-- MyComponent.wc.svelte -->
<svelte:options customElement={{ tag: 'my-component' }} />
<script lang="ts">
let { myvalue = 'Default', 'item-src': itemSrc = '' } = $props();
</script><my-component myvalue="Hello" item-src="https://example.com/article"></my-component>Values set through an attribute always arrive as strings. A prop that needs another type has to declare it in the customElement.props config ({ type: 'Number' | 'Boolean' | 'Array' | 'Object' }), or be assigned as a DOM property rather than an attribute.
See ContentContainer.wc.svelte for the same pattern with the prop types declared in types.ts.
Events
Between plain Svelte components you'd signal the parent with a callback prop or createEventDispatcher; neither travels well across the custom element boundary. createEventDispatcher events are never re-dispatched on the element, and a callback prop only reaches the component if the consumer assigns it as a DOM property (el.onLogin = fn) — it can't be wired up from plain HTML. So a .wc.svelte component talks to the host page by dispatching a real DOM CustomEvent, which consumers listen for with addEventListener. Inside a component compiled as a custom element the $host() rune returns that element — under Svelte 5 this is the supported API rather than a workaround.
Here's an example:
<!-- MyComponent.wc.svelte -->
<svelte:options customElement={{ tag: 'my-component' }} />
<script>
// example function for dispatching events
const dispatchEvent = (name, detail) =>
$host().dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
</script>
<button onclick={() => dispatchEvent('test', 'Hello!')}>Click to dispatch event</button>Both flags matter. Without bubbles: true the event only reaches a listener bound directly to the element, so event delegation on a container won't see it; without composed: true it can't escape an enclosing shadow root, which bites as soon as the element is nested inside another component's shadow DOM. The components in this repo get both from the typed dispatchSesamyEvent helper in packages/lib/src/events.ts, which also type-checks detail against SesamyElementEventMap — prefer it over a raw CustomEvent.
Create a new component
These are the files needed to create a new component:
- Add the
my-component.wc.sveltefile in thepackages/lib/srcfolder. - Add the class in the
packages/lib/src/sesamy-components.d.tsfile to get the types exported. - Add the component to the
packages/lib/index.tsfile to export it. - Add a story in the
packages/lib/src/storiesfolder.
License
This project is proprietary software by Sesamy.
