Get started

Getting Started

Set up the build, write your first component and mount the application, with or without JSX.

This section covers setting up the MotifJS build chain, writing the first component and getting the application on screen. The examples come in two forms: with JSX (.tsx) and without JSX, using component calls directly (.ts).

Requirements

The two packages (@motifx/compiler and @motifx/core) are always published with the same version number, whether or not one of them changed, and they are installed at the same version. The surface that compiled code relies on in the runtime (the compiler contract) changes only in a major version. If the runtime does not support the contract number declared by the compiled code, development mode prints an MJX121 warning (the mismatch is noticed while modules load, but the warning appears when app.useDevelopment(true) is called).

Installation

New project

npm create motifjs@latest notes

pnpm create motifjs, yarn create motifjs, bun create motifjs and npx create-motifjs do the same thing. create-motifjs is the MotifJS community's project generator and needs Node 20.19 or later. It asks for the language (TypeScript or JavaScript), whether components are written with or without JSX, the AI agent docs and DevTools, and creates a Vite project with the compiler plugin and motif-lint already set up. Today the only template is starter, a complete notes application.

Adding to an existing project

The core is a dependency of the application, and the compiler is a development dependency:

npm install @motifx/core
npm install -D @motifx/compiler

With pnpm, yarn and bun the same two packages are installed with add, the compiler again as a development dependency. Then make the two settings in Build setup.

Without a bundler

The core's UMD file is the package's unpkg / jsdelivr entry and exposes every export on the global motif object:

<!doctype html>
<html>
  <head><meta charset="utf-8" /><title>MotifJS App</title></head>
  <body>
    <div id="app"></div>
    <script src="https://unpkg.com/@motifx/core"></script>
    <script src="/main.js"></script>
  </body>
</html>

main.js starts with const { Application, motifComponent, reactive } = motif;. JSX and decorator syntax need the compiler, so here the view is written without JSX, services are registered with builder.services, and code marked @DEV-ONLY is not stripped. In production, pin the address to a version: https://unpkg.com/@motifx/core@<version>.

Build setup

MotifJS JSX is not handled by a standard jsxFactory but by its own compiler (@motifx/compiler). The compiler turns every JSX element into a motifComponent call, and the reactive expressions inside it (text, conditions, lists) into bindings.* calls. That is why two settings are needed:

  1. Add the @motifx/compiler plugin to the bundler. The plugin runs before Vite's own transform (enforce: 'pre') and leaves no JSX behind, so Vite needs no separate JSX setting.
  2. Set "jsx": "preserve" in tsconfig.json: TypeScript checks the types of the JSX but does not compile it. A project without JSX does not need this line.
import { defineConfig } from 'vite';
import compiler from '@motifx/compiler';

export default defineConfig({
  plugins: [compiler()],
  resolve: {
    extensions: ['.tsx', '.ts', '.jsx', '.js'],
  },
  server: { port: 3000 },
});
import { defineConfig } from 'vite';
import compiler from '@motifx/compiler';

export default defineConfig({
  plugins: [compiler()],
  resolve: {
    extensions: ['.tsx', '.ts', '.jsx', '.js'],
  },
  server: { port: 3000 },
});
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "esnext",
    "moduleResolution": "bundler",
    "strict": true,
    "jsx": "preserve",
    "useDefineForClassFields": true,
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "types": ["vite/client"]
  },
  "include": ["src"]
}

Why jsx: "preserve"? In MotifJS, JSX does not turn into a "render function call" but into compiled output that sets up reactive bindings. For example, the compiler turns the expression {model.count} into a sender.bindings.add("textContent", model, "count") call. Only @motifx/compiler knows this transform; TypeScript only checks the types, and @motifx/compiler is what compiles the JSX.

The plugin compiles .jsx and .tsx files. In .ts/.js files it transforms decorators (@Injectable) (see Registering Services), names module-level stores for DevTools during vite dev, and strips development-only code from production builds. In .ts/.mts/.cts files it adds a static elementTag from an element generic such as Component<HTMLUListElement> (see Root element). In files under node_modules, only development-only code is stripped. In Vite 8 (Rolldown), if the application has not set optimizeDeps.rolldownOptions.transform.jsx for dependency scanning, the plugin sets it to 'preserve', so .jsx files are read without looking for another JSX runtime.

