Skip to content

Your First Component

The easiest way to learn Ornata is to enhance a small piece of existing HTML with a clear component contract.

<section data-counter>
<p>
<span data-count-label>Clicks</span>:
<strong data-count-value>0</strong>
</p>
<button type="button" data-count-button>Increment</button>
</section>

Even without JavaScript, this markup still renders understandable content.

import { defineComponent } from "ornata";
const Counter = defineComponent({
name: "Counter",
state: {
count: { default: 0 },
label: { default: "Clicks" },
},
elements: {
label: { query: "[data-count-label]" },
value: { query: "[data-count-value]" },
button: { query: "[data-count-button]" },
},
methods: {
increment() {
this.state.count += 1;
},
},
watch: {
count({ newValue, oldValue, isInitial }) {
if (isInitial) return;
console.log(`Count changed from ${oldValue} to ${newValue}`);
},
},
computed: {
total() {
return this.state.count;
},
},
render: {
label() {
return {
text: this.state.label,
};
},
value() {
return {
text: String(this.computed.total),
};
},
button() {
return {
events: {
click: () => this.methods.increment(),
},
};
},
},
});
const root = document.querySelector("[data-counter]");
if (root) {
Counter.mount(root);
}

Each top-level option has a clear job:

  • state defines reactive values
  • elements finds important DOM nodes inside the root
  • methods defines reusable internal behavior
  • watch reacts to state changes
  • computed derives values from state
  • render applies DOM updates to resolved elements

That separation is one of Ornata’s main strengths. It keeps the contract between markup, behavior, and state visible, which makes the component easier to reuse in larger HTML-first systems.

This example intentionally shows several top-level sections together. Smaller components do not need every section, but the shape stays consistent across the library.

If you want a deeper look at action-oriented component logic, read Methods.

When you want clearer contracts, provide typed component parts:

interface CounterState {
count: number;
label: string;
}
interface CounterMethods {
increment(): void;
}
const Counter = defineComponent<{
state: CounterState;
methods: CounterMethods;
}>({
name: "Counter",
state: {
count: { default: 0 },
label: { default: "Clicks" },
},
methods: {
increment() {
this.state.count += 1;
},
},
});

Continue to Component Anatomy for a deeper look at every option section.

If you want a fuller walkthrough of inference and explicit typed parts, read TypeScript.