Getting started
Build a working rooted app from scratch. By the end you have a component that mounts, an app shell, and (optionally) a route.
This guide assumes Vite. Rooted does not require Vite at runtime, but the route discovery plugin and the CSS loader plugin do.
1. Install
pnpm add @rooted/components
pnpm add -D vite typescriptAdd the rest as you need them: @rooted/router for routing, @rooted/store for shared state, @rooted/storage for localStorage and friends.
2. Set up the page
Rooted mounts the app inside an element on the page. The default selector is #app.
<!-- index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My app</title>
<link rel="stylesheet" href="/index.tokens.css">
<link rel="stylesheet" href="/index.theme.css">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.mts"></script>
</body>
</html>The two stylesheets are the app's global CSS: index.tokens.css holds design values as CSS custom properties, index.theme.css holds app-global styles like fonts and the #app element. They're linked here so they load before any JavaScript. Step 5 covers component styles, Styling covers both files.
3. Write a component
// src/greeting.mts
import { component } from '@rooted/components'
export const Greeting = component({
name: 'greeting',
onMount({ append, element }) {
append(element('p', { textContent: 'Hello from rooted!' }))
},
})A component is a plain object with a name and an onMount. The runtime wraps it in a custom element when the component is mounted. The name must be unique across the whole app.
4. Bootstrap the app
// src/main.mts
import { application } from '@rooted/components/application'
import { component } from '@rooted/components'
import { Greeting } from './greeting.mts'
const App = component({
name: 'app',
onMount({ append, element }) {
append(element('h1', { textContent: 'My App' }))
append(Greeting)
},
})
application(App)application(App) looks for #app and mounts the app root inside it, replacing whatever it held. The #app element itself stays, which is what lets a pre-rendered page find it again in the browser. You can pass { selector: '#root' } or { element: someNode } if you want something different.
5. Add styles
CSS lives in .css files next to the component. Import the file as a CSS module and pass it to styles.
// src/greeting.mts
import { component } from '@rooted/components'
import styles from './greeting.css'
export const Greeting = component({
name: 'greeting',
styles,
onMount({ append, element }) {
append(element('p', { classes: styles.message, textContent: 'Hello!' }))
},
})/* src/greeting.css */
:scope {
display: block;
padding: 1rem;
}
.message {
color: tomato;
}:scope is the host element of the component. Class names are scoped to this component only.
For the full CSS layering story, see Styling.
6. Add a route (optional)
Routes live in _routes.mts files next to the components they render. The Vite plugin discovers them at build time and writes a generated file that imports every route at once.
// src/articles/_routes.mts
import { route, token } from '@rooted/router/routes'
export const ArticleRoute = route`/articles/${token('id', Number)}/`({
async resolve({ create, tokens }) {
const { Article } = await import('./article.mts')
return create(Article, {
id: tokens.id
})
},
})// vite.config.mts
import { defineConfig } from 'vite'
import { generateRouteManifest } from '@rooted/router/manifest'
export default defineConfig({
plugins: [
generateRouteManifest({
glob: './src/**/_routes.mts',
routeManifestPath: './src/_routes.g.mts',
}),
],
})The plugin writes src/_routes.g.mts on build, so it isn't there yet on a fresh checkout. Reference the ambient types so TypeScript still resolves the import:
// src/vite-env.d.ts
/// <reference types="@rooted/router/types" />// src/main.mts
import { application } from '@rooted/components/application'
import { component } from '@rooted/components'
import { router } from '@rooted/router/application'
import { Home } from './home.mts'
import { NotFound } from './not-found.mts'
import { appRoutes } from './_routes.g.mts' // generated by the plugin
const Router = router({
home: Home,
notFound: NotFound,
...appRoutes,
})
application(
component({
name: 'app',
onMount({ append }) {
append(Router)
},
})
)For the full router story, see Routing.
Where to next
- Application model. The vertical-slice layout rooted apps use, and how the pieces fit together.
- Components. Everything
component()and the mount context can do. - FAQ. Things that trip people up.