Skip to content
JSX / compiledstatic templateprecise DOM ops

Build in
sunlight.

Sol compiles familiar JSX into static HTML templates and fine-grained DOM operations. Setup runs once. Updates land exactly where data changed.

bun add @soljs/sol
DOMonly what changes
Block / 01Writable statelet count = 0
Block / 02Computedcount * 2
Block / 03Binding$bind={state}

01 / The mechanism

Familiar in.
Focused out.

The compiler traces dependencies before code reaches the browser. The runtime gets a static template and only the operations needed to keep it current.

  1. 01

    Write a component

    Plain declarations, derived values, JSX.

    $component(fn)
  2. 02

    Compile the graph

    Static template plus dependency-aware DOM work.

    ↘ ↗
  3. 03

    Patch with precision

    No component rerun; dependent DOM updates directly.

    mounted

02 / Working assemblies

Code with gravity.

Switch between code, preview, or both. Every preview is compiled by Sol and keeps its state while the panels move.

01 / Reactivity

Normal reads. Direct updates.

CounterExample.tsx
import { $component } from "@soljs/sol";export const CounterExample = $component(function CounterExample() {  let count = 0;  const doubled = count * 2;  return (    <section aria-label="Reactive counter">      <h4>Reactive output</h4>      <output aria-live="polite">{count}</output>      <p>doubled / {doubled}</p>      <footer>        <button type="button" disabled={count === 0} onClick={() => count--}>        </button>        <button type="button" onClick={() => count++}>          Add one        </button>      </footer>    </section>  );});

Reactive output

0

doubled / 0

02 / Deep state

Mutate the data you own.

ListExample.tsx
import { $component } from "@soljs/sol";export const ListExample = $component(function ListExample() {  let items = [    { id: 1, label: "Static template", ready: true },    { id: 2, label: "Dependency graph", ready: false },  ];  return (    <section aria-label="Compiler assembly list">      <ul>        {items.map((item) => (          <li key={item.id}>            <button              type="button"              aria-pressed={item.ready}              onClick={() => (item.ready = !item.ready)}            >              <strong>{item.label}</strong>              <small>{item.ready ? "Ready" : "Draft"}</small>            </button>          </li>        ))}      </ul>      <button        type="button"        onClick={() =>          items.push({            id: items.length + 1,            label: `DOM operation ${items.length + 1}`,            ready: false,          })        }      >        Add block      </button>    </section>  );});
03 / Validation

Parsed output crosses the boundary.

FormExample.tsx
import { $component, $form } from "@soljs/sol";import * as v from "valibot";const emailSchema = v.object({  email: v.pipe(v.string(), v.trim(), v.email("Enter a valid email address.")),});export const FormExample = $component(function FormExample() {  let submitted = "";  const form = $form(    {      schema: v.parser(emailSchema),      defaultValues: { email: "" },      validationStrategy: "onSubmit",    },    (values) => {      submitted = values.email;    },  );  return (    <form $form={form}>      <label for="landing-email">Email address</label>      <input        id="landing-email"        name="email"        type="email"        $bind={form.values.email}        aria-invalid={Boolean(form.errors.email)}        aria-describedby="landing-email-error"        placeholder="you@example.com"      />      <p id="landing-email-error" role="alert">        {form.errors.email?.[0] ?? ""}      </p>      <button type="submit" disabled={form.isSubmitting}>        Validate      </button>      <p role="status" aria-live="polite">        {submitted ? `Accepted: ${submitted}` : "Only parsed output reaches submit."}      </p>    </form>  );});

Only parsed output reaches submit.

Sol

Experimental JSX framework / Sunblock system

Static blocks + precise motion