Skip to content

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 ​

sh
pnpm add @rooted/components
pnpm add -D vite typescript

Add 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.

html
<!-- 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 ​

ts
// 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 ​

ts
// 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.

ts
// 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!' }))
  },
})
css
/* 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.

ts
// 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
    })
  },
})
ts
// 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:

ts
// src/vite-env.d.ts
/// <reference types="@rooted/router/types" />
ts
// 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.