Product design for apps and websites

Made in Sydney by independent designer/developer Chris Sealey, in collaboration with discerning people all over the world. User interface design, icons and front-end code for iOS, macOS and the Web, meticulously crafted and shipped for the platforms that embrace good design.

Twine's app iconTwine Beta 51bits picturemark51bits 3 WillyWeather précis iconsWillyWeather 2 Stat iOS app iconStat

Twine Beta: a mouldable approach to productivity

#web #shipped

This morning, we sent out the first round of beta invites for Twine, a product I've been working on with the folks at Anomaly. Twine is a new approach to productivity; an organisation tool without a prescribed workflow. Rather than projects or lists, you share tasks with friends, family and colleagues and then organise them however you like.

By assigning metadata such as tags, locations and dates to each task, dynamic views—specific to you—can be created in Twine. Think of Smart Playlists in iTunes, where parameters are defined to build a view of songs, but can be adjusted or destroyed at any time without affecting the contents.

Twine steps into a very crowded market, but acknowledges that everyone approaches productivity differently. While GTD or alternative systems may work for some, ultimately we all think and organise our lives individually. Being able to mould a workflow rather than the other way around is an idea we feel is worth exploring. For small teams, large companies or individuals, if you've been looking for more fluidity in the way you manage things, we'd love for you to give Twine a go.

We anticipate our beta period will run for 1–2 months before we open it up. It is early days and we have a lot of work to do, but if you'd like to be considered for early access, register your interest.

Return of the contact form

#ux #web #shipped

Two years ago, I removed the contact form on this site and published my rationale shortly afterwards. Yesterday, I reversed that decision and restored the form, tackling it from a different angle. Here is how and why.

There were several reasons for removing the form in the first place. The lack of server code and reduced UI appealed to me and I figured most people preferred to use their own email client anyway.

The advantages of plain email:

Turns out, it might not be that black and white. I noticed a clear drop in leads through the site after removing the form. I also struggled to find an ultimate 'call to action' for the site, because having the most prominent button on a page open an email client is a little odd.

My conclusion is now that, while some people enjoy the freedom and familiarity of email, others like a bit of hand-holding. Filling out a form is more subconscious and requires less thinking than an empty white screen. So why not offer both options? The main roadblock was that I really didn't want to write any server code.

I found a solution in one of my favourite products, Campaign Monitor, which offered all of the features I needed to make this work without a server:

All I had to do was write the HTML, CSS and XHR function to communicate with Campaign Monitor's service. This matched up with most of the advantages email had over my previous form, but for those it didn't, I've actually found the restrictions—250-character cap on inputs and no file attachments—to be beneficial; they force brevity, which invokes conversation.

In the end, email is still available and having several methods of contact on offer can't hurt.

Handling images in JavaScript

#javascript #html #ux #web

When I started redesigning this site, I wanted a simple way to process images, covering a few basic needs. While there are various methods being formed for handling responsive images and display density, there is currently no prescribed approach. This site has no server code, so JavaScript was the obvious choice.

My requirements were:

I wrote a function which passes one or more imgs through a switch and creates a new Image object for each, appending the desired attributes, before replacing the original with a new img.

I'll go through each stage of imageHandler() in detail before linking to the full source but, first, let's take a look at the markup. This is what an image looks like:

<img src="image.png">

To have that image lazy-load, do this instead:

<img src="{{ site.lazy }}" data-src="image.png">

site.lazy is a Liquid variable I have defined in Jekyll's config.yml, which outputs the smallest possible Base64-encoded .gif, preventing the image from returning a 404 before JavaScript runs:


If you are not using a site generator or CMS, you may have to manually enter this string each time.

While the image loads in the background, it is nice to display a fallback colour:

<img data-fallback="#f33">

Lastly, if @2x or @3x versions of the image are available, we can flag that too. I've called them retina and plus:

<img data-retina data-plus>

Any combination of these attributes and properties can be used. The end result might look something like this:

<img src="{{ site.lazy }}" data-src="image.png" data-retina data-plus data-fallback="#f33">

The imageHandler() function contains a switch with two cases; 'fallback'—called on the DOMContentLoaded—and 'parse', which is called on window.onload.

function imageHandler(action, image, callback) {
    switch (action) {
        case 'fallback':
        case 'parse':

'fallback' is simple; it conditions the existence of data-fallback on an img and, if present, sets its value as the background-color:

var fallback = image.hasAttribute('data-fallback');

if (fallback) { = fallback;

There is a little more to case 'parse'. First, we prepare a new attributes object and a new image object:

var attrs = new Object();
var newImage = new Image();

Then, we take the original images' attributes and place them into the attrs object we just defined:

for (var i = 0, nodes = image.attributes; i < nodes.length; i++) {
    attrs[nodes[i].nodeName] = nodes[i].nodeValue;

Now we have everything we need for JavaScript to do its work. First, lazy-loading:

if ('data-src' in attrs) {
    attrs.src = attrs['data-src'];

This will replace the src value with the original data-src attribute we defined in markup.

Next, condition the display's pixel density and define a src for the new image:

var path = attrs.src.substring(0, attrs.src.lastIndexOf('.'));
var extension = attrs.src.split('.').pop();

if (window.devicePixelRatio > 2 && 'data-plus' in attrs) {
    newImage.src = path + '@3x.' + extension;
} else if (window.devicePixelRatio > 1 && 'data-retina' in attrs) {
    newImage.src = path + '@2x.' + extension;
} else {
    newImage.src = attrs.src;

Note: this assumes your images are named with the following convention:

As you can see, if window.devicePixelRatio is not greater than 1, we simply use the src value which may or may not have been lazy-loaded.

After that, it is important to remove the attributes that are no longer needed so that anything else (such as the alt attribute) can be preserved and restored:

var garbage = ['src', 'data-src', 'data-retina', 'data-plus', 'data-fallback', 'style'];

for (var i = 0; i < garbage.length; i++) {
    delete attrs[garbage[i]];

for (var attr in attrs) {
    newImage.setAttribute(attr, attrs[attr]);

The new Image is now ready. Once it has loaded, we can swap it in for the original:

newImage.onload = function() {
    image.parentNode.insertBefore(newImage, image);

    if (callback) {

The reason for going through the process of building a new image rather than simply manipulating the attributes of the original is that, with this approach, it is possible to fire a callback once the image loads. For instance, you may want to display some form of activity-indication while the image is loading and remove it onload.

Lastly, we need to call the function on the relevant events:

document.addEventListener('DOMContentLoaded', function() {

    forEach(document.querySelectorAll('img[data-fallback]'), function(index, image) {
        imageHandler('fallback', image);

}, false);

window.addEventListener('load', function() {

    forEach(document.querySelectorAll('img'), function(index, image) {
        imageHandler('parse', image);

}, false);

Note: in this example I am using a custom forEach method to avoid NodeList hacks. More on that here. Feel free to loop over imgs in whichever way you choose.

The full source of the imageHandler() function is available as a Gist here.

This is how every image on is handled. Some images lazy-load, others do not. Some have @3x support, others maybe only @2x. It is not perfect but as there is yet to be a silver bullet for handling images on the web, this fits my needs so I feel it is worth sharing. I'm no JavaScript master so there could be better ways to do this, but it works for me.

Known issues and room for improvement: