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.
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#
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:
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:
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:
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.
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.