[]previous and next page

1. Set up the project

Create a ShadowJS app with create-shadow-app, start the dev server and replace the starter page with the first lines of the todo app.

For agentsUpdated

Goal: a running app whose page says "Todos", and a clear idea of what each generated file does. The Quick start covers adding ShadowJS to an existing Vite project instead.

Build a todo app with state, a form, filters, persistence and server rendering. Each step adds one part to the same app.

Requirements#

Node.js 22.12 or newer and npm. The generated project uses Vite 8 and TypeScript 7.

Create the app#

sh
npm create shadow-app@latest todo-app

In a terminal the command asks whether to add Tailwind CSS (default no) and whether to add Biome (default yes); without a directory argument it asks for the directory too. Pass the answers as flags to skip the prompts. Flags go after -- when you use npm create:

sh
npm create shadow-app@latest todo-app -- --no-tailwind --biome
Flag Effect
--tailwind / --no-tailwind Add Tailwind CSS 4 through its Vite plugin.
--biome / --no-biome Add Biome as linter and formatter.
-y, --yes Accept the defaults without prompting.

The command refuses to write into a directory that already has files in it. Then install and start:

sh
cd todo-app
npm install
npm run dev

Vite prints a local URL. The starter page is a counter; you replace it in a moment.

What was generated#

File Purpose
index.html Has <div id="app"> and loads /src/main.tsx as a module.
vite.config.ts Registers the ShadowJS plugin, which compiles JSX.
tsconfig.json Strict TypeScript with "jsx": "preserve" and "jsxImportSource": "@shadow-js/web".
src/main.tsx Entry point: mounts the app into #app.
src/App.tsx The root component.
src/index.css Global styles.
package.json Scripts dev, build, preview, typecheck (and lint, format with Biome).

The three settings that make JSX work are covered in Configuration. The layout of a whole app is described in Project structure.

Replace the starter component#

Open src/App.tsx, delete the counter and write:

src/App.tsxtsx
export function App() {
  return (
    <main>
      <h1>Todos</h1>
    </main>
  );
}

A component is a plain function that returns JSX. There is nothing to import for JSX itself: the compiler turns the markup into DOM operations, and jsxImportSource only tells TypeScript which element types to check against.

The entry point#

The starter src/main.tsx already does what the app needs: it finds the element, fails loudly if it is missing, and calls render. It also imports ./index.css; keep that line if your project has it.

src/main.tsxtsx
import { render } from "@shadow-js/web";
import { App } from "./App";

const root = document.getElementById("app");
if (root === null) throw new Error("Missing #app element");

render(() => <App />, root);

render runs the function once, inserts the result into the container and returns a function that removes everything again. The function you pass builds the tree a single time; nothing calls it again when data changes.

What you should see#

The page shows the heading "Todos". The next step adds a component with state.

Next: 2. Write the first component.