Skip to content

UI Components

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.

<script type="module">
// 1. Initialize the SDK first
const { init } = await import('@latch/sdk');
init({
apiKey: 'pk_...',
apiUrl: 'https://latch-api.v3ck.com',
});
// 2. Then import UI components — triggers branding fetch
await import('@latch/ui');
</script>

Or import individual components:

import { LatchAuthModal, LatchUserMenu } from '@latch/ui';

Standalone email + password login form.

<latch-login-form
heading="Sign in"
button-text="Sign in"
show-register="true"
></latch-login-form>
AttributeDefaultDescription
heading"Sign in"Form heading text
button-text"Sign in"Submit button label
show-register"true"Show “Create account” link
EventDetailDescription
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.

<latch-register-form
heading="Create account"
show-name="true"
></latch-register-form>
AttributeDefaultDescription
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
EventDetailDescription
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()">
Sign in
</button>
AttributeDefaultDescription
open—If present, the modal is visible
tab"login"Initial tab: "login" or "register"
headingAutoHeading (defaults based on active tab)
show-name"true"Show name field in register tab
MethodDescription
.show(tab?)Open the modal. Optionally set the starting tab.
.hide()Close the modal.
EventDetailDescription
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.

<latch-user-menu
login-text="Sign in"
show-account-link="true"
></latch-user-menu>
AttributeDefaultDescription
login-text"Sign in"Button text when logged out
show-account-link"true"Show “Account” link in dropdown
avatar"true"Show initials avatar
EventDetailDescription
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.

<latch-paywall-modal
message="Subscribe to continue reading"
description="Get unlimited access to all content."
cta-text="Subscribe"
product-ids="prod_abc123,prod_def456"
></latch-paywall-modal>
AttributeDefaultDescription
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
EventDetailDescription
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".

<latch-sticky-bar
open
position="bottom"
message="Subscribe to keep reading"
cta-text="Subscribe"
product-ids="prod_abc123"
></latch-sticky-bar>
AttributeDefaultDescription
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
MethodDescription
.show()Sets open
.hide()Removes open
EventDetailDescription
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().

<latch-inline-paywall
open
message="Subscribe to continue reading"
description="Unlimited access for $5/month."
cta-text="Subscribe"
product-ids="prod_abc123"
></latch-inline-paywall>
AttributeDefaultDescription
open—If present, the card is visible
messageBranding paywallHeading or "Subscribe to continue reading"Heading text
descriptionBranding paywallDescriptionSecondary description
cta-textBranding paywallCtaText or "Subscribe"CTA button label
product-ids—Comma-separated product IDs
logo-urlBranding logoUrlOptional logo image URL
MethodDescription
.show()Sets open
.hide()Removes open
EventDetailDescription
latch:subscribe{ productIds }CTA button clicked

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:

  1. If #latch-lock exists in the page, it hides every sibling element after it.
  2. Otherwise, if target-selector is set, it truncates that container after roughly 3 paragraphs.
  3. Otherwise, it auto-detects <article>, <main>, .post-content, or [role="main"] and truncates the first match.
<!-- Place a marker where the lock should start -->
<article>
<p>Free preview paragraph 1...</p>
<p>Free preview paragraph 2...</p>
<div id="latch-lock"></div>
<p>Premium content (will be hidden)...</p>
</article>
<latch-content-lock
open
message="Subscribe to continue reading"
cta-text="Subscribe"
product-ids="prod_abc123"
></latch-content-lock>
AttributeDefaultDescription
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
MethodDescription
.show()Sets open and truncates content
.hide()Removes open and restores hidden elements
EventDetailDescription
latch:subscribe{ productIds }CTA button clicked

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").

<latch-newsletter-wall
open
rule-id="rule_abc123"
message="Subscribe to our newsletter"
description="Get the weekly digest in your inbox."
cta-text="Subscribe"
></latch-newsletter-wall>
AttributeDefaultDescription
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()
MethodDescription
.show()Resets state and sets open
.hide()Removes open
EventDetailDescription
latch:newsletter:submit{ email }Email submitted successfully

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").

<latch-data-wall
open
rule-id="rule_abc123"
message="Tell us about yourself"
description="A few details and you're in."
cta-text="Submit"
fields='[
{ "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+"] }
]'
></latch-data-wall>
AttributeDefaultDescription
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";
required: boolean;
options?: string[]; // only for type: "select"
}
MethodDescription
.show()Resets state and sets open
.hide()Removes open
EventDetailDescription
latch:data:submit{ fields }Form submitted successfully (fields is the values map)

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.

<latch-pricing-table
heading="Choose your plan"
description="Cancel anytime. No commitment."
></latch-pricing-table>
AttributeDefaultDescription
heading"Choose your plan"Section heading
description—Subheading text
columns"auto"Grid columns: "auto", "1", "2", "3"
show-free"false"Show free-tier prices
EventDetailDescription
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>
AttributeDefaultDescription
heading"Your account"Panel heading
show-portal"true"Show “Manage subscription” button
EventDetailDescription
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-auth-modal {
--latch-primary: #e74c3c;
--latch-primary-hover: #c0392b;
--latch-radius: 16px;
}
/* Override globally */
:root {
--latch-primary: #2ecc71;
--latch-font: 'Georgia', serif;
}
PropertyDefaultDescription
--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:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My Publication</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 24px; }
header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 32px; }
/* Theme the components to match your brand */
:root { --latch-primary: #2563eb; }
</style>
</head>
<body>
<header>
<h1>My Publication</h1>
<latch-user-menu></latch-user-menu>
</header>
<!-- Auth modal (hidden by default) -->
<latch-auth-modal id="auth-modal"></latch-auth-modal>
<!-- Article content -->
<article>
<h2>Premium Article Title</h2>
<p>Article content goes here...</p>
</article>
<!-- Pricing section -->
<latch-pricing-table heading="Subscribe today"></latch-pricing-table>
<!-- Account section -->
<latch-account-panel></latch-account-panel>
<script type="module">
import { init, checkAccess, onAuthChange } from '@latch/sdk';
import '@latch/ui';
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();
if (!result.granted) {
// 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');
}
});
</script>
</body>
</html>

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.