Using Your Browser and HTML5 "content editable" as an Editor

HTML5 has the concept of content editable which allows any DOM element to become a user-editable canvas. All you need to do is add the contenteditable="true" attribute to an element:

The beauty of this feature is that the content is part of the HTML DOM and can be edited with the chrome inspector, so you have all the power of the inspector to drag and drop nodes, change the element styles from the style panel, etc...

This gives you a fantastic editor to work in with a very minimal amount of code required.


Persisting Locally

Obviously, an editor that can't save isn't very useful, but we can make use of another HTML5 technology, localStorage, to keep the content saved. The basic form of this looks like:

<div id="content" contenteditable="true">

    var key = "draft",
        contentEl = document.getElementById("content");

    contentEl.innerHTML = localStorage[key] || '';
    contentEl.onkeyup=function() {
        localStorage[key] = contentEl.innerHTML;

This saves the content to localStorage whenever you type a key, so it persists with your browser.

See the following jsFiddle for a working example:

If all you want is a scratch pad that saves locally, you can stop here. If you want to persist more permanently, keep reading.

Saving Remotely

The next step is to save the contents external to the editor, which is possible by posting the contents to an API endpoint when Ctrl-S is pressed. But first, we need an API to save documents to. This very basic API (in PHP) saves the posted document to a local redis store:


 // composer provided autoloader
header("Content-Type: application/json");

$prefix = "doc:";

$redis = new Predis\Client();

$key = $_POST["key"];
$content = $_POST["content"];

$redis->set($prefix.$key, $content);

    "key" => $key



// composer provided autoloader
header("Content-Type: application/json");

$prefix = "doc:";

$redis = new Predis\Client();

$key = $_GET["key"];
$content = $redis->get($prefix.$key);

if ($content != null) {
        "key" => $key,
        "content" => $content
} else {
    print(json_encode(array("error" => "404")));

Once the API is in place, you need to catch Ctrl-S and post the content from the content editable div to save.php (for brevity, I'm going to use Zepto/jQuery syntax for ajax calls):

function save(key, content) {
    $.post('save.php', {content: content, key: key}, function(response) {
        // keep a copy of the last saved version is localStorage
        // so we can check for changes.
        localStorage[key + ":saved"] = content;
    }, 'json');

$("body").keydown(function(event) {
    // Only Ctrl-S and Command-S (19)
    if (!( String.fromCharCode(event.which).toLowerCase() == 's' && event.ctrlKey) && !(event.which == 19)) 
        return true;

    save(key, contentEl.innerHTML);

    return false;

This means you have a draft version in localStorage until you explicitly save it. Then it posts to the remote endpoint. In the examples here, the key we post to is hard coded to 'draft'.

The only step left is to add a function for loading saved content from the remote API and getting the key from the URL:

function load(key, fn) {
    $.getJSON('get.php?key='+key, function(data) {
        var content = data.content;
        if (content) fn(content);

// get key from url hash and load content
// otherwise, use default key of "content"

var key,
    hash = window.location.hash.replace("#", "");

if (hash) {
    key = hash; 
    load(hash, function(content) {
        localStorage[hash] = content;
        contentEl.innerHTML = content;
} else {
    key = "draft";

Here we've replaced our previous static key of "draft" with the content of the url hash, defaulting it to "draft" if it isn't set so we can have different documents at different URLs. Once pulled together, you get a nice, easy to setup editor in the web.

I've also packaged it together as a site you can setup locally along with some additional features such as document status and conflicts notices. It's available on Github as editor5.

It's pretty basic at this point, but I'll be adding features to it over time.