Rollup uses the same compiler() plugin; there is no separate Rollup or Babel plugin. Rollup has no mode of its own, so tell the plugin about production builds: compiler({ production: !process.env.ROLLUP_WATCH }). The plugin also strips types from the .tsx and .jsx files it compiles. Stripping types from plain .ts files and resolving imports written without an extension or by package name is the job of the usual Rollup plugins: rollup-plugin-esbuild or @rollup/plugin-typescript, plus @rollup/plugin-node-resolve with the .tsx and .ts extensions added. The vite/client types and import.meta.env are specific to Vite.

Development-only code

A production build strips code marked as development-only from application files and from packages under node_modules. MotifJS marks its own DevTools hooks this way, so this code never makes it into a production bundle, whatever app.useDevelopment() says. The app.useDevelopment(true) line is written with the same mark, so the production bundle contains neither that line nor the DevTools object.

/* @DEV-ONLY */ app.useDevelopment(true);

/* @DEV-ONLY-BEGIN */
app.useReactiveMonitor({ enabled: true, threshold: 200, name: 'app' });
(globalThis as any).app = app;
/* @DEV-ONLY-END */
/* @DEV-ONLY */ app.useDevelopment(true);

/* @DEV-ONLY-BEGIN */
app.useReactiveMonitor({ enabled: true, threshold: 200, name: 'app' });
(globalThis as any).app = app;
/* @DEV-ONLY-END */
/* @DEV-ONLY */ app.useDevelopment(true);

/* @DEV-ONLY-BEGIN */
app.useReactiveMonitor({ enabled: true, threshold: 200, name: 'app' });
globalThis.app = app;
/* @DEV-ONLY-END */
/* @DEV-ONLY */ app.useDevelopment(true);

/* @DEV-ONLY-BEGIN */
app.useReactiveMonitor({ enabled: true, threshold: 200, name: 'app' });
globalThis.app = app;
/* @DEV-ONLY-END */

The single-line mark strips the statement or class member that follows it. To strip several statements or members in a row, use the @DEV-ONLY-BEGIN / @DEV-ONLY-END pair; the pair sits between statements and class members.

Plugin options

compiler({
  diagnostics: false,
  explain: 'pages/Home',
  exclude: 'legacy/',
  storeNames: true,
  production: true,
})
compiler({
  diagnostics: false,
  explain: 'pages/Home',
  exclude: 'legacy/',
  storeNames: true,
  production: true,
})
Option Description
diagnostics With false, compile-time warnings (MJX001–MJX004, MJX015, MJX016) are not printed; on by default (see Compile-time warnings).
explain Prints what every JSX expression compiles to in the matching files: true means every file, a string matches paths that contain it, a RegExp uses test(id) (see What did this expression compile to?).
exclude Files the plugin keeps its hands off: a string matches paths that contain it (includes), a RegExp uses test(id), or an (id) => boolean function. MotifJS does not compile the JSX in these files, which is left to Vite's own transform; in a production build the development-only code inside them is still stripped.
storeNames Names module-level stores for DevTools; on by default in vite dev and off in production builds. In Rollup it is off and is turned on with storeNames: true.
production Strips development-only code; the default is vite build in Vite and NODE_ENV=production elsewhere.

HTML host

The application needs a root element to be mounted into, and a <script> tag loads the entry file:

<!doctype html>
<html>
  <head><meta charset="utf-8" /><title>MotifJS App</title></head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
<!doctype html>
<html>
  <head><meta charset="utf-8" /><title>MotifJS App</title></head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
<!doctype html>
<html>
  <head><meta charset="utf-8" /><title>MotifJS App</title></head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>
<!doctype html>
<html>
  <head><meta charset="utf-8" /><title>MotifJS App</title></head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

First component

A settings page: SettingsService holds the theme and density, writes them to <html> as data-theme / data-density and saves them to localStorage. SettingsPage shows the options as buttons, the selected one is marked with aria-pressed, and a summary line follows the choice. The class, function and Options API forms use the same core, and each is written both with and without JSX.

import { reactive } from "@motifx/core";

export type Theme = "system" | "light" | "dark";
export type Density = "comfortable" | "compact";

