Tutorial 1: Create your first IWC project
Objective: create an IWC project, run it with live reload, understand every file the starter contains, and make your first change.
Prerequisites: IWC installed (iwc --version prints a version — see Installation). About 10 minutes.
Step 1: Create the project
cd ~/Projects
iwc create hello --yes
cd hello--yes skips the question about targets and creates a web app. Without it, iwc create asks whether you also want an admin area and Android or iOS apps.
✓ Created hello from the basic template — targets: web
cd hello
iwc dev --open # webStep 2: Look at what was created
hello/
├── assets/
│ └── logo.svg
├── components/
│ └── footer.component.iwc
├── layouts/
│ └── main.layout.iwc
├── state/
│ └── counter.state.iwc
├── styles/
│ └── main.css
├── tests/
│ ├── menu.test.iwc
│ └── smoke.test.iwc
├── ui/
│ ├── about.ui.iwc
│ ├── home.ui.iwc
│ ├── menu.ui.iwc
│ └── not-found.ui.iwc
├── app.iwc
├── package.iwc
└── README.mdThe starter also writes .vscode/ (editor settings and JSON Schemas for autocompletion) and a .gitignore. The ui/menu.ui.iwc and tests/menu.test.iwc files are added in Tutorial 2.
app.iwc
The application file. It is the only file every project must have.
{
"name": "Hello",
"iwc": 1,
"theme": {
"tokens": {
"color-primary": "#5b4ff5",
"color-on-primary": "#ffffff",
"color-text": "#14141f",
"color-muted": "#5d6073",
"color-background": "#ffffff",
"color-surface": "#f6f6fb",
"color-border": "#e6e6ef",
"radius": "12px",
"font-body": "Inter, system-ui, -apple-system, 'Segoe UI', sans-serif",
"font-mono": "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
},
"dark": {
"color-primary": "#8b7fff",
"color-text": "#ececf4",
"color-muted": "#a2a4b8",
"color-background": "#0b0b13",
"color-surface": "#15151f",
"color-border": "#26263a"
},
"fonts": [
{ "family": "Inter", "google": true, "weights": [400, 500, 600, 700] }
]
},
"styles": ["styles/main.css"],
"head": {
"title": "%s · Hello",
"description": "Hello, a new IWC application.",
"favicon": "/assets/logo.svg"
},
"notFound": "not-found.ui.iwc",
"platforms": {
"web": {}
}
}| Property | What it does |
|---|---|
name | The application's name (shown in titles and native apps) |
iwc | The file-format version |
theme.tokens | Design tokens, emitted as CSS variables: color-primary becomes var(--color-primary) |
theme.dark | Tokens that replace the defaults when the visitor's system is in dark mode |
theme.fonts | Fonts to load; google: true loads them from Google Fonts |
styles | Your stylesheets, in load order |
head | <title> template (%s is each screen's title), meta description, favicon |
notFound | The screen shown for unknown routes |
platforms | The application targets: here, the web |
ui/home.ui.iwc
A screen. route is its URL, layout the frame around it, and sections its content: a tree of elements.
{
"name": "Home",
"route": "/",
"layout": "main.layout.iwc",
"title": "Welcome",
"sections": [
{
"type": "section",
"className": "welcome",
"elements": [
{
"type": "column",
"className": "welcome-inner",
"layout": { "direction": "column", "align": "center", "gap": "20px" },
"elements": [
{ "type": "icon", "source": "/assets/logo.svg", "ariaLabel": "IWC", "className": "welcome-logo" },
{ "type": "text", "variant": "span", "content": "IWC v0.1.0", "className": "version" },
{ "type": "text", "variant": "title", "content": "Welcome to IWC" },
{
"type": "text",
"className": "lead",
"content": "Hello is a new IWC application. Its screens, state and theme are plain .iwc files, built for every platform in app.iwc."
},
{
"type": "column",
"className": "counter",
"layout": { "direction": "column", "align": "center", "gap": "12px" },
"elements": [
{ "type": "text", "variant": "caption", "content": "Shared behaviour: state/counter.state.iwc" },
{ "type": "text", "variant": "h2", "bind": "state.counter.count", "prefix": "Count: ", "className": "count" },
{
"type": "row",
"layout": { "justify": "center", "align": "center", "gap": "12px" },
"elements": [
{ "type": "button", "label": "Increment", "action": "state", "store": "counter", "mutation": "increment" },
{ "type": "button", "label": "Reset", "variant": "secondary", "action": "state", "store": "counter", "mutation": "reset" }
]
}
]
},
{
"type": "column",
"className": "next-step",
"layout": { "direction": "column", "align": "center", "gap": "8px" },
"elements": [
{ "type": "text", "variant": "caption", "content": "Get started by editing" },
{ "type": "text", "variant": "span", "content": "ui/home.ui.iwc", "className": "file" },
{ "type": "text", "variant": "caption", "content": "Save the file and every running target rebuilds." }
]
},
{
"type": "row",
"className": "actions",
"layout": { "justify": "center", "align": "center", "gap": "12px", "wrap": true },
"elements": [
{ "type": "button", "label": "About this app", "variant": "secondary", "action": "navigate", "target": "/about" },
{ "type": "button", "label": "Read the documentation", "action": "navigate", "target": "/_iwc/docs/", "platforms": ["web"] }
]
}
]
}
]
}
]
}Things to notice:
"type": "column"with"layout": {"direction": "column", "align": "center", "gap": "20px"}is a flexbox column."bind": "state.counter.count"shows a value from the client-side store instate/counter.state.iwc; the two buttons run itsincrementandresetmutations."platforms": ["web"]keeps the documentation button out of native apps.
state/counter.state.iwc
{
"name": "counter",
"persist": "session",
"initial": { "count": 0 },
"mutations": {
"increment": { "op": "increment", "path": "count" },
"reset": { "op": "clear", "path": "count" }
}
}A browser-side store. persist: "session" keeps the count while the tab is open. See Client state.
layouts/main.layout.iwc and components/footer.component.iwc
{
"name": "MainLayout",
"slots": {
"content": "PAGE_CONTENT",
"footer": "footer.component.iwc"
},
"className": "app-root"
}{
"name": "Footer",
"root": {
"type": "row",
"className": "app-footer",
"layout": { "justify": "center", "align": "center", "gap": "6px", "wrap": true },
"elements": [
{ "type": "text", "variant": "caption", "content": "Built with IWC" },
{ "type": "text", "variant": "caption", "content": "·", "className": "dot", "platforms": ["web"] },
{ "type": "link", "label": "Documentation", "target": "/_iwc/docs/", "platforms": ["web"] }
]
}
}The layout has two slots, rendered in order: the page's own content (PAGE_CONTENT) and a footer component. Every screen that says "layout": "main.layout.iwc" gets the same frame.
tests/smoke.test.iwc
{
"name": "smoke",
"cases": [
{ "name": "home renders the welcome page", "request": { "path": "/" }, "expect": { "status": 200, "contains": ["Welcome to IWC", "ui/home.ui.iwc"] } },
{ "name": "home has the shared counter", "request": { "path": "/" }, "expect": { "status": 200, "contains": ["Increment", "Reset", "data-iwc-state=\"counter.count\""] } },
{ "name": "home links the about screen and the documentation", "request": { "path": "/" }, "expect": { "status": 200, "contains": ["href=\"/about\"", "/_iwc/docs/"] } },
{ "name": "the about screen renders", "request": { "path": "/about" }, "expect": { "status": 200, "contains": ["One app, every platform", "Back to home"] } },
{ "name": "the documentation is served", "request": { "path": "/_iwc/docs/getting-started" }, "expect": { "status": 200, "contains": ["iwc dev"] } },
{ "name": "unknown routes render the 404 screen", "request": { "path": "/no/such/page" }, "expect": { "status": 404, "contains": ["This page doesn"] } }
]
}Each case sends a request to the app and checks the status and what the response contains.
Step 3: Run it
iwc dev --openIWC Development Server
✓ Project validated
✓ Runtime initialized
✓ Port 3000 available
✓ Application compiled (9 files in 10ms · 3 screens · 0 apis · 0 schemas)
✓ Hot reload enabled
Local: http://localhost:3000
Docs: http://localhost:3000/_iwc/docs/
Project: /Users/you/Projects/hello
Logs: .iwc/logs/dev.log (iwc logs -f)
Watching for file changes... (Ctrl+C stops)Your browser opens the welcome page. Press Increment a few times, then open About this app: the count is shared between screens because it lives in the store.
Step 4: Change something
In ui/home.ui.iwc, change the title text:
{ "type": "text", "variant": "title", "content": "Hello from my first IWC app" }Save. The page updates in place, keeping its scroll position and the counter. Now make a mistake on purpose — change "variant": "title" to "variant": 12 — and save. The terminal shows the error with its position, and the page shows an overlay until you fix it.
Step 5: Check and test
iwc check
iwc test✓ targets: web
✓ 9 files · 3 screens · 1 layouts · 1 components · 0 apis · 0 schemas · 0 actions
✓ smoke › home renders the welcome page
✓ smoke › home has the shared counter
✓ smoke › home links the about screen and the documentation
✓ smoke › the about screen renders
✓ smoke › the documentation is served
✓ smoke › unknown routes render the 404 screen
6 passed, 0 failedIf you changed the title in step 4, the first test now fails, because it expects the text "Welcome to IWC". That is the point of tests: update the expectation in tests/smoke.test.iwc to your new title.
Troubleshooting
| Problem | Fix |
|---|---|
hello already exists | Pick another name or remove the folder. |
| The browser does not open | Open the Local: URL printed by iwc dev. |
Port 3000 is already occupied | iwc dev moves to the next free port and prints it. |
| Changes don't appear | Check the terminal for an error; the page shows an overlay while one exists. |
Next: Build your first UI page.
Source: apps/marketing/content/docs/tutorials/first-project.md