The @latch/ui package provides drop-in Web Components that work in any HTML page — no React, Vue, or build step required. Each component uses Shadow DOM for style isolation and CSS custom properties for theming.
Initialize the SDK before importing the UI package. This ensures the branding configuration is fetched automatically when components load.
// 1. Initialize the SDK first
const { init } = await import ( ' @latch/sdk ' );
apiUrl: ' https://latch-api.v3ck.com ' ,
// 2. Then import UI components — triggers branding fetch
await import ( ' @latch/ui ' );
Or import individual components:
import { LatchAuthModal, LatchUserMenu } from ' @latch/ui ' ;
Tip
Automatic branding : When @latch/ui is imported, it fetches your publication’s branding configuration from the API. Colors, logo, fonts, and custom text configured in Settings > Branding in the dashboard are applied automatically. See the Branding docs for details.
Standalone email + password login form.
Attribute Default Description heading"Sign in"Form heading text button-text"Sign in"Submit button label show-register"true"Show “Create account” link
Event Detail Description latch:login{ customer }Successful login latch:login:error{ error }Login failure latch:show-register— “Create account” link clicked
Standalone registration form with optional name field.
Attribute Default Description heading"Create account"Form heading button-text"Create account"Submit button label show-login"true"Show “Sign in” link show-name"true"Show the optional name field
Event Detail Description latch:register{ customer }Successful registration latch:register:error{ error }Registration failure latch:show-login— “Sign in” link clicked
Combined login/register modal with tab switching. Show it by setting the open attribute or calling .show().
< latch-auth-modal id = " auth " ></ latch-auth-modal >
< button onclick = " document . getElementById ( ' auth ' ) . show () " >
Attribute Default Description open— If present, the modal is visible tab"login"Initial tab: "login" or "register" headingAuto Heading (defaults based on active tab) show-name"true"Show name field in register tab
Method Description .show(tab?)Open the modal. Optionally set the starting tab. .hide()Close the modal.
Event Detail Description latch:login{ customer }Successful login latch:register{ customer }Successful registration latch:auth-modal:close— Modal was closed
Shows a “Sign in” button when logged out, or a user dropdown with avatar and sign-out when logged in. Automatically updates when auth state changes.
Attribute Default Description login-text"Sign in"Button text when logged out show-account-link"true"Show “Account” link in dropdown avatar"true"Show initials avatar
Event Detail Description latch:show-login— Sign in button clicked latch:show-account— Account link clicked latch:logout— Logout completed
A styled paywall overlay. Trigger it from an access check result or manually.
message = " Subscribe to continue reading "
description = " Get unlimited access to all content. "
product-ids = " prod_abc123,prod_def456 "
Attribute Default Description open— If present, the modal is visible message"Subscribe to continue reading"Paywall heading description"Get unlimited access..."Secondary text cta-text"Subscribe"CTA button label dismiss-text"Not now"Dismiss button label dismissible"true"Show dismiss button lock-icon"true"Show lock icon product-ids— Comma-separated product IDs meter-remaining— If set, shows meter info
Event Detail Description latch:subscribe{ productIds }CTA button clicked latch:dismiss— Dismiss button clicked
A non-intrusive sticky bar pinned to the top or bottom of the viewport. Good for soft prompts that don’t block reading. Use when a paywall rule’s action.template is "sticky-bar" or "bottom-bar".
message = " Subscribe to keep reading "
product-ids = " prod_abc123 "
Attribute Default Description open— If present, the bar is visible position"bottom""top" or "bottom"message"Subscribe to continue reading"Bar message text cta-text"Subscribe"CTA button label dismiss-text"✕"Dismiss button label dismissible"true"Whether the bar can be dismissed product-ids— Comma-separated product IDs
Method Description .show()Sets open .hide()Removes open
Event Detail Description latch:subscribe{ productIds }CTA button clicked latch:dismiss— Bar dismissed
A small in-flow paywall card that renders inside the parent layout instead of an overlay. Use when a paywall rule’s action.template is "inline" and you’ve configured paywallSelector on Latch.init().
message = " Subscribe to continue reading "
description = " Unlimited access for $5/month. "
product-ids = " prod_abc123 "
Attribute Default Description open— If present, the card is visible messageBranding paywallHeading or "Subscribe to continue reading" Heading text descriptionBranding paywallDescription Secondary description cta-textBranding paywallCtaText or "Subscribe" CTA button label product-ids— Comma-separated product IDs logo-urlBranding logoUrl Optional logo image URL
Method Description .show()Sets open .hide()Removes open
Event Detail Description latch:subscribe{ productIds }CTA button clicked
Tip
Latch.showPaywall(result) mounts this into the element matched by config.paywallSelector (or document.body as fallback) when action.template is "inline". Heading, description, CTA, and logo fall back to your publication branding when attributes aren’t set.
Truncates article content with a gradient fade and a paywall overlay below it. Use when a paywall rule’s action.template is "inline-lock".
The component decides what to truncate using three strategies, in order:
If #latch-lock exists in the page, it hides every sibling element after it.
Otherwise, if target-selector is set, it truncates that container after roughly 3 paragraphs.
Otherwise, it auto-detects <article>, <main>, .post-content, or [role="main"] and truncates the first match.
<!-- Place a marker where the lock should start -->
< p > Free preview paragraph 1... </ p >
< p > Free preview paragraph 2... </ p >
< div id = " latch-lock " ></ div >
< p > Premium content (will be hidden)... </ p >
message = " Subscribe to continue reading "
product-ids = " prod_abc123 "
Attribute Default Description open— If present, the lock is active (content truncated + overlay shown) message"Subscribe to continue reading"Overlay message text cta-text"Subscribe"CTA button label product-ids— Comma-separated product IDs target-selector— Optional CSS selector for the content container
Method Description .show()Sets open and truncates content .hide()Removes open and restores hidden elements
Event Detail Description latch:subscribe{ productIds }CTA button clicked
Host CSS contract
<latch-content-lock> owns truncation while it’s open. If your page already ships a hand-rolled preview-fade (e.g. .is-preview { max-height: 320px; overflow: hidden }), the two layouts will collide and the overlay ends up clipped inside the host’s clamp.
While the lock is open, the component sets data-latch-lock-active on <html>. Either drop your host-side fade entirely and let the SDK own it, or scope it so it disengages while the lock is active:
html :not ([ data-latch-lock-active ]) .article-body.is-preview {
Modal overlay with an email input that lets readers exchange their email for content access. Use this for newsletter-type paywall rules (paywallRule.type === "newsletter").
message = " Subscribe to our newsletter "
description = " Get the weekly digest in your inbox. "
></ latch-newsletter-wall >
Attribute Default Description open— If present, the wall is visible message"Subscribe to continue reading"Heading text description— Optional description below the heading cta-text"Subscribe"Submit button label rule-id— Required. Paywall rule ID; passed to the SDK’s submitNewsletter()
Method Description .show()Resets state and sets open .hide()Removes open
Event Detail Description latch:newsletter:submit{ email }Email submitted successfully
Tip
Latch.showPaywall(result) routes here automatically when paywallRule.type === "newsletter", regardless of action.template. The component calls the SDK’s submitNewsletter(email, ruleId) bridge on submit, then auto-dismisses after a short success state.
Modal overlay with a dynamic form that gates content behind custom data collection (e.g. role, company size, country). Used for data-wall paywall rules (paywallRule.type === "data").
message = " Tell us about yourself "
description = " A few details and you're in. "
{ "name": "role", "label": "Your role", "type": "text", "required": true },
{ "name": "size", "label": "Company size", "type": "select", "required": true,
"options": ["1-10", "11-50", "51-200", "200+"] }
Attribute Default Description open— If present, the wall is visible message"Complete the form to continue reading"Heading text description— Optional description below the heading cta-text"Submit"Submit button label rule-id— Required. Paywall rule ID; passed to the SDK’s submitDataWall()fields[]JSON-encoded array of field definitions (see below)
Each field in fields has the shape:
name: string; // form field name
label: string; // visible label
type: " text " | " email " | " select " | " number " ;
options ?: string[]; // only for type: "select"
Method Description .show()Resets state and sets open .hide()Removes open
Event Detail Description latch:data:submit{ fields }Form submitted successfully (fields is the values map)
Tip
Latch.showPaywall(result) routes here automatically when paywallRule.type === "data". The component calls the SDK’s submitDataWall(values, ruleId) bridge on submit, then auto-dismisses after a short success state.
Fetches active products and prices from the API and renders a pricing grid. Each price has a checkout button that automatically redirects authenticated customers to Stripe Checkout.
heading = " Choose your plan "
description = " Cancel anytime. No commitment. "
Attribute Default Description heading"Choose your plan"Section heading description— Subheading text columns"auto"Grid columns: "auto", "1", "2", "3" show-free"false"Show free-tier prices
Event Detail Description latch:checkout{ priceId, productId, productName }A price was selected latch:pricing:loaded{ products }Products loaded from API latch:pricing:error{ error }Failed to load products
Shows the logged-in customer’s profile, subscription status, and a “Manage subscription” button that opens the Stripe Customer Portal. When logged out, shows a sign-in prompt.
< latch-account-panel heading = " Your account " ></ latch-account-panel >
Attribute Default Description heading"Your account"Panel heading show-portal"true"Show “Manage subscription” button
Event Detail Description latch:show-login— Sign in prompt clicked latch:logout— Logout completed
All components support theming via CSS custom properties. Set them on the component element or any ancestor:
/* Override on a specific component */
--latch-primary : # e74c3c ;
--latch-primary-hover : # c0392b ;
--latch-primary : # 2ecc71 ;
--latch-font : ' Georgia ' , serif ;
Property Default Description --latch-primary#6C5CE7Primary brand color --latch-primary-hover#5A4BD1Primary hover color --latch-primary-text#ffffffText color on primary backgrounds --latch-bg#ffffffComponent background --latch-bg-secondary#f8f9faSecondary background --latch-bg-hover#f1f3f5Hover background --latch-text#0f172aPrimary text color --latch-text-secondary#64748bSecondary text color --latch-text-muted#94a3b8Muted text color --latch-border#e2e8f0Border color --latch-border-focus#6C5CE7Focus ring color --latch-error#ef4444Error color --latch-success#22c55eSuccess color --latch-radius8pxBorder radius --latch-radius-lg12pxLarge border radius --latch-fontsystem-uiFont family --latch-shadow— Box shadow --latch-overlayrgba(0,0,0,0.5)Modal overlay color
A complete publisher page with auth, paywalls, and pricing:
< title > My Publication </ title >
body { font-family : system-ui , sans-serif ; max-width : 800 px ; margin : 0 auto ; padding : 24 px ; }
header { display : flex ; justify-content : space-between ; align-items : center ; margin-bottom : 32 px ; }
/* Theme the components to match your brand */
:root { --latch-primary : # 2563eb ; }
< latch-user-menu ></ latch-user-menu >
<!-- Auth modal (hidden by default) -->
< latch-auth-modal id = " auth-modal " ></ latch-auth-modal >
< h2 > Premium Article Title </ h2 >
< p > Article content goes here... </ p >
< latch-pricing-table heading = " Subscribe today " ></ latch-pricing-table >
< latch-account-panel ></ latch-account-panel >
import { init, checkAccess, onAuthChange } from ' @latch/sdk ' ;
init ({ apiKey: ' pk_... ' , apiUrl: ' https://latch-api.v3ck.com ' });
// Wire up user menu -> auth modal
document . addEventListener ( ' latch:show-login ' , () => {
document . getElementById ( ' auth-modal ' ) . show ( ' login ' );
// Re-check access after login
document . addEventListener ( ' latch:login ' , async () => {
const result = await checkAccess ();
// Show paywall or redirect
// Wire up pricing table -> auth modal (if not logged in)
document . addEventListener ( ' latch:checkout ' , ( e ) => {
// If not authenticated, the pricing table won't auto-redirect.
// Open the auth modal so the customer can log in first.
const { isAuthenticated } = await import ( ' @latch/sdk ' );
if ( ! isAuthenticated ()) {
document . getElementById ( ' auth-modal ' ) . show ( ' register ' );
The components use Web Components (Custom Elements v1 + Shadow DOM v1), supported in all modern browsers:
Chrome/Edge 54+
Firefox 63+
Safari 10.1+
No polyfills are needed for any browser released after 2018.
When integrating Latch on a customer site it can be hard to tell which DOM elements are Latch components vs. host markup. A toggleable debug border helps:
window . LATCH_DEBUG = true ;
window . dispatchEvent ( new Event ( " latch:debug-toggle " ));
Every Latch component on the page gets an animated gradient pulse around its bounding box, and logs [latch-debug] <tag-name> mounted on connect / unmounted on disconnect. The effect is non-layout-affecting (uses box-shadow, not border).
To turn it off:
window . LATCH_DEBUG = false ;
window . dispatchEvent ( new Event ( " latch:debug-toggle " ));
Hidden by default — zero visual impact when the flag is unset.