export const THEMES: readonly Theme[] = ["system", "light", "dark"];
export const DENSITIES: readonly Density[] = ["comfortable", "compact"];

const STORAGE_KEY = "settings";

export class SettingsService {
  readonly state = reactive({ theme: "system" as Theme, density: "comfortable" as Density });

  constructor() {
    try {
      const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}");
      if (THEMES.includes(saved.theme)) this.state.theme = saved.theme;
      if (DENSITIES.includes(saved.density)) this.state.density = saved.density;
    } catch {}
    this.apply();
  }

  setTheme(theme: Theme) {
    this.state.theme = theme;
    this.save();
  }

  setDensity(density: Density) {
    this.state.density = density;
    this.save();
  }

  private save() {
    this.apply();
    try {
      localStorage.setItem(STORAGE_KEY, JSON.stringify({ theme: this.state.theme, density: this.state.density }));
    } catch {}
  }

  private apply() {
    const root = document.documentElement;
    if (this.state.theme === "system") delete root.dataset.theme;
    else root.dataset.theme = this.state.theme;
    if (this.state.density === "comfortable") delete root.dataset.density;
    else root.dataset.density = this.state.density;
  }
}
import { reactive } from "@motifx/core";

export const THEMES = ["system", "light", "dark"];
export const DENSITIES = ["comfortable", "compact"];

const STORAGE_KEY = "settings";

export class SettingsService {
  state = reactive({ theme: "system", density: "comfortable" });

  constructor() {
    try {
      const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}");
      if (THEMES.includes(saved.theme)) this.state.theme = saved.theme;
      if (DENSITIES.includes(saved.density)) this.state.density = saved.density;
    } catch {}
    this.apply();
  }

  setTheme(theme) {
    this.state.theme = theme;
    this.save();
  }

  setDensity(density) {
    this.state.density = density;
    this.save();
  }

  save() {
    this.apply();
    try {
      localStorage.setItem(STORAGE_KEY, JSON.stringify({ theme: this.state.theme, density: this.state.density }));
    } catch {}
  }

  apply() {
    const root = document.documentElement;
    if (this.state.theme === "system") delete root.dataset.theme;
    else root.dataset.theme = this.state.theme;
    if (this.state.density === "comfortable") delete root.dataset.density;
    else root.dataset.density = this.state.density;
  }
}
import { Component } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES, type Density, type Theme } from "../services/settings";

const LABELS: Record<Theme | Density, string> = { system: "System", light: "Light", dark: "Dark", comfortable: "Comfortable", compact: "Compact" };

export class SettingsPage extends Component {
  settings = this.getService(SettingsService)!;

  override view() {
    return (
      <main class="page">
        <h1>Settings</h1>
        {this.setting("Theme", THEMES, (theme) => this.settings.state.theme === theme, (theme) => this.settings.setTheme(theme))}
        {this.setting("Density", DENSITIES, (density) => this.settings.state.density === density, (density) => this.settings.setDensity(density))}
        <p>{() => `Theme ${LABELS[this.settings.state.theme]}, density ${LABELS[this.settings.state.density]}.`}</p>
      </main>
    );
  }

  private setting<T extends Theme | Density>(name: string, options: readonly T[], isChosen: (option: T) => boolean, choose: (option: T) => void) {
    return (
      <section class="setting">
        <span>{name}</span>
        <div class="seg" role="group" aria-label={name}>
          {options.map((option) => (
            <button key={option} type="button" aria-pressed={() => isChosen(option)} onclick={() => choose(option)}>
              {LABELS[option]}
            </button>
          ))}
        </div>
      </section>
    );
  }
}
import { FromService } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES, type Density, type Theme } from "../services/settings";

const LABELS: Record<Theme | Density, string> = { system: "System", light: "Light", dark: "Dark", comfortable: "Comfortable", compact: "Compact" };

