CHART/DOCS/FIRST COMPONENT

Your first component

A server page renders static HTML. A client island adds interactivity.

A component is a plain function that takes props and returns JSX - no classes, no hooks, no framework imports. The only imports the compiler adds are from @marisjs/runtime when you use signals.

// @runsOn server
import { data } from '@marisjs/runtime';
import { Counter } from '../islands/Counter.tsx';

type Props = {};

export function Index(props: Props) {
  const greeting = await data(async () => 'Hello from the server');

  return (
    <div>
      <h1>{greeting}</h1>
      <Counter client:hydrate />
    </div>
  );
}
// @runsOn client
import { signal } from '@marisjs/runtime';

type Props = {};

export function CounterDemo(props: Props) {
  const count = signal(0);
  function inc() { count.set(count.value + 1); }
  function dec() { count.set(count.value - 1); }
  function reset() { count.set(0); }
  return (
    <div class="counter-wrap">
      <div class="counter-num">{count.value}</div>
      <div class="counter-btns">
        <button class="counter-btn" onClick={dec} disabled={count.value === 0} aria-label="decrement">-</button>
        <button class="counter-btn" onClick={inc} aria-label="increment">+</button>
        <button class="counter-reset" onClick={reset}>reset</button>
      </div>
    </div>
  );
}

What happens at build time

On the first build, the server page is prerendered to static HTML - the page above produces dist/index.html with the counter markup already in it. The island is compiled to a separate module and hydrated only when the page loads in a browser.

The naming rule

The exported component must match its file name: counter.tsx must export Counter, not anything else. The compiler rejects mismatches with a FILENAME_MISMATCH error instead of guessing.

The component body

The only allowed statements in a component body, in this order: signal and computed declarations, plain local consts derived from them, event handler functions, and a single return. Nothing else - no let, no reassignment, no helper functions defined mid-body.

The props type is declared in the same file (immediately above the component) or imported from a dedicated *.types.ts file - never inline in the signature. Non-component helpers live in a separate plain .ts file and are imported.

Children: one typed slot

children is an explicit, typed prop - not implicit magic. A component that accepts nested JSX declares it like any other field:

// @runsOn client

type CardProps = {
  title: string;
  children: JSX.Element;
};

export function Card(props: CardProps) {
  return (
    <div class="card">
      <h3>{props.title}</h3>
      <div class="card-body">{props.children}</div>
    </div>
  );
}

// <Card title="Hello"><p>Nested content</p></Card>
  • The caller passes exactly one root element between the tags - multiple siblings must be wrapped.
  • Passing children to a component whose props do not declare them is a hard error (UNEXPECTED_CHILDREN).
  • The attribute form (children={...}) is a hard error (CHILDREN_ATTRIBUTE) - nested JSX is the single sanctioned channel.
  • Islands may receive children: they are server-composed and adopted, never re-parsed or transformed.
Next: interactivity means state, and marisjs has exactly one way to hold it - signal(). See Signals and computed.