Skip to main content
This guide walks you through building two components from scratch: a Button with a typed prop contract, then a Counter that adds reactive state with useState. Along the way you’ll see how Solar validates props at the boundary, what a ContractError looks like when you pass the wrong type, and how to inspect your component registry. By the end, you’ll have a working Solar project and a clear mental model of how the framework’s core pieces fit together.
1

Create your project

Scaffold a new Solar project with the CLI, then start the dev server:
npm create solar@latest my-app
cd my-app
npm run dev
Open http://localhost:3000. You’ll see the starter App component running. Now open components/App.js — that’s where you’ll work for the rest of this guide.
2

Define your first component

Replace the contents of components/Button.js (create it if it doesn’t exist) with the following. This is a complete Solar component: it declares a prop schema, renders to the DOM using createElement, registers itself, and exports its definition.
import { defineComponent, createElement, registry } from '../solar/index.js'

const Button = defineComponent({
  name: 'Button',
  props: {
    label:   { type: 'string',   required: true },
    onClick: { type: 'function', required: true },
    variant: { type: 'string',   enum: ['primary', 'secondary'], default: 'primary' },
  },
  render({ label, onClick, variant }) {
    return createElement('button', { class: `btn btn--${variant}`, onclick: onClick }, label)
  },
})

registry.register(Button)
export default Button
The props object is your component’s contract. Every key declares a type, and optionally required: true, an enum of allowed values, or a default. Solar validates every prop at call time — before render ever runs.
3

Mount it to the DOM

Open main.js and mount your Button to the page. mountComponent takes the component function, a props object, and a DOM container, and returns a numeric id you can later pass to unmountComponent to remove the component.
import { mountComponent } from './solar/index.js'
import Button from './components/Button.js'

mountComponent(
  Button,
  {
    label: 'Click me',
    onClick: () => console.log('clicked'),
    variant: 'primary',
  },
  document.getElementById('app'),
)
Save the file and check your browser. The button renders and logs to the console when clicked.
4

Add state with useState

Create components/Counter.js. This component uses useState to track a count value and re-renders whenever it changes.
import { defineComponent, createElement, useState, registry } from '../solar/index.js'

const Counter = defineComponent({
  name: 'Counter',
  props: {
    initialCount: { type: 'number', default: 0 },
  },
  render({ initialCount }) {
    const [count, setCount] = useState(initialCount)

    return createElement('div', {},
      createElement('p', {}, `Count: ${count}`),
      createElement('button', { onclick: () => setCount(c => c + 1) }, '+'),
      createElement('button', { onclick: () => setCount(c => c - 1) }, '-'),
    )
  },
})

registry.register(Counter)
export default Counter
Mount it in main.js alongside the button:
import { mountComponent } from './solar/index.js'
import Counter from './components/Counter.js'

mountComponent(Counter, { initialCount: 5 }, document.getElementById('app'))
useState returns the current value and a setter function. Pass the setter a new value, or a function that receives the previous value and returns the next one — both forms work.
5

See contract validation in action

Pass the wrong type for a required prop and Solar throws a ContractError immediately — before any rendering happens.
import Button from './components/Button.js'

try {
  Button({ label: 42, onClick: () => {} })
} catch (e) {
  console.log(JSON.stringify(e.toJSON(), null, 2))
}
The error’s .toJSON() method returns a structured object:
{
  "error": "ContractError",
  "component": "Button",
  "prop": "label",
  "expected": "string",
  "received": "number",
  "fix": "Pass a string value for \"label\"",
  "message": "Button: prop \"label\": expected string, got number"
}
Every field is machine-readable. An AI agent can inspect prop, expected, and fix directly and regenerate the correct call without parsing a freeform error string.
Call registry.manifest() at any point to get a complete JSON catalog of every registered component and its prop schema. This is the same call an AI model makes before generating composition code — it shows exactly which components exist, what props they accept, which are required, and what defaults and enums are in play.
import { registry } from './solar/index.js'

console.log(registry.manifest())

Next steps

Now that you have a working component with contract validation and state, explore the rest of Solar’s building blocks.

Components

Learn how defineComponent, slots, and typed composition work in depth.

Contracts

Understand the full prop schema syntax, enum validation, slot types, and ContractError structure.