export function SettingsPage() {
  const settings = FromService(SettingsService)!;

  const setting = <T extends Theme | Density>(name: string, options: readonly T[], isChosen: (option: T) => boolean, choose: (option: T) => void) => (
    <section class="setting">
      <span>{name}</span>
      <div class="seg" role="group" aria-label={name}>
        {options.map((option) => (
          <button key={option} type="button" aria-pressed={() => isChosen(option)} onclick={() => choose(option)}>
            {LABELS[option]}
          </button>
        ))}
      </div>
    </section>
  );

  return (
    <main class="page">
      <h1>Settings</h1>
      {setting("Theme", THEMES, (theme) => settings.state.theme === theme, (theme) => settings.setTheme(theme))}
      {setting("Density", DENSITIES, (density) => settings.state.density === density, (density) => settings.setDensity(density))}
      <p>{() => `Theme ${LABELS[settings.state.theme]}, density ${LABELS[settings.state.density]}.`}</p>
    </main>
  );
}
import { FromService, motifOptions } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES, type Density, type Theme } from "../services/settings";

const LABELS: Record<Theme | Density, string> = { system: "System", light: "Light", dark: "Dark", comfortable: "Comfortable", compact: "Compact" };

export const SettingsPage = () => {
  const settings = FromService(SettingsService)!;

  return motifOptions({
    el: "main",

    view() {
      this.class.add("page");
      return (
        <>
          <h1>Settings</h1>
          {this.setting("Theme", THEMES, (theme) => settings.state.theme === theme, (theme) => settings.setTheme(theme))}
          {this.setting("Density", DENSITIES, (density) => settings.state.density === density, (density) => settings.setDensity(density))}
          <p>{() => `Theme ${LABELS[settings.state.theme]}, density ${LABELS[settings.state.density]}.`}</p>
        </>
      );
    },

    setting<T extends Theme | Density>(name: string, options: readonly T[], isChosen: (option: T) => boolean, choose: (option: T) => void) {
      return (
        <section class="setting">
          <span>{name}</span>
          <div class="seg" role="group" aria-label={name}>
            {options.map((option) => (
              <button key={option} type="button" aria-pressed={() => isChosen(option)} onclick={() => choose(option)}>
                {LABELS[option]}
              </button>
            ))}
          </div>
        </section>
      );
    },
  });
};
import { Component, ComponentBase, motifComponent } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES, type Density, type Theme } from "../services/settings";

const LABELS: Record<Theme | Density, string> = { system: "System", light: "Light", dark: "Dark", comfortable: "Comfortable", compact: "Compact" };

export class SettingsPage extends Component {
  settings = this.getService(SettingsService)!;

  override view() {
    return motifComponent("main", (main: ComponentBase) => {
      main.class.add("page");
      const heading = motifComponent("h1", (h1: ComponentBase) => h1.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Settings"))));
      const summary = motifComponent("p", (p: ComponentBase) => {
        p.bindings.text(() => `Theme ${LABELS[this.settings.state.theme]}, density ${LABELS[this.settings.state.density]}.`);
      });
      main.controls.add(
        heading,
        this.setting("Theme", THEMES, (theme) => this.settings.state.theme === theme, (theme) => this.settings.setTheme(theme)),
        this.setting("Density", DENSITIES, (density) => this.settings.state.density === density, (density) => this.settings.setDensity(density)),
        summary,
      );
    });
  }

  private setting<T extends Theme | Density>(name: string, options: readonly T[], isChosen: (option: T) => boolean, choose: (option: T) => void) {
    return motifComponent("section", (section: ComponentBase) => {
      section.class.add("setting");
      const label = motifComponent("span", (span: ComponentBase) => span.controls.add(motifComponent("text", (node: ComponentBase) => node.setText(name))));
      const group = motifComponent("div", (seg: ComponentBase) => {
        seg.class.add("seg");
        seg.attr.add({ role: "group", "aria-label": name });
        for (const option of options) {
          const button = motifComponent("button", (button: ComponentBase) => {
            button.attr.add({ type: "button", "aria-pressed": () => isChosen(option) });
            button.motif.on("click", () => choose(option));
            button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText(LABELS[option])));
          });
          seg.controls.add(button);
        }
      });
      section.controls.add(label, group);
    });
  }
}
import { ComponentBase, FromService, motifComponent } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES, type Density, type Theme } from "../services/settings";

const LABELS: Record<Theme | Density, string> = { system: "System", light: "Light", dark: "Dark", comfortable: "Comfortable", compact: "Compact" };

