Skip to main content
Home pageGetting StartedUsage

Usage

Vanilla HTML​

No build tool required:

<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="node_modules/@latty-ds/tokens/dist/tokens.css" />
<link rel="stylesheet" href="node_modules/@latty-ds/tokens/dist/semantic.css" />
</head>
<body>
<lt-button variant="primary" icon-start="save">Save changes</lt-button>

<script type="module">
import '@latty-ds/web';
</script>

</body>
</html>

With a bundler (Vite, webpack, etc.)​

entry.js
import '@latty-ds/tokens/tokens.css';
import '@latty-ds/tokens/semantic.css';
import '@latty-ds/web';
<lt-button variant="primary">Click me</lt-button>

<lt-textfield
label="Email address"
type="email"
required
helper-text="We'll never share your email"

> </lt-textfield>

React​

Use the @latty-ds/react wrapper package for typed props and idiomatic React event names:

import { Button, Textfield } from '@latty-ds/react';

export function Example() {
return (
<Button variant="primary" iconStart="save" onClick={() => console.log('saved')}>
Save changes
</Button>
);
}

Without the React wrapper​

If you use lt-* elements directly in React without @latty-ds/react, attach event listeners via ref — React's synthetic event system does not handle custom DOM events:

import { useRef, useEffect } from 'react';

function EmailField() {
const ref = useRef<HTMLElement>(null);

useEffect(() => {
const el = ref.current;
if (!el) return;
const handler = (e: Event) => console.log((e as CustomEvent).detail.value);
el.addEventListener('change', handler);
return () => el.removeEventListener('change', handler);
}, []);

return <lt-textfield ref={ref} label="Email" type="email" />;
}

React Server Components​

Custom elements run in the browser — they cannot be used inside React Server Components. In Next.js App Router, any file that renders Latty components must be marked 'use client':

'use client';

import { Button, Dialog } from '@latty-ds/react';

export function ConfirmDialog({ onConfirm }: { onConfirm: () => void }) {
return (
<Dialog heading="Are you sure?">
<Button slot="footer" variant="danger" onClick={onConfirm}>Delete</Button>
</Dialog>
);
}

Vue​

Vue 3 supports Web Components natively. Add one-time config in your app entry:

main.ts
import { createApp } from 'vue';
import '@latty-ds/tokens/tokens.css';
import '@latty-ds/tokens/semantic.css';
import '@latty-ds/web';
import App from './App.vue';

const app = createApp(App);
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('lt-');
app.mount('#app');

Use @event for listeners and :prop for reactive bindings in templates:

<script setup lang="ts">
function save() { /* ... */ }
function onEmail(e: Event) {
console.log((e as CustomEvent).detail.value);
}
</script>

<template>
<lt-button variant="primary" @click="save">Save changes</lt-button>
<lt-textfield label="Email" type="email" @change="onEmail"></lt-textfield>
</template>

Svelte​

Web Components work natively in Svelte. Import tokens and components in your root layout:

+layout.svelte
<script>
import '@latty-ds/tokens/tokens.css';
import '@latty-ds/tokens/semantic.css';
import '@latty-ds/web';
</script>

<slot />
<script>
let value = '';
</script>

<lt-textfield label="Name" bind:value></lt-textfield>
<lt-button variant="primary" on:click={() => console.log(value)}>Submit</lt-button>

Slots​

Components use standard HTML slots to accept content. Content placed directly inside an element goes into its default slot. Content with a slot="name" attribute targets a specific named region.

The most common named slot is trigger on lt-dropdown — any element placed there becomes the anchor that opens the menu:

<lt-dropdown>
<lt-icon-button slot="trigger" name="more-horiz" label="Options"></lt-icon-button>
<lt-dropdown-item>Edit</lt-dropdown-item>
<lt-dropdown-item>Delete</lt-dropdown-item>
</lt-dropdown>

lt-dialog uses named slots to place content in distinct regions of the overlay:

<lt-dialog id="confirm-dialog" heading="Delete item">
Are you sure you want to permanently delete this item?
<div slot="footer">
<lt-button appearance="outlined" id="cancel-btn">Cancel</lt-button>
<lt-button variant="danger" id="confirm-btn">Delete</lt-button>
</div>
</lt-dialog>

Components that expose slots list them in their API table. Omitting a named slot usually hides that region entirely — for example, omitting slot="footer" on a dialog removes the footer bar.

Compound components​

Some components are designed as parent/child pairs. The parent manages shared state; child elements are slotted in as its content.

Tabs​

lt-tab-group owns the active tab state via its value prop. Each lt-tab declares its own value, and panel content is slotted with a matching data-value:

<lt-tab-group value="profile">
<lt-tab label="Profile" value="profile" icon-start="user"></lt-tab>
<lt-tab label="Settings" value="settings" icon-start="settings"></lt-tab>
<lt-tab label="Billing" value="billing" icon-start="credit-card"></lt-tab>

<div slot="panel" data-value="profile">Profile content here</div>
<div slot="panel" data-value="settings">Settings content here</div>
<div slot="panel" data-value="billing">Billing content here</div>
</lt-tab-group>

