Your first editor

This guide will teach you how to build a simple Substance editor with basic editing functionalities. We call it SimpleWriter.

The model

Let's start with defining a model that our editor can operate on. We want to support the following node types:

  • Body - holds references to the actual content elements
  • Paragraph a text paragraph that can be annotated
  • Heading Headings of 3 different levels
  • Strong Annotation that marks text as strong
  • Emphasis Annotation that marks text as emphasized
  • Link Link annotation
  • Comment Comments in the text

Substance has a number of preimplemented node types, so we only need define the Body and Comment nodes.

The body node inherits from Container.

class Body extends Container {}

  type: 'body'

The Comment node is a PropertyAnnotation and defines a content property to hold the comment text.

class Comment extends PropertyAnnotation {}

  type: 'comment',
  content: { type: 'string', default: '' }

Node Converters

We choose HTML as a serialization format for our article. Hence, we need to define a converter for our body node, which converts an HTML body element to a Substance body node referencing a sequence of content nodes (paragraphs, headings).

export default {
  type: 'body',
  tagName: 'body',
  import: function(el, node, converter) { = 'body'
    node.nodes = {
      var childNode = converter.convertElement(child)
  export: function(node, el, converter) {

And here is the converter for the comment node.

export default {

  type: 'comment',
  tagName: 'span',

  matchElement: function(el) {

  import: function(el, node) {
    node.content = el.attr('data-comment')

  export: function(node, el) {
      'data-type': 'comment',
      'data-comment': node.content

HTML Importer

class SimpleHTMLImporter extends HTMLImporter {
  convertDocument(htmlEl) {
    var bodyEl = htmlEl.find('body')

Node Components

For each node, we need to implement a Component in order to display it. The body element for us will be where editing starts. Containers can be made editable by just wrapping them in ContainerEditor, which we do here.

class BodyComponent extends Component {
  render($$) {
    let node = this.props.node;
    let el = $$('div')

      $$(ContainerEditor, {
        disabled: this.props.disabled,
        node: node,
        commands: this.props.commands,
        textTypes: this.props.textTypes
    return el;


In order to create comments in the user interface, we need to define a Command for it.

class CommentCommand extends AnnotationCommand {
  canFuse()   { return false }
  canDelete() { return false }

Tool Components

In order to edit comments we provide a tool component for it.

class EditCommentTool extends Tool {

  render($$) {
    let Input = this.getComponent('input')
    let Button = this.getComponent('button')
    let el = $$('div').addClass('sc-edit-comment-tool')

      $$(Input, {
        type: 'text',
        path: [, 'content'],
        placeholder: 'Please enter comment here'
      $$(Button, {
        icon: 'delete',
      }).on('click', this.onDelete)
    return el

Nodes as packages

Substance uses packages where possible, to be able to extend editors with new functionalities. Each plugin/extension comes with it's own package definition.

Here's the BodyPackage.

export default {
  name: 'body',
  configure: function(config) {
    config.addComponent(Body.type, BodyComponent)
    config.addConverter('html', BodyConverter)

And the CommentPackage.

export default {
  name: 'link',
  configure: function(config, options) {
    config.addConverter('html', CommentConverter)

    // Tool to insert a new comment
    config.addCommand('comment', CommentCommand, {nodeType: 'comment'})
    config.addTool('comment', AnnotationTool, {target: options.toolTarget || 'annotations'})
    // Tool to edit an existing comment, should be displayed as an overlay
    config.addCommand('edit-comment', EditAnnotationCommand, {nodeType: 'comment'})
    config.addTool('edit-comment', EditCommentTool, { target: 'overlay' })

    // Icons and labels
    config.addIcon('comment', { 'fontawesome': 'fa-comment'})
    config.addLabel('comment', 'Comment')

Now we provide a configuration on editor level.

import {
  BasePackage, StrongPackage, EmphasisPackage, LinkPackage, Document,
  ParagraphPackage, HeadingPackage
} from 'substance'

export default {
  name: 'simple-writer',
  configure: function (config) {
      name: 'simple-article',
      ArticleClass: Document,
      defaultTextType: 'paragraph'

    // BasePackage provides core functionaliy, such as undo/redo
    // and the SwitchTextTypeTool. However, you could import those
    // functionalities individually if you need more control

    // core nodes
    config.import(StrongPackage, {toolTarget: 'annotations'})
    config.import(EmphasisPackage, {toolTarget: 'annotations'})
    config.import(LinkPackage, {toolTarget: 'annotations'})

    // custom nodes
    config.import(CommentPackage, {toolTarget: 'annotations'})

    // Override Importer/Exporter
    config.addImporter('html', SimpleHTMLImporter)

Define a SimpleWriter component.

The SimpleWriter component forms our editor's heart. Some basic Substance infrastructure is set up by AbstractEditor, which we inherit from. We need to implement AbstractEditor#render and AbstractEditor#documentSessionUpdated. Substance uses a Component API similar to React, which be relatively easy to understand.

The following code shows the setup of an editor, rendering a toolbar on top and document's body. We delegate setting up the editor to the Body component, which is defined in a package and sets up the actual editor.

class SimpleWriter extends AbstractEditor {

  render($$) {
    let el = $$('div').addClass('sc-simple-writer')
    let configurator = this.props.configurator
    let SplitPane = this.componentRegistry.get('split-pane')
    let ScrollPane = this.componentRegistry.get('scroll-pane')
    let commandStates = this.commandManager.getCommandStates()
    let Body = this.componentRegistry.get('body')

    let contentPanel = $$(ScrollPane, {
      scrollbarPosition: 'right',
      overlay: SimpleWriterOverlayTools,
      $$(Body, {
        disabled: this.props.disabled,
        node: this.doc.get('body'),
        commands: configurator.getSurfaceCommandNames(),
        textTypes: configurator.getTextTypes()

      $$(SplitPane, {splitType: 'horizontal'}).append(
        $$(Toolbar, {
          commandStates: commandStates
    return el

  documentSessionUpdated() {
    let toolbar = this.refs.toolbar
    if (toolbar) {
      let commandStates = this.commandManager.getCommandStates()
        commandStates: commandStates

Use your new editor

let cfg = new Configurator()

window.onload = function() {
  // Import article from HTML markup
  let importer = cfg.createImporter('html')
  let doc = importer.importDocument(fixture)
  // This is the data structure manipulated by the editor
  let documentSession = new DocumentSession(doc)
  // Mount SimpleWriter to the DOM and run it.
    documentSession: documentSession,
    configurator: cfg
  }, document.body)

Find the complete code for SimpleWriter on Github.


  • Enable a Substance core node type (e.g. Superscript, Image) for SimpleWriter (very easy)
  • Create a simple Highlight node type to emphasize parts of the text with a yellow background. Serialize as <span data-type="highlight">...</span> Tip: Look at existing implementations such as Strong. (easy)
  • Create a new text type FancyParagraph, that works like a regular paragraph, just with different styles. Serialize as <p data-type="fancy">...</p> (easy)
  • Create a simple Person node type with properties firstname, lastname, which are editable via regular input elements. Look at Input Example as a reference implementation. Create a tool that allows insertion of Person nodes into the document (as a block element). Serialize as <div data-type="person" data-firstname="John" data-lastname="Doe"></div>. (medium)
  • Create new Monster node type that can be inserted inside the text. See InlineNode example as a reference implementation. Render a monster as small image that appears in the text. Bonus points: Allow different monster types and provide UI to change the moster type. Render a different image for each monster type. (medium)