export function SettingsPage() {
  const settings = FromService(SettingsService)!;

  const setting = <T extends Theme | Density>(name: string, options: readonly T[], isChosen: (option: T) => boolean, choose: (option: T) => void) =>
    motifComponent("section", (section: ComponentBase) => {
      section.class.add("setting");
      const label = motifComponent("span", (span: ComponentBase) => span.controls.add(motifComponent("text", (node: ComponentBase) => node.setText(name))));
      const group = motifComponent("div", (seg: ComponentBase) => {
        seg.class.add("seg");
        seg.attr.add({ role: "group", "aria-label": name });
        for (const option of options) {
          const button = motifComponent("button", (button: ComponentBase) => {
            button.attr.add({ type: "button", "aria-pressed": () => isChosen(option) });
            button.motif.on("click", () => choose(option));
            button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText(LABELS[option])));
          });
          seg.controls.add(button);
        }
      });
      section.controls.add(label, group);
    });

  return motifComponent("main", (main: ComponentBase) => {
    main.class.add("page");
    const heading = motifComponent("h1", (h1: ComponentBase) => h1.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Settings"))));
    const summary = motifComponent("p", (p: ComponentBase) => {
      p.bindings.text(() => `Theme ${LABELS[settings.state.theme]}, density ${LABELS[settings.state.density]}.`);
    });
    main.controls.add(
      heading,
      setting("Theme", THEMES, (theme) => settings.state.theme === theme, (theme) => settings.setTheme(theme)),
      setting("Density", DENSITIES, (density) => settings.state.density === density, (density) => settings.setDensity(density)),
      summary,
    );
  });
}
import { ComponentBase, FromService, motifComponent, motifOptions } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES, type Density, type Theme } from "../services/settings";

const LABELS: Record<Theme | Density, string> = { system: "System", light: "Light", dark: "Dark", comfortable: "Comfortable", compact: "Compact" };

export const SettingsPage = () => {
  const settings = FromService(SettingsService)!;

  return motifOptions({
    el: "main",

    view() {
      this.class.add("page");
      const heading = motifComponent("h1", (h1: ComponentBase) => h1.controls.add(motifComponent("text", (node: ComponentBase) => node.setText("Settings"))));
      const summary = motifComponent("p", (p: ComponentBase) => {
        p.bindings.text(() => `Theme ${LABELS[settings.state.theme]}, density ${LABELS[settings.state.density]}.`);
      });
      return [
        heading,
        this.setting("Theme", THEMES, (theme) => settings.state.theme === theme, (theme) => settings.setTheme(theme)),
        this.setting("Density", DENSITIES, (density) => settings.state.density === density, (density) => settings.setDensity(density)),
        summary,
      ];
    },

    setting<T extends Theme | Density>(name: string, options: readonly T[], isChosen: (option: T) => boolean, choose: (option: T) => void) {
      return motifComponent("section", (section: ComponentBase) => {
        section.class.add("setting");
        const label = motifComponent("span", (span: ComponentBase) => span.controls.add(motifComponent("text", (node: ComponentBase) => node.setText(name))));
        const group = motifComponent("div", (seg: ComponentBase) => {
          seg.class.add("seg");
          seg.attr.add({ role: "group", "aria-label": name });
          for (const option of options) {
            const button = motifComponent("button", (button: ComponentBase) => {
              button.attr.add({ type: "button", "aria-pressed": () => isChosen(option) });
              button.motif.on("click", () => choose(option));
              button.controls.add(motifComponent("text", (node: ComponentBase) => node.setText(LABELS[option])));
            });
            seg.controls.add(button);
          }
        });
        section.controls.add(label, group);
      });
    },
  });
};
import { Component } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES } from "../services/settings";

const LABELS = { system: "System", light: "Light", dark: "Dark", comfortable: "Comfortable", compact: "Compact" };

export class SettingsPage extends Component {
  settings = this.getService(SettingsService);

  view() {
    return (
      <main class="page">
        <h1>Settings</h1>
        {this.setting("Theme", THEMES, (theme) => this.settings.state.theme === theme, (theme) => this.settings.setTheme(theme))}
        {this.setting("Density", DENSITIES, (density) => this.settings.state.density === density, (density) => this.settings.setDensity(density))}
        <p>{() => `Theme ${LABELS[this.settings.state.theme]}, density ${LABELS[this.settings.state.density]}.`}</p>
      </main>
    );
  }

