A component is reusable setup
The starter shows Counter A. Add Counter B using the same Counter function, then increase A: only A’s number should change. The label is a prop—an input passed from App to each Counter.
Each call to Counter creates a separate signal in the preview’s memory. Keeping createSignal inside that function makes the counters independent. Moving one shared signal outside Counter would connect both displays to the same number instead.
Compose the page’s structure
App owns the single main landmark. Each Counter returns a labelled section, so the document still has one main area after adding a second counter. Distinct labels also make the controls understandable to keyboard and screen-reader users.
The labels are fixed for each counter; passing a string does not make it reactive. The changing number comes from that counter’s signal. A fresh run recreates both counters at 0, regardless of the numbers you reached before.
Your exercise
- At the marked location in App, add a Counter with label Counter B.
- Keep createSignal inside Counter and mount App once.
- Use the controls to check that increasing or resetting one counter leaves the other unchanged.
The starter already works. Make the requested changes in app.tsx, then choose Run again. A successful run starts a fresh preview with your new code and its initial values. A compiler error leaves the last successful preview in place; check Diagnostics before judging the result.
What Run, Reset and refresh keep
The values you change in these previews live only in browser memory. Running again or refreshing the page discards them. Your source edits are separate: the editor keeps them while this page is open, but does not autosave them.
Share link puts the current source files in the URL; Download ZIP saves those files as a local project. Neither saves the current count, checkbox states or typed notes. Refreshing a lesson link reloads the source in its current URL and waits for Run; edits not captured in that URL are lost.
On a lesson link, the playground’s Reset restores the source originally loaded by that link and clears the preview. Choose Run to start it again. This is separate from an application button such as Reset count, which changes a value inside a running preview.
Read the complete starter files
app.tsx
import { createSignal, h, mount } from '@getforma/core';
import './styles.css';
type CounterProps = { label: string };
function Counter(props: CounterProps) {
// Each Counter call creates its own signal.
const [count, setCount] = createSignal(0);
return <section class="card" aria-label={props.label}>
<h2>{props.label}</h2>
<div class="controls">
<output aria-label={props.label + ' count'}>{() => count()}</output>
<button aria-label={'Increase ' + props.label}
onClick={() => setCount(n => n + 1)}>Increase</button>
<button aria-label={'Reset ' + props.label}
onClick={() => setCount(0)}>Reset</button>
</div>
</section>;
}
function App() {
return <main>
<p class="eyebrow">02 / Components</p>
<h1>Independent counters.</h1>
<p>Each instance owns its state. Changing one should leave the other alone.</p>
<Counter label="Counter A" />
{/* Exercise: add a second Counter labelled Counter B here. */}
</main>;
}
mount(App, '#app');
styles.css
:root { font-family: Inter, ui-sans-serif, system-ui, sans-serif; color: #162225; background: #f2f5f1; }
* { box-sizing: border-box; }
body { margin: 0; padding: 32px; }
main { max-width: 540px; margin: 0 auto; }
h1 { font-size: clamp(28px, 7vw, 40px); line-height: 1.1; letter-spacing: -.05em; }
p { line-height: 1.6; color: #52615d; }
button, input { font: inherit; }
button { cursor: pointer; border: 1px solid #1a3530; border-radius: 8px; padding: 10px 15px; background: #173e34; color: #fff; }
button:hover { background: #24624f; }
button:focus-visible, input:focus-visible { outline: 3px solid #cb7bff; outline-offset: 3px; }
button:disabled { opacity: .5; cursor: default; }
input { min-width: 0; padding: 10px; border: 1px solid #a5b4aa; border-radius: 8px; background: white; }
label { display: grid; gap: 8px; }
output { display: inline-block; font-variant-numeric: tabular-nums; font-size: 42px; font-weight: 700; }
.eyebrow { text-transform: uppercase; letter-spacing: .15em; font-size: 11px; font-weight: 700; color: #326b58; }
.controls { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; margin: 24px 0; }
.card { padding: 22px; border: 1px solid #cbd8ce; border-radius: 14px; background: #fff; }
ul { list-style: none; padding: 0; display: grid; gap: 10px; }
li { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 14px; border: 1px solid #cbd8ce; border-radius: 10px; background: white; }
li > span:first-child { flex: 1; }
.inventory-row { display: grid; grid-template-columns: 1fr auto; }
.inventory-row label { grid-column: 1 / -1; }
@media (max-width: 420px) { body { padding: 20px; } }
Verify the behavior
| Action | Expected result |
|---|---|
| Run the solution | Counter A and Counter B both start at 0. |
| Increase Counter A twice | A is 2 and B remains 0. |
| Increase B once, then reset A | A is 0 and B remains 1. |
These checks exercise the visible result. Running without a compiler error is only the first step.
A working solution
App adds another Counter with a different label; Counter creates its own signal each time it is called. Resetting A therefore cannot change B. This is browser composition with mount; the native-rendering lesson later shows how a server-rendered island gets its behavior.
Read the complete solution files
app.tsx
import { createSignal, h, mount } from '@getforma/core';
import './styles.css';
type CounterProps = { label: string };
function Counter(props: CounterProps) {
// Each Counter call creates its own signal.
const [count, setCount] = createSignal(0);
return <section class="card" aria-label={props.label}>
<h2>{props.label}</h2>
<div class="controls">
<output aria-label={props.label + ' count'}>{() => count()}</output>
<button aria-label={'Increase ' + props.label}
onClick={() => setCount(n => n + 1)}>Increase</button>
<button aria-label={'Reset ' + props.label}
onClick={() => setCount(0)}>Reset</button>
</div>
</section>;
}
function App() {
return <main>
<p class="eyebrow">02 / Components</p>
<h1>Independent counters.</h1>
<p>Each instance owns its state. Changing one should leave the other alone.</p>
<Counter label="Counter A" />
<Counter label="Counter B" />
</main>;
}
mount(App, '#app');
styles.css
:root { font-family: Inter, ui-sans-serif, system-ui, sans-serif; color: #162225; background: #f2f5f1; }
* { box-sizing: border-box; }
body { margin: 0; padding: 32px; }
main { max-width: 540px; margin: 0 auto; }
h1 { font-size: clamp(28px, 7vw, 40px); line-height: 1.1; letter-spacing: -.05em; }
p { line-height: 1.6; color: #52615d; }
button, input { font: inherit; }
button { cursor: pointer; border: 1px solid #1a3530; border-radius: 8px; padding: 10px 15px; background: #173e34; color: #fff; }
button:hover { background: #24624f; }
button:focus-visible, input:focus-visible { outline: 3px solid #cb7bff; outline-offset: 3px; }
button:disabled { opacity: .5; cursor: default; }
input { min-width: 0; padding: 10px; border: 1px solid #a5b4aa; border-radius: 8px; background: white; }
label { display: grid; gap: 8px; }
output { display: inline-block; font-variant-numeric: tabular-nums; font-size: 42px; font-weight: 700; }
.eyebrow { text-transform: uppercase; letter-spacing: .15em; font-size: 11px; font-weight: 700; color: #326b58; }
.controls { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; margin: 24px 0; }
.card { padding: 22px; border: 1px solid #cbd8ce; border-radius: 14px; background: #fff; }
ul { list-style: none; padding: 0; display: grid; gap: 10px; }
li { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 14px; border: 1px solid #cbd8ce; border-radius: 10px; background: white; }
li > span:first-child { flex: 1; }
.inventory-row { display: grid; grid-template-columns: 1fr auto; }
.inventory-row label { grid-column: 1 / -1; }
@media (max-width: 420px) { body { padding: 20px; } }
Need the API details? Read the Forma 3.0 core reference. This playground compiles TSX and runs browser code. It does not run a Rust server or demonstrate server rendering; the later native lessons cover that path.