Skip to content
Switch branches/tags

Latest commit


Git stats


Failed to load latest commit information.
Latest commit message
Commit time

seview: S-Expression View

A simple way of writing views with s-expressions, and meant to be used with a virtual DOM library.


Because plain JavaScript is simpler to write and build than JSX, and it's great to write views in a way that is independent of the virtual DOM library being used. It's also nice to use convenient features even if the underlying virtual DOM library does not support them.


Instead of writing this in JSX:

<div id="home">
  <span className="instruction">Enter your name:</span>
  <input type="text" id="username" name="username" size="10"/>
  {isMessage && <div className={"message" + (isError ? " error" : "")}>{message}</div>}

Or even this in hyperscript:

h("div", { id: "home" }, [
  h("span", { className: "instruction" }, "Enter your name:"),
  h("input", { type: "text", id: "username", name: "username", size: 10 }),
  isMessage && h("div", { className: "message" + (isError ? " error" : "") }, message)

You can write this with seview:

  ["span.instruction", "Enter your name:"],
  isMessage && ["div.message", { className: { "error": isError } }, message]

Besides the conveniences of the syntax, you also don't have to write h at every element. To switch from one virtual DOM library to another, you only need to make changes in one place. All your view code can remain the same.

If you are using the Meiosis pattern, seview is a great way to further decouple your code from specific libraries. Your views become independent of the underlying virtual DOM library API.


seview supports CSS-style selectors in tag names, { className: boolean } for toggling classes, using an array or varags for children, flattening of nested arrays, and removal of null/empty elements.


An element is an array:

[tag, attrs, children]

or a string (text node):

"this is a text node"

The tag can be a string, or something that your virtual DOM library understands; for example, a Component in React. For the latter, seview just returns the selector as-is.


When the tag is a string, it is assumed to be a tag name, possibly with CSS-style selectors:

  • "div", "span", "h1", "input", etc.
  • "div.highlighted", "button.btn.btn-default" for classes
  • "div#home" for id
  • "input:text" for <input type="text">. There can only be one type, so additional types are ignored. "input:password:text" would result in <input type="password">.
  • "input[name=username][required]" results in <input name="username" required="true">
  • if you need spaces, just use them: "input[placeholder=Enter your name here]"
  • default tag is "div", so you can write "", ".highlighted", "#home", etc.
  • these features can all be used together, for example "input:password#duck.quack.yellow[name=pwd][required]" results in <input type="password" id="duck" class="quack yellow" name="pwd" required="true">


If the second item is an object, it is considered to be the attributes for the element.

Of course, for everything that you can do with a CSS-style selector in a tag as shown in the previous section, you can also use attributes:

["input", { type: "password", name: "password", placeholder: "Enter your password here" }]

You can also mix selectors and attributes. If you specify something in both places, the attribute overwrites the selector.

["input:password[name=password]", { placeholder: "Enter your password here" }]
<input type="password" name="password" placeholder="Enter password name here">
["input:password[name=username]", { type: "text", placeholder: "Enter your username here" }]
<input type="text" name="username" placeholder="Enter your username here">


Classes can be specified in the tag as a selector (as shown above), and/or in attributes using className:

["", { className: "btn-default special" }]
<button class="btn info btn-default special">

If you specify an object instead of a string for className, the keys are classes and the values indicate whether or not to include the class. The class is only included if the value is truthy.

// isDefault is true
// isError is false
["button.btn", { className: { "btn-default": isDefault, "error": isError } }]
<button class="btn btn-default">

Note that className is the default key, but this can be configured to be something else, such as class.

Children (array or varags)

The last item(s), (starting with the second if there are no attributes, and starting with the third if attributes are present), are the children. The children can be:

  • an array, or
  • varargs.

Using an array

You can specify children as an array:

["div", [
  ["span", ["Hello"]],
  ["b", ["World"]]

Using varargs

You can specify children as varargs:

  ["span", "Hello"],
  ["b", "World"]

Varargs and text nodes

The problem with supporting varargs is, how do you differentiate a single element from two text nodes?

For example:

["div", ["b", "hello"]]


["div", ["hello", "there"]]

For the second case, varargs must be used:

["div", "hello", "there"]

Flattened arrays

Whether using an array of children or varargs, nested arrays are automatically flattened:

["div", [
  ["div", "one"],
    ["div", "two"],
      ["div", "three"]


  ["div", "one"],
    ["div", "two"],
      ["div", "three"]

Both result in


Ignored elements

The following elements are ignored and not included in the output:

  • undefined
  • null
  • false
  • ""
  • []

This makes it simple to conditionally include an element by writing:

condition && ["div", "message"]

If condition is falsy, the div will not be included in the output. Because it is completely excluded, this will work even if the virtual DOM library that you are using does not handle false, null, or undefined.

Elements converted to a string

The following elements will be converted to a string:

  • true
  • numbers
  • NaN
  • Infinity


Using Node.js:

npm i -S seview

With a script tag:

<script src=""></script>


seview exports a single function, sv, that you use to obtain a function which you can name as you wish; in the examples, I name this function h. Calling h(view), where view is the view expressed as arrays as we have seen above, produces the final result suitable for your virtual DOM library.

When you call sv, you pass it a function that gets called for every node in the view. Each node has the following structure:

  tag: "button",
  attrs: { id: "save", className: "btn btn-default", ... }
  children: [ ... ]

The function that you write needs to convert the structure above to what is expected by the virtual DOM library that you are using. Note that your function will also be called for each element in children.

You can optionally pass a second parameter to sv to indicate something other than className as the property to use for CSS classes. For example:

const h = sv(func, { className: "class" })

This would use the class property in the attrs to indicate the CSS classes.

So you need to write a snippet of code that you pass to sv to wire up seview with the virtual DOM library that you are using. Below, you will find examples for 3 libraries. Using a different library is not difficult; you should get a pretty good idea of what to do from the examples below.

In these examples, we assume writing views with the following attributes:

  • className for the HTML class attribute
  • htmlFor for the HTML for attribute
  • innerHTML for using unescaped HTML
  • onClick, onChange, etc. for DOM events

Also, please note that the snippets below are just examples; feel free to change and adapt according to your specific needs. For your convenience, these snippets are available in seview. They are just a handful of code, though, so feel free to copy them into your project and tweak the code to your preference.


You can import this snippet into your project with:

import { h } from "seview/react";

The snippet is as follows:

import React from "react";
import { sv } from "seview";

export const h = sv(node => {
  if (typeof node === "string") {
    return node;
  const attrs = node.attrs || {};
  if (attrs.innerHTML) {
    attrs.dangerouslySetInnerHTML = { __html: attrs.innerHTML };
    delete attrs.innerHTML;
  const args = [node.tag, node.attrs || {}].concat(node.children || []);
  return React.createElement.apply(null, args);

seview + React - live example


You can import this snippet into your project with:

import { h } from "seview/preact";

The snippet is as follows:

import preact from "preact";
import { sv } from "seview";

export const h = sv(node => {
  if (typeof node === "string") {
    return node;
  const attrs = node.attrs || {};
  if (attrs.innerHTML) {
    attrs.dangerouslySetInnerHTML = { __html: attrs.innerHTML };
    delete attrs.innerHTML;
  return preact.h(node.tag, node.attrs || {}, node.children || []);

seview + Preact - live example


You can import this snippet into your project with:

import { h } from "seview/mithril";

The snippet is as follows:

import m from "mithril";
import { sv } from "seview";

const processAttrs = (attrs = {}) => {
  Object.keys(attrs).forEach(key => {
    if (key.startsWith("on")) {
      const value = attrs[key];
      delete attrs[key];
      attrs[key.toLowerCase()] = value;
  return attrs;

export const h = sv(node =>
  (typeof node === "string")
  ? { tag: "#", children: node }
  : node.attrs && node.attrs.innerHTML
    ? m(node.tag,
    : m(node.tag, processAttrs(node.attrs), node.children || [])

seview + Mithril - live example


seview is inspired by the following. Credit goes to the authors and their communities - thank you for your excellent work!

seview is developed by foxdonut (@foxdonut00) and is released under the MIT license.