  setting(name, options, isChosen, choose) {
    return (
      <section class="setting">
        <span>{name}</span>
        <div class="seg" role="group" aria-label={name}>
          {options.map((option) => (
            <button key={option} type="button" aria-pressed={() => isChosen(option)} onclick={() => choose(option)}>
              {LABELS[option]}
            </button>
          ))}
        </div>
      </section>
    );
  }
}
import { FromService } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES } from "../services/settings";

const LABELS = { system: "System", light: "Light", dark: "Dark", comfortable: "Comfortable", compact: "Compact" };

export function SettingsPage() {
  const settings = FromService(SettingsService);

  const setting = (name, options, isChosen, choose) => (
    <section class="setting">
      <span>{name}</span>
      <div class="seg" role="group" aria-label={name}>
        {options.map((option) => (
          <button key={option} type="button" aria-pressed={() => isChosen(option)} onclick={() => choose(option)}>
            {LABELS[option]}
          </button>
        ))}
      </div>
    </section>
  );

  return (
    <main class="page">
      <h1>Settings</h1>
      {setting("Theme", THEMES, (theme) => settings.state.theme === theme, (theme) => settings.setTheme(theme))}
      {setting("Density", DENSITIES, (density) => settings.state.density === density, (density) => settings.setDensity(density))}
      <p>{() => `Theme ${LABELS[settings.state.theme]}, density ${LABELS[settings.state.density]}.`}</p>
    </main>
  );
}
import { FromService, motifOptions } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES } from "../services/settings";

const LABELS = { system: "System", light: "Light", dark: "Dark", comfortable: "Comfortable", compact: "Compact" };

export const SettingsPage = () => {
  const settings = FromService(SettingsService);

  return motifOptions({
    el: "main",

    view() {
      this.class.add("page");
      return (
        <>
          <h1>Settings</h1>
          {this.setting("Theme", THEMES, (theme) => settings.state.theme === theme, (theme) => settings.setTheme(theme))}
          {this.setting("Density", DENSITIES, (density) => settings.state.density === density, (density) => settings.setDensity(density))}
          <p>{() => `Theme ${LABELS[settings.state.theme]}, density ${LABELS[settings.state.density]}.`}</p>
        </>
      );
    },

    setting(name, options, isChosen, choose) {
      return (
        <section class="setting">
          <span>{name}</span>
          <div class="seg" role="group" aria-label={name}>
            {options.map((option) => (
              <button key={option} type="button" aria-pressed={() => isChosen(option)} onclick={() => choose(option)}>
                {LABELS[option]}
              </button>
            ))}
          </div>
        </section>
      );
    },
  });
};
import { Component, motifComponent } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES } from "../services/settings";

const LABELS = { system: "System", light: "Light", dark: "Dark", comfortable: "Comfortable", compact: "Compact" };

export class SettingsPage extends Component {
  settings = this.getService(SettingsService);

  view() {
    return motifComponent("main", (main) => {
      main.class.add("page");
      const heading = motifComponent("h1", (h1) => h1.controls.add(motifComponent("text", (node) => node.setText("Settings"))));
      const summary = motifComponent("p", (p) => {
        p.bindings.text(() => `Theme ${LABELS[this.settings.state.theme]}, density ${LABELS[this.settings.state.density]}.`);
      });
      main.controls.add(
        heading,
        this.setting("Theme", THEMES, (theme) => this.settings.state.theme === theme, (theme) => this.settings.setTheme(theme)),
        this.setting("Density", DENSITIES, (density) => this.settings.state.density === density, (density) => this.settings.setDensity(density)),
        summary,
      );
    });
  }

  setting(name, options, isChosen, choose) {
    return motifComponent("section", (section) => {
      section.class.add("setting");
      const label = motifComponent("span", (span) => span.controls.add(motifComponent("text", (node) => node.setText(name))));
      const group = motifComponent("div", (seg) => {
        seg.class.add("seg");
        seg.attr.add({ role: "group", "aria-label": name });
        for (const option of options) {
          const button = motifComponent("button", (button) => {
            button.attr.add({ type: "button", "aria-pressed": () => isChosen(option) });
            button.motif.on("click", () => choose(option));
            button.controls.add(motifComponent("text", (node) => node.setText(LABELS[option])));
          });
          seg.controls.add(button);
        }
      });
      section.controls.add(label, group);
    });
  }
}
import { FromService, motifComponent } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES } from "../services/settings";