<script type="module">
document.querySelector('lt-tab-group').addEventListener('change', (e) => {
console.log('active tab:', e.detail.value);
});
</script>

lt-nav-item elements can be nested to create collapsible groups. In vertical mode they expand inline; in horizontal mode they open as a dropdown panel:

<lt-nav>
<lt-nav-item href="/dashboard" label="Dashboard" icon-start="home" active></lt-nav-item>
<lt-nav-item label="Settings" icon-start="settings">
<lt-nav-item href="/settings/profile" label="Profile"></lt-nav-item>
<lt-nav-item href="/settings/security" label="Security"></lt-nav-item>
</lt-nav-item>
</lt-nav>

Events​

Components dispatch standard DOM events — no custom prefix. Interactive components carry data in e.detail:

// Text inputs — e.detail.value is the current string value
document.querySelector('lt-textfield').addEventListener('change', (e) => {
console.log(e.detail.value); // string
});

// Select — e.detail.value is the selected option's value
document.querySelector('lt-select').addEventListener('change', (e) => {
console.log(e.detail.value); // string
});

// Checkbox — e.detail.checked is the boolean state
document.querySelector('lt-checkbox').addEventListener('change', (e) => {
console.log(e.detail.checked); // boolean
});

// Tab group — e.detail.value is the newly active tab's value
document.querySelector('lt-tab-group').addEventListener('change', (e) => {
console.log(e.detail.value); // string
});

// Dropdown — e.detail.item is the selected lt-dropdown-item element
document.querySelector('lt-dropdown').addEventListener('select', (e) => {
console.log(e.detail.item.textContent);
});

// Overlays — open and close carry no detail
document.querySelector('lt-dialog').addEventListener('open', () => {});
document.querySelector('lt-dialog').addEventListener('close', () => {});

Composing events​

These are real DOM events, and like native ones (click, input) they bubble and cross shadow roots — so what happens next is yours to compose. A listener on a container also hears events from components nested inside it (an lt-dialog's close listener hears the close of an lt-alert inside it). Check event.target to see which component fired, or call event.stopPropagation() in the inner handler, exactly as you would for any native event. preventDefault() works where an event is cancelable (lt-alert's close).

// An alert inside a dialog: keep its close from reaching the dialog's own listener
alert.addEventListener('close', (e) => e.stopPropagation());

// ...or let it bubble and ignore anything that isn't the dialog itself
dialog.addEventListener('close', (e) => {
if (e.target !== dialog) return;
// the dialog's own close
});

With @latty-ds/react, every handler receives the event itself, typed with its payload on e.detail (onChange={(e) => e.detail.value}), so the same calls work:

<Dialog onClose={(e) => { if (e.target === e.currentTarget) setOpen(false); }}>
<Alert closable onClose={(e) => e.stopPropagation()}>Saved</Alert>
</Dialog>

Attributes vs properties​

HTML attributes are always strings — they can represent text, boolean presence, and simple enumerations. For anything richer (arrays, objects, render functions), set the JavaScript property directly:

Attributes — strings and booleans
<!-- String values -->
<lt-button variant="primary" size="md">Save</lt-button>

<!-- Boolean: presence = true, absence = false -->

<lt-button disabled>Can't click</lt-button>
<lt-textfield required label="Email"></lt-textfield>
Properties — rich data
const table = document.querySelector('lt-table');

// Arrays and objects must be set as JS properties, not attributes
table.columns = [
{ key: 'name', label: 'Name' },
{ key: 'role', label: 'Role', render: (v) => {
const badge = document.createElement('lt-badge');
badge.textContent = v;
return badge;
}}
];
table.data = [
{ name: 'Alice', role: 'Admin' },
{ name: 'Bob', role: 'Member' }
];

A common gotcha: setting element.setAttribute('disabled', false) does not disable the boolean — the attribute is still present. Use element.removeAttribute('disabled') to clear it, or set the property: element.disabled = false.

Forms​

lt-button participates in native HTML form submission — a button with type="submit" inside a <form> triggers the form's submit event as expected. Input components (lt-textfield, lt-select, lt-checkbox, etc.) do not submit their values natively. Read them via events or the element's value property on submit:

<form id="signup-form">
<lt-textfield name="email" label="Email" type="email" required></lt-textfield>
<lt-select id="role-select" label="Role"></lt-select>
<lt-button type="submit" variant="primary">Sign up</lt-button>
</form>

<script type="module">
import '@latty-ds/web';

const roleSelect = document.getElementById('role-select');
roleSelect.options = [
{ value: 'admin', label: 'Admin' },
{ value: 'member', label: 'Member' }
];

document.getElementById('signup-form').addEventListener('submit', (e) => {
e.preventDefault();
const email = document.querySelector('lt-textfield[name="email"]').value;
const role = roleSelect.value;
console.log({ email, role });
});
</script>

Next steps​

Theming — customize tokens to match your brand

Components — explore the full component catalog