The component contract
A component is one file whose default export is a defineTaggorComponent call, from the
@taggor/component SDK. The call declares everything Taggor needs to know about the component, its
manifest, and a render function that returns a Preact view. The build reads the manifest from the
source, checks it against the contract, and the runtime mounts the component in a Shadow DOM on your
site, with an error boundary around it.
The numbers and tables on this page are generated from the contract package, and the code is copied from templates that have their own tests, so the page cannot drift from what the build checks.
The shape of a component
Section titled “The shape of a component”import { defineTaggorComponent, prop, useLocale, useTrack } from "@taggor/component";
export default defineTaggorComponent({ name: "faq-accordion", tag: "faq-accordion", title: "FAQ accordion", description: "Lists questions whose answers open on click or with the keyboard.", props: { headline: prop.string({ maxLength: 120, description: "Replaces the default heading." }), items: prop.array({ description: "The questions and answers, in the site's language.", items: { type: "object", fields: { question: { type: "string", minLength: 1, maxLength: 200 }, answer: { type: "string", minLength: 1, maxLength: 2000 }, }, required: ["question", "answer"], }, minItems: 1, maxItems: 30, default: [ { question: "Como funciona a entrega?", answer: "Enviamos para todo o Brasil em até 5 dias úteis.", }, { question: "Posso trocar um produto?", answer: "Sim, em até 30 dias após o recebimento, sem custo.", }, { question: "Quais formas de pagamento vocês aceitam?", answer: "Pix, boleto e cartão de crédito em até 12 vezes.", }, ], }), multiple: prop.boolean({ description: "Lets several answers stay open at once." }), }, events: { toggle: { description: "The visitor opened or closed an answer.", detail: { type: "object", fields: { index: { type: "number", integer: true, min: 0 }, open: { type: "boolean" }, }, required: ["index", "open"], }, }, }, data: { track: { "faq-open": { props: { index: { type: "number", integer: true, min: 0, max: 29 } } } }, }, parts: { question: { description: "Each question button." }, answer: { description: "Each answer panel." }, }, cssVars: { "--faq-accent": { type: "color", default: "#0f766e", description: "Open question color." }, "--faq-border": { type: "color", default: "#d1d5db", description: "Divider color." }, }, // locales and styles: see the template // render(props, context): next block});nameandtag: the component’s id and the custom element’s tag. A tag is lowercase, has a hyphen, and cannot start withtaggor-.titleanddescription: shown in the portal and to agents.props,events,slots,parts,cssVars,data: the sections below.
render: (props, context) => { const { t } = useLocale(context); const track = useTrack(context); const { part, emit } = context; const [open, setOpen] = useState<ReadonlySet<number>>(new Set());
const toggle = (index: number) => { const opening = !open.has(index); const next = new Set(props.multiple ? open : []); if (opening) next.add(index); else next.delete(index); setOpen(next); emit("toggle", { index, open: opening }); if (opening) track("faq-open", { index }); }; const allOpen = open.size === props.items.length;
return ( <section class="card"> <div class="top"> <h2>{props.headline ?? t("headline")}</h2> {props.multiple ? ( <button type="button" class="all" onClick={() => { setOpen(new Set(allOpen ? [] : props.items.map((_item, index) => index))); }} > {allOpen ? t("closeAll") : t("openAll")} </button> ) : null} </div> {props.items.map((item, index) => { const expanded = open.has(index); return ( <div class="item" key={index}> <h3> <button type="button" id={`q${String(index)}`} part={part("question")} aria-expanded={expanded ? "true" : "false"} aria-controls={`a${String(index)}`} onClick={() => { toggle(index); }} > {item.question} </button> </h3> <div id={`a${String(index)}`} role="region" aria-labelledby={`q${String(index)}`} part={part("answer")} hidden={!expanded} > {item.answer} </div> </div> ); })} </section> ); },});render receives the typed props and a context with emit (events), part (theming) and what
the hooks need (useLocale, useTrack, useTaggorData, useSlot).
A prop is a typed, bounded value the host page sets as an attribute (max-value="10") or as a
property on the element. The attribute name is the kebab-case form of the prop name. Every prop has a
type, and a value that does not fit it falls back to the default.
| Type | SDK builder | Options |
|---|---|---|
string |
prop.string() |
minLength, maxLength, format (email, url, tel, date, date-time), default |
number |
prop.number() |
min, max, integer, default, step |
boolean |
prop.boolean() |
default |
enum |
prop.enum() |
values, default |
color |
prop.color() |
default |
url |
prop.url() |
protocols, default |
date |
prop.date() |
min, max, default |
array |
prop.array() |
items, minItems, maxItems, default |
object |
prop.object() |
fields, required, default |
Events
Section titled “Events”An event is declared with a description and the shape of its detail. The component calls
emit("toggle", { index, open }), and the page receives a DOM CustomEvent named
<tag>:<event>, here faq-accordion:toggle, with that detail:
document.querySelector("faq-accordion").addEventListener("faq-accordion:toggle", (event) => { console.log(event.detail.index, event.detail.open);});Slots and fallback
Section titled “Slots and fallback”A slot is a place where the page can put its own HTML inside the component. Declare it in slots and
render a native <slot name="…"> where it should appear. useSlot("title").hasContent tells the
component whether the page filled it.
export default defineTaggorComponent({ name: "fallback-card", tag: "fallback-card", title: "Fallback card", description: "A call to action whose title the page can replace; plain HTML stands in for it.", props: { headline: prop.string({ maxLength: 60, default: "Simule seu financiamento" }) }, slots: { title: { description: "Replaces the default headline." } }, styles: [ ":host { display: block; min-height: 120px; padding: 8px; box-sizing: border-box }", "h3 { margin: 0 0 8px }", ], render: ({ headline }) => { const title = useSlot("title"); return h( "section", // Exposed for the browser test: what `useSlot` reported on the last render. { "data-title-slot": title.hasContent ? "filled" : "empty" }, h("h3", null, title.hasContent ? h("slot", { name: "title" }) : headline), h("button", { type: "button" }, "Simular"), ); },});The HTML the page writes inside the tag that has no slot attribute is the fallback: it is what a
visitor sees before the component loads, or if it never does. How to write it is in
Install the snippet.
Fonts and the component’s box
Section titled “Fonts and the component’s box”A font is a .woff2 file in the component’s folder, imported and registered with useFont;
fontFamily gives the name to write in the CSS. Every component that shows the font calls useFont,
and the file is one cached object for all of them. A @font-face rule in the component’s CSS does not
apply inside a Shadow DOM.
Every component starts with :host { display: block; contain: content }. The containment keeps the
component from painting over the page, and it also cuts what an element paints outside the component’s
own box: a box-shadow, a drop-shadow(), a focus outline pushed outward. Leave room inside the component (padding on an inner wrapper), or add
:host { contain: none } to its styles when the paint must leave the box.
Theming
Section titled “Theming”parts names the elements a page may style with ::part(name), and cssVars the CSS variables it may
set (style="--faq-accent: #7c3aed"), each with a type and a default. Nothing else of the component’s
styles is reachable from the page, and the page’s CSS cannot reach into the component.
A component that collects or records something declares it in data, and reads and writes it through
the useTaggorData and useTrack hooks. The data belongs to the customer; the visitor is never
tracked across sites, and no metric carries personal data.
data: { collections: { contacts: { fields: { name: { type: "string", minLength: 1, maxLength: 120 }, email: { type: "string", format: "email" }, message: { type: "string", maxLength: 2000 }, }, required: ["name", "email"], }, }, track: { "contact-submit": { props: { ok: { type: "boolean" } } } }, personal: true, privacyPolicyUrl: "privacyPolicy", },collectionsare the tables the component can submit to, with typed fields. The component’s data can be read back withtaggor data export. The whole flow, with a complete example, is in Forms and data.tracknames the interactions to measure, with a few small typed props; never free text.personal: truemakes every submission need the visitor’s consent, andprivacyPolicyUrlnames the prop that holds the link to the site’s privacy policy.
Limits
Section titled “Limits”The build refuses a manifest above these limits.
| What | Limit |
|---|---|
| Props | 50 |
| Events | 20 |
| Slots | 10 |
| Parts | 30 |
| CSS variables | 30 |
| Data collections | 10 |
| Fields per collection | 50 |
| Tracked interactions | 30 |
| Values of one enum | 50 |
| Manifest size (characters of JSON) | 65,536 |
What the build checks
Section titled “What the build checks”On publish the builder checks the manifest, types, size, CSS and security rules. A failure prints a
code with a page under build diagnostics: what it means and how to fix it. The
JSON Schema of the manifest is public at
/schema/component-manifest/v1.json, so an editor can validate
a manifest and an agent can read the contract.