const LABELS = { system: "System", light: "Light", dark: "Dark", comfortable: "Comfortable", compact: "Compact" };

export function SettingsPage() {
  const settings = FromService(SettingsService);

  const setting = (name, options, isChosen, choose) =>
    motifComponent("section", (section) => {
      section.class.add("setting");
      const label = motifComponent("span", (span) => span.controls.add(motifComponent("text", (node) => node.setText(name))));
      const group = motifComponent("div", (seg) => {
        seg.class.add("seg");
        seg.attr.add({ role: "group", "aria-label": name });
        for (const option of options) {
          const button = motifComponent("button", (button) => {
            button.attr.add({ type: "button", "aria-pressed": () => isChosen(option) });
            button.motif.on("click", () => choose(option));
            button.controls.add(motifComponent("text", (node) => node.setText(LABELS[option])));
          });
          seg.controls.add(button);
        }
      });
      section.controls.add(label, group);
    });

  return motifComponent("main", (main) => {
    main.class.add("page");
    const heading = motifComponent("h1", (h1) => h1.controls.add(motifComponent("text", (node) => node.setText("Settings"))));
    const summary = motifComponent("p", (p) => {
      p.bindings.text(() => `Theme ${LABELS[settings.state.theme]}, density ${LABELS[settings.state.density]}.`);
    });
    main.controls.add(
      heading,
      setting("Theme", THEMES, (theme) => settings.state.theme === theme, (theme) => settings.setTheme(theme)),
      setting("Density", DENSITIES, (density) => settings.state.density === density, (density) => settings.setDensity(density)),
      summary,
    );
  });
}
import { FromService, motifComponent, motifOptions } from "@motifx/core";
import { DENSITIES, SettingsService, THEMES } from "../services/settings";

const LABELS = { system: "System", light: "Light", dark: "Dark", comfortable: "Comfortable", compact: "Compact" };

export const SettingsPage = () => {
  const settings = FromService(SettingsService);

  return motifOptions({
    el: "main",

    view() {
      this.class.add("page");
      const heading = motifComponent("h1", (h1) => h1.controls.add(motifComponent("text", (node) => node.setText("Settings"))));
      const summary = motifComponent("p", (p) => {
        p.bindings.text(() => `Theme ${LABELS[settings.state.theme]}, density ${LABELS[settings.state.density]}.`);
      });
      return [
        heading,
        this.setting("Theme", THEMES, (theme) => settings.state.theme === theme, (theme) => settings.setTheme(theme)),
        this.setting("Density", DENSITIES, (density) => settings.state.density === density, (density) => settings.setDensity(density)),
        summary,
      ];
    },

    setting(name, options, isChosen, choose) {
      return motifComponent("section", (section) => {
        section.class.add("setting");
        const label = motifComponent("span", (span) => span.controls.add(motifComponent("text", (node) => node.setText(name))));
        const group = motifComponent("div", (seg) => {
          seg.class.add("seg");
          seg.attr.add({ role: "group", "aria-label": name });
          for (const option of options) {
            const button = motifComponent("button", (button) => {
              button.attr.add({ type: "button", "aria-pressed": () => isChosen(option) });
              button.motif.on("click", () => choose(option));
              button.controls.add(motifComponent("text", (node) => node.setText(LABELS[option])));
            });
            seg.controls.add(button);
          }
        });
        section.controls.add(label, group);
      });
    },
  });
};

In the example:

Mount the application

The Application class is the application's entry point. The usual flow:

  1. Create a builder with Application.CreateBuilder().
  2. Register services on builder.services if needed (see Introduction to Dependency Injection).
  3. Produce the Application instance with builder.build().
  4. Optionally define routing with app.useRouter(...).
  5. Mount the application on a DOM element with app.run(host, rootComponent).

A class component is passed as an instance; it can also be built with the <SettingsPage /> tag in JSX or with motifComponent(SettingsPage) when writing without JSX. Function and Options components are written as tags in JSX; without JSX, a function component is called and an Options component is built with motifComponent(SettingsPage):

