Skip to main content

Your first component

Let's build a counter. This mirrors the examples/counter project in the repository.

The component

A Tanni component is a single .tanni file with <script>, <template>, and (optionally) <style> blocks:

<!-- src/App.tanni -->
<script lang="ts">
import { createSignal } from 'tannijs';

const [count, setCount] = createSignal(0);

function increment() {
setCount((c) => c + 1);
}

function decrement() {
setCount((c) => c - 1);
}

function reset() {
setCount(0);
}
</script>

<template>
<main>
<h1>Tanni Counter</h1>
<p>{{ count() }}</p>
<button @click="increment">+</button>
<button @click="decrement"></button>
<button @click="reset">Reset</button>
</main>
</template>

A few things to notice:

  • createSignal(0) returns a [getter, setter] pair. You call the getter (count()) to read the current value.
  • {{ count() }} interpolates a reactive value into the DOM. When count changes, only that text node updates.
  • @click="increment" attaches an event handler. See Template Syntax for the full set of bindings.

Mounting the app

Every compiled .tanni component exports a factory function that returns a DOM node. Mount it by appending the result to an element on the page:

// src/main.ts
import App from './App.tanni';

const root = document.getElementById('app')!;
root.append(App());
<!-- index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Tanni App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>

TypeScript: declaring .tanni modules

So TypeScript understands .tanni imports, add an ambient declaration (the starter template includes this as tanni-env.d.ts):

// tanni-env.d.ts
declare module '*.tanni' {
const component: (props?: Record<string, unknown>) => Node;
export default component;
}

Where to go next

  • ReactivitycreateSignal, createEffect, createMemo.
  • Directivestn-if, tn-for, tn-model, and more.
  • Components — props, child components, and slots.