Light. Fast. Reactive.

Sinuous is a low-level UI library with a tiny footprint.

  • Less code

    Sinuous keeps your bundle size down, a basic counter is just 1.4kB. This makes it an ideal library to use in embeds, components or UI widgets. Don't be afraid, Sinuous will also render large web apps.

    One of the goals is to keep that plain Javascript feel. Views are either defined in native tagged template literals or in h function calls.

  • Performant

    Blazing! Sinuous is powered by fine-grained DOM operations. It's in good company with libraries like Surplus, Solid and Svelte which use the same technique.

    On top of that Sinuous provides a template add-on that can pre-render repetitive HTML snippets which makes a big difference in performance.

  • Declarative

    All this with sweet declarative views, the primary goal of most UI libraries. This makes your code clear, more predictable and easier to debug.

    As the application state changes in the observables, Sinuous will update and render the view with surgical precision at speeds matching those of direct DOM manipulations.


A Simple Component

A familiar example which needs little explanation.

The only thing to point out here is that the html tag is compiled to h function calls by the awesome HTM library. This can be done either at runtime or build time.

1.32 kB
import { html } from 'sinuous';

const HelloMessage = ({ name }) => html`
<!-- Prints Hello World -->
<div>Hello ${name}</div>
`
;

document.querySelector('.hello-example').append(
html`<${HelloMessage} name=World />`
);

A Stateful Component

In addition to taking input data (accessed via props), a component can maintain internal state data (accessed via observables o). When a component’s state data changes, the rendered markup will be updated by re-invoking the stored DOM operations.

1.42 kB
import { o, html } from 'sinuous';

const Timer = (props) => {
const seconds = o(0);

function tick() {
seconds(seconds() + 1);
}
setInterval(tick, 1000);

return html`
<div>Seconds: ${seconds}</div>
`
;
};

document.querySelector('.counter-example').append(
html`<${Timer}/>`
);

An Application

The o function creates an observable which can hold any value you would like to make reactive in the view. Just keep in mind that the observable returns a function. By calling this function without an argument it acts as a getter, if an argument is passed it will set the value of the observable.

The first time the view is rendered Sinuous will detect any used observables causing the accompanying DOM operations to be stored. At a later point when a new value is set to an observable it will simply execute the previously stored DOM operation with the new value.

2.58 kB
import { o, html } from 'sinuous';
import { map } from 'sinuous/map';

const TodoApp = () => {
let items = o([]);
let text = o('');

const view = html`
<div>
<h3>TODO</h3>
<${TodoList} items=${items} />
<form onsubmit=${handleSubmit}>
<label htmlFor="new-todo">
What needs to be done?
</label>
<input
id="new-todo"
onchange=${handleChange}
value=${text}
/>

<button>
Add #${() => items().length + 1}
</button>
</form>
</div>
`
;

function handleSubmit(e) {
e.preventDefault();
if (!text().length) {
return;
}
const newItem = {
text: text(),
id: Date.now()
};
items(items().concat(newItem));
text('');
}

function handleChange(e) {
text(e.target.value);
}

return view;
};

const TodoList = ({ items }) => {
return html`
<ul>
${map(items, (item) => html`<li id=${item.id}>${item.text}</li>`)}
</ul>
`
;
};

document.querySelector('.todos-example').append(TodoApp());