Components
Components are the building blocks of Coralite. They allow you to encapsulate HTML, CSS, and logic into reusable custom elements. Coralite supports two types of components: Static and Dynamic.
Static Components #
Static components are purely HTML-based elements without complex scripting logic. If a piece of UI doesn't need to fetch data, handle user events, or compute complex state, it should be a static component.
Component Definition #
Every component in Coralite is defined inside an HTML file using the standard <template> tag. The most important part of a component is its id attribute. This ID becomes the custom HTML tag name you will use to include the component in your pages. Note: All component IDs MUST contain a hyphen (e.g., my-header).
<!-- src/components/header.html -->
<template id="coralite-header">
<header class="site-header">
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
</template>
Using Components #
To use the component in your pages (or inside other components), the custom element tag name must exactly match the component's id.
Coralite components are server-side rendered (SSR). By default, Coralite preserves your custom host tag in the final HTML and injects the template content inside it. Learn more about how this works in the SSR Guide.
<!-- src/pages/index.html -->
<html lang="en">
<head>
<title>Home</title>
</head>
<body>
<!-- The tag name matches the template ID -->
<coralite-header></coralite-header>
<main>
<h1 class="display">Welcome to my site!</h1>
</main>
</body>
</html>
Basic Data Binding #
You can pass data into static components using standard HTML attributes. Coralite allows you to display these attribute values inside your component using double curly braces ({{ }}).
<!-- src/components/user-card.html -->
<template id="user-card">
<div class="card">
<!-- The 'name' attribute value will be injected here -->
<h2> </h2>
<!-- The 'role' attribute value will be injected here -->
<p class="role"> </p>
</div>
</template>
Now, when using the component, simply pass the data as attributes:
<!-- src/pages/team.html -->
<body>
<h1 class="display">Our Team</h1>
<div class="team-grid">
<!-- Passing data via attributes -->
<user-card name="Alice Smith" role="Lead Developer"></user-card>
<user-card name="Bob Jones" role="Designer"></user-card>
</div>
</body>
Standard Slots #
Sometimes you need to pass entire blocks of HTML into a component, not just simple text attributes. Coralite supports standard HTML <slot> elements for this purpose.
Note: In Coralite, <slot> elements are styled with display: contents; out of the box at the stylesheet root. This makes slot containers layout-transparent, allowing slotted children to participate directly in parent Flexbox and Grid layouts without disrupting layout flow or requiring extra CSS wrapper hacks.
<!-- src/components/alert-box.html -->
<template id="alert-box">
<div class="alert ">
<div class="alert-content">
<!-- Content passed between tags goes here -->
<slot></slot>
</div>
</div>
</template>
Zero-JS Static Components with Attributes #
A component is Zero-JS static when it serializes zero client lifecycle code: client: null and getters: {}. You can include a <script type="module"> that exports attributes (for type coercion and default values) or style and still maintain a 100% Zero-JS footprint in the browser runtime.
<!-- src/components/ui-button.html -->
<template id="ui-button">
<button type="">
<slot></slot>
</button>
</template>
<script type="module">
export const attributes = {
type: {
type: String,
default: 'button'
},
disabled: {
type: Boolean,
default: false
}
}
</script>
Both and render directly from state at SSR time without getters or client code. Native boolean attributes reflect automatically (present when truthy, omitted when falsy).
Dynamic Components #
While static components are great for simple layouts, modern web development often requires more power. When your component needs data fetching, reactive state, or client-side interactivity, you create a Dynamic Component.
Dynamic components use standard JavaScript top-level Named Exports inside a <script type="module"> block. They follow the Smart State, Dumb Template philosophy, where component logic is cleanly declared through dedicated export functions and constants.
Coralite components declare behavior using top-level named exports:
- Handler Functions:
export function server(ctx),export function client(ctx),export function onError(ctx) - Declarations:
export const attributes = {},export const getters = {},export const style = {},export const slots = {},export const provide = ...,export const consume = ...,export const formControl = true,export const listStrategy = ...
Wrapper Compatibility: The legacy export default defineComponent({ ... }) and export default definePage({ ... }) syntaxes remain fully supported for existing code. If both a wrapper and flat exports are present, the wrapper takes precedence. New components and pages should use flat named exports; plugins continue to use definePlugin.
By default, Coralite preserves the host tag of dynamic components in the final HTML but applies display: contents via an internal stylesheet. This ensures the tag doesn't affect your layout while remaining available for hydration.
Note: If you use the no-hydration attribute, the host tag is completely removed (spliced) and replaced by its contents. Learn more in the SSR Guide.
Upgrading a Component #
To turn a static component into a dynamic one, append a <script type="module"> tag below your <template> and export the component behaviors.
<template id="my-dynamic-component">
<div>...</div>
</template>
<!-- The addition of this script block makes it dynamic -->
<script type="module">
export const attributes = {
// ...
}
export async function server () {
// ...
}
export const getters = {
// ...
}
export const style = {
// ...
}
export const slots = {
// ...
}
export async function client () {
// ...
}
</script>
The Pillars of Component State #
Coralite unifies input attributes, server data, derived getters, reactive styles, and slot projections into a single reactive state object available to your templates and controllers.
1. Attributes (Inputs) #
Attributes are the "props" of your component. Coralite supports type coercion, enum validation (values / array shorthand), mandatory requirements (required: true), synchronous transformations (transform), and custom validation predicates (validate).
Note: Attributes only support String, Number, and Boolean types. Object and Array types are blocked; use the server block for complex data. For full details on the 6-step attribute execution pipeline, see the Component API Reference.
export const attributes = {
// Type shorthand & array shorthand (allowed enum values)
expanded: Boolean,
variant: ['primary', 'secondary', 'danger'],
// Verbose schema definition with transform and validate pipeline
theme: { type: String, default: 'light', values: ['light', 'dark'] },
apiKey: { type: String, required: true },
count: {
type: Number,
default: 0,
transform: (val) => Math.max(0, val),
validate: (val) => Number.isInteger(val) || 'Count must be an integer'
}
}
6. OnError (Component Error Boundary) #
The onError function provides component-level fault isolation for production rendering. If server data fetching, derived getters, or slot processing throws an error during SSR, onError catches the failure and renders fallback UI with noHydration: true and stripped data-cid hydration tokens.
export function onError ({ error, state }) {
return {
template: '<div class="fallback"><p></p></div>',
state: { message: 'Content temporarily unavailable' }
}
}
2. Server (Server-Side Logic) #
The server function runs strictly on the server during build-time. It's the place for database queries or API calls. External dependencies should be loaded via dynamic await import(). See the SSR Guide for more details.
export async function server ({ myPlugin }) {
const posts = await db.getPosts();
return { posts, pluginData: myPlugin.data };
}
3. Getters (Derived State) #
Getters calculate state based on other state. They are pure, synchronous, and reactive, receiving a destructured context object ({ state, root, refs, slots, signal }).
export const getters = {
postCount: ({ state }) => state.posts.length
}
4. Style (Reactive CSS Properties) #
The style declaration allows you to declaratively bind reactive CSS variables (CSS Custom Properties) and standard inline CSS properties directly to the component's host custom element tag. Style functions receive state directly and must be synchronous.
export const style = {
'--accent-color': (state) => state.theme === 'dark' ? '#38bdf8' : '#0284c7',
opacity: (state) => state.isVisible ? 1 : 0
}
5. Slots (Light DOM Transformation) #
The slots declaration allows you to intercept and transform the Light DOM nodes passed to your component before they are rendered.
export const slots = {
default: (nodes, state) => {
// Wrap all nodes in a div if count is even
if (state.count % 2 === 0) {
return `<div class="wrapper">${nodes.map(n => n.outerHTML).join('')}</div>`;
}
return nodes;
}
}
Referencing Elements (Refs) #
Before writing client-side logic, you often need a way to target specific DOM elements inside your template without relying on brittle CSS classes or IDs. Coralite provides the ref attribute for this.
<template id="counter-btn">
<button ref="my-btn">Clicked <span> </span> times</button>
</template>
6. Client (Client Controller) #
The client function is where interactivity lives. It runs in the browser and has full read/write access to state, as well as access to element references (refs), state observation (observe), custom event dispatching (emit), and render completion synchronization (updateComplete). Because client runs in the browser, any external modules must be imported using dynamic await import().
export async function client ({ state, signal, refs, observe, emit, updateComplete }) {
// 'my-btn' matches the ref="my-btn" in the template above
const btn = refs('my-btn');
btn.addEventListener('click', async () => {
state.count++; // Mutating state triggers a re-render!
await updateComplete; // Deterministically await DOM and observer flush!
emit('count-changed', { count: state.count }); // Dispatch custom event to parent
}, { signal });
}
Form-Associated Custom Elements (FACE) #
Web Components often need to participate in native HTML forms—submitting values in FormData, participating in constraint validation, resetting on form reset, and responding when disabled by a parent <fieldset disabled>. Coralite makes this effortless with first-class, standard-aligned Form-Associated Custom Elements support.
To enable form association, export formControl = true (legacy: formAssociated: true):
<template id="custom-text-input">
<label class="field">
<span class="label-text"> </span>
<input ref="inputEl" type="text" value="" placeholder=""></input>
<span class="error-msg"> </span>
</label>
</template>
<script type="module">
// Enables native ElementInternals & form participation
export const formControl = true
export const attributes = {
name: String,
label: String,
placeholder: {
type: String,
default: ''
},
value: {
type: String,
default: '',
validate: (val) => (!val || val.length >= 2) || 'Must be at least 2 characters'
},
disabled: Boolean
}
export function client ({ state, refs, signal, onFormReset, onFormDisabled }) {
const input = refs('inputEl')
// Update state on user input (automatically syncs form submission value & validity)
input.addEventListener('input', (e) => {
state.value = e.target.value
}, { signal })
// React to form reset events (state.value automatically reverts to default)
onFormReset(() => {
console.log('Custom input reset to initial value')
})
// React to <fieldset disabled> or form-level disabled changes
onFormDisabled((disabled) => {
console.log('Disabled status changed:', disabled)
})
}
</script>
When formControl: true is active:
- Form Value Auto-Sync: Any mutation to
state.valueautomatically callsinternals.setFormValue(value). Form submissions andnew FormData(form)seamlessly pick up the component's value. - Automatic Validity Bridge: Attribute schema validation errors on
valueautomatically invokeinternals.setValidity()and anchor validation popups to the internal input element. - Form Reset Restoration: Resetting the form automatically restores initial attribute/default snapshots, clears errors, and fires
onFormReset. - Fieldset Cascading: Ancestor
<fieldset disabled>state automatically cascades intostate.disabledand triggersonFormDisabled.
For complete details on host element properties (such as el.name, el.form, el.validity, and el.checkValidity()), see the defineComponent API Reference.
The Flow of State #
It helps to visualize the component pillars as different input streams feeding into a single, unified State Proxy. Whether data comes from the server, an HTML attribute, or a client-side mutation, it all ends up in the same place.
- Initialization: When the page builds, Coralite gathers Attributes and executes the Server block.
- Serialization: This combined data is serialized into a JSON object and sent to the browser.
- Hydration: On the client, Coralite "wakes up" the component and merges the serialized data into a reactive Proxy.
- Mutation: When the Client block or a Getter mutates the state, the Proxy detects the change.
- Re-render: Coralite automatically updates the Template (and re-runs Slots if necessary) to reflect the new state.
Reactivity & Performance #
Coralite uses a Lazy Deep Proxy system. This means that even if you have massive datasets in your state, you only pay the performance cost for the parts of the state that you actually access or mutate. This ensures that Coralite stays fast even as your application grows.
Best Practices: Smart State, Dumb Template #
- Keep Templates Simple: Templates should only contain HTML and
placeholders. Avoid complex logic in HTML. - Boolean & ARIA Attribute Toggling: Native HTML boolean attributes (43 standard attributes) and
aria-*attributes are automatically added or removed when bound to single state tokens. Learn more in the Component API Reference. - Prefer Primitives for Attributes: Only pass Strings, Numbers, and Booleans via attributes. Use
serverfor complex objects. - Clean Up with AbortSignal: Always pass the
signaltoaddEventListenerorfetchinside yourclientto prevent memory leaks.
Anti-Patterns: Respect the Component Boundary #
In Coralite, Pages are strictly consumers. To ensure your application remains maintainable and robust, you must respect the boundary between a page and its components.
- Avoid Page-Level Scripts for Component Logic: Do not use
<script>tags in standard.htmlpages to manipulate components. All logic related to a component's behavior should be encapsulated within the component itself. - Never Use querySelector on Component Internals: Attempting to use
document.querySelectorfrom a page to find an element inside a component is an anti-pattern. Because Coralite uses therefsystem to dynamically generate unique, namespaced selectors for every instance, relying on specific classes or IDs from a page is brittle and unreliable. - Do Not Manually Sync Attributes: Avoid using page-level JavaScript to manually update component attributes (e.g.,
setAttribute) to trigger changes. Instead, manage state within the component'sclientblock. - Avoid Logic Coordination in Pages: If multiple components need to coordinate (e.g., a search bar filtering a list), do not write this logic in the page. Instead, move both components into a parent component that handles the coordination logic.
For detailed API signatures, see the Component API Reference.