import { Application } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", new SettingsPage());
import { Application } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", <SettingsPage />);
import { Application } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", new SettingsPage());
import { Application } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", SettingsPage());
import { Application, motifComponent } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", motifComponent(SettingsPage));
import { Application } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", new SettingsPage());
import { Application } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", <SettingsPage />);
import { Application } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", new SettingsPage());
import { Application } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", SettingsPage());
import { Application, motifComponent } from "@motifx/core";
import { SettingsPage } from "./pages/SettingsPage";
import { SettingsService } from "./services/settings";

const builder = Application.CreateBuilder();
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
app.run("#app", motifComponent(SettingsPage));

The first argument of app.run accepts a CSS selector ("#app"), an HTMLElement or a Node; if the selector matches nothing, an MJX406 error is thrown. The second argument is the root component. The host's existing content is kept, and the root component is appended after it. If the second argument is left out, MotifJS places a RouterView at the root by itself (for router-based applications; see below). Application.CreateBuilder() is called once per application; a second call throws MJX405 (it can be called again after app.dispose()).

Router-based startup

In multi-page applications, use routing instead of a root component. In that case do not pass a second argument to run; MotifJS adds the root RouterView itself. When a second argument is given, the root is your component and you put the RouterView in its view yourself:

import { Application } from "@motifx/core";
import { routes } from "./routes";

const app = Application.CreateBuilder().build();

/* @DEV-ONLY */ app.useDevelopment(true);
app.useLogging(true);

app.useRouter({
  routes,
  mode: "history",
  fallbacks: {
    notFound: () => import("./pages/NotFoundPage"),
  },
});

app.run("#app");
import { Application } from "@motifx/core";
import { routes } from "./routes";

const app = Application.CreateBuilder().build();

/* @DEV-ONLY */ app.useDevelopment(true);
app.useLogging(true);

app.useRouter({
  routes,
  mode: "history",
  fallbacks: {
    notFound: () => import("./pages/NotFoundPage"),
  },
});

app.run("#app");

useDevelopment turns on development mode: MJX warnings, route checks when it is called before useRouter, and DevTools. useLogging turns on console error logging (on by default). The mode values are 'history' | 'hash' | 'file' | 'shell'. Route definitions are covered in detail on the Route Table page, and placing a RouterView on the RouterView page. The module loaded through notFound must export the page as default; for a named export, map it: notFound: () => import("./pages/NotFoundPage").then((m) => m.NotFoundPage). A module without a default export is reported as MJX127 (the cause of MJX304), and the error view is shown instead of the 404 page (see Lazy loading).

Development helpers

Application has chainable configuration methods:

Method Description
app.useDevelopment(true) Turns on development mode: MJX warnings are written to the console (console.warn), and so are errors the framework catches internally and passes over quietly; when called before useRouter, route definitions are checked. MJX warnings are not buffered: warnings raised before development mode is turned on (other than MJX121) are not printed later, which is why useDevelopment is called before run. DevTools is turned on as well: the globalThis.__MOTIF_DEVTOOLS__ object is created (The DevTools Contract); useDevelopment(false) turns DevTools off and removes the object. Read it with app.isDevelopmentModeEnabled. In a production build the core's DevTools hooks are stripped from the code (Development-only code), so DevTools produces no data.
app.useLogging(true) Turns writing errors to the console on or off (on by default). It does not turn off MJX warnings; those follow development mode (useDevelopment).
app.useReactiveMonitor({ enabled: true, threshold: 200, name: 'app' }) Turns on tracking of reactive dependency leaks (recommended during development; when the threshold is exceeded an MJX507 warning is printed, and this warning only shows while useDevelopment(true) is on; see Leak tracking).
app.useTransitions({ mode: 'out-in' }) Picks the default mode for enter/leave transitions (see Enter/leave mode).

Errors thrown in hooks (MJX122), event handlers (MJX123), effect/binding functions (MJX208) and using handlers (MJX129) are reported regardless of development mode, in production too: they are written to console.error (unless useLogging(false)) and passed to errorHandler.addListener(fn) listeners as a MotifError (error.code, error.cause; see Error Handling). The application keeps running.

Next step

To build the parts of the application, go on to Components.