Quick start
Create an app, run it, and change a component.
You need Node.js 22.12 or newer and npm.
npm create shadow-app@latest my-app
cd my-app
npm install
npm run dev
Vite prints the address. Open src/App.tsx and change something: the page updates without a reload.
The command asks for a few choices (Tailwind, Biome and other add-ons). Flags answer them up front, after --:
npm create shadow-app@latest my-app -- --tailwind --no-biome
--yes accepts the defaults and --help lists every option. The folder must be missing or empty. Pressing Ctrl-C removes whatever the command created.
The starter#
This is src/App.tsx as generated. It runs here:
Show the code
import { useMemo, useStore } from "@shadow-js/core";
import { For, Show } from "@shadow-js/web";
export function App() {
const [count, setCount] = useStore(0);
const squares = useMemo(() => Array.from({ length: Math.min(count(), 5) }, (_, i) => (i + 1) ** 2));
return (
<main class="app">
<h1 class="title">ShadowJS</h1>
<p>Count: {count()}</p>
<button type="button" class="button" onClick={() => setCount((n) => n + 1)}>
Increment
</button>
<Show when={count() > 0} fallback={<p>Press the button.</p>}>
<ul>
<For each={squares()}>{(value) => <li>{value}</li>}</For>
</ul>
</Show>
</main>
);
}
What the command writes#
| Path | What it holds |
|---|---|
package.json |
@shadow-js/core and @shadow-js/web, and @shadow-js/vite, vite and typescript for development. Scripts dev, build, preview and typecheck. |
vite.config.ts |
defineConfig({ plugins: [shadow()] }). |
tsconfig.json |
Strict TypeScript with "jsx": "preserve" and "jsxImportSource": "@shadow-js/web". |
index.html, src/main.tsx |
A <div id="app"> and the script that calls render(() => <App />, root). |
src/App.tsx, src/index.css |
The starter component and its styles. |
Add it to a Vite project#
Install the runtime and the build tools:
npm install @shadow-js/core @shadow-js/web
npm install --save-dev @shadow-js/vite vite typescript
Register the plugin. It compiles .jsx and .tsx files before Vite's own transforms run:
import shadow from "@shadow-js/vite";
import { defineConfig } from "vite";
export default defineConfig({ plugins: [shadow()] });
Tell TypeScript where the JSX types live. The compiler does the transform, so "preserve" leaves the JSX alone:
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "@shadow-js/web",
"strict": true,
"moduleResolution": "Bundler",
"noEmit": true,
"types": ["vite/client"]
}
}
Give index.html a container and mount the app:
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
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 returns a function that removes what it inserted.
Next#
The mental model explains what runs once and what runs again. Project structure shows where each package fits, and Configuration lists every option.