Desktop GUIs for Web Developers

Over the past few years I’ve become more interested in making desktop applications. For some context, I’m a web developer with around 15 years of experience. I mostly work with Laravel and Vue.JS but I have dabbled in a whole heap of other languages and frameworks.

I love a good desktop app and where possible I’ll generally prefer having an app rather than visiting a website. I’ve also had to turn websites into desktop apps at my job so I’ve explored a fair few different technologies for this purpose.

I’ve written this blog to share the desktop technologies that I'm currently interested in. Bear in mind, some of these tools I’ve built full apps with and some I’ve only gone through briefly with tutorials. I’ll make this clear throughout the article.

I hope to have given you an idea of what to look for when choosing a desktop application framework. Hint: There is no golden ticket, each tool has pros and cons. All I can give you is my experience with each of them and when you should consider them for your projects.

The tools I'll be reviewing are:

What to look for in a GUI tool

There are almost as many GUI tools as there are frontend Javascript frameworks. So how do you pick one for the project you’re working on?

If you use a Javascript framework for the web, a good place to start is to see if there is a desktop counterpart for that library. For example, Quasar for Vue developers, React Native for React developers, Ember Electron for Ember developers and so on.

Two of the three mentioned above are Electron based tools and I think it’s worth pointing out, if you want something built fast with access to a large community, ecosystem and is regularly updated then Electron is absolutely worth investigating. It gets a lot of gripe because release builds have a large file size, it’s not as fast as native, and generally most apps don’t feel quite right, but these downsides can often be forgiven.

As with all of the tools that I mention below, you have to weigh up various concerns.

With those key points in mind, there are a few additional things to think about

Finally, if you’re not familiar with a front end javascript library—maybe because you’re a backend developer—You might also want to look into libraries for programming languages that you’re familiar with. There are often wrappers for existing technologies like GTK, FLTK, Qt. For example, FLTK-rs for Rust, or the GTK3 gem for Ruby.

So, what’s out there?

Here comes the fun stuff. Obviously I can’t go through every single option available, but I will show you what has piqued my interest

Compose Multiplatform

Image.png

Not to be confused with Jetpack Compose, the modern toolkit for building Android apps, JetBrains’ Compose Multiplatform is based on the same technology but allows you to build for Windows/MacOS, Linux and the web.

Compose uses Kotlin and my opinion, this language feels great. So far I’ve run through the Ray Wenderlich tutorial by Roberto Orgiu and I enjoyed the experience. However, there is a lack of resources available for learning it. This tutorial and the official docs and examples are the only things I've come across.

fun main() = Window(
  title = "Sunny Desk",
  size = IntSize(800, 700),
) {
  val repository = Repository(API_KEY)

  MaterialTheme {
    WeatherScreen(repository)
  }
}

As mentioned on the website, it supports keyboard shortcuts, window manipulation, and notifications. It renders with Skia which means your apps will have native performance, however, you will need to build your own ‘Widgets’ or find an existing library if you want your app to actually look native to each platform.

Code sharing between Compose Multiplatform and the Jetpack Compose is possible, too, but I believe most of the UI elements have to be built separately. Still, this is a lot of platform support and I’m genuinely excited to see where this framework goes in the future.

Here’s some example code to get a feel of what it looks like

image.png

import androidx.compose.desktop.DesktopMaterialTheme
import androidx.compose.foundation.ContextMenuDataProvider
import androidx.compose.foundation.ContextMenuItem
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication

@OptIn(ExperimentalComposeUiApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class)
fun main() = singleWindowApplication(title = "Compose for Desktop") {
	DesktopMaterialTheme { //it is mandatory for Context Menu
		val text = remember {mutableStateOf("Hello!")}
		Column(modifier = Modifier.padding(all = 5.dp)) {
			ContextMenuDataProvider(  
				items = {
					listOf(  
						ContextMenuItem("User-defined Action") {/*do something here*/},
					)  
				}
			) {
				TextField(  
					value = text.value,
					onValueChange = { text.value = it },
					label = { Text(text = "Input") },
					modifier = Modifier.fillMaxWidth()  
				)

            Spacer(Modifier.height(16.dp))  

            SelectionContainer {  
					Text(text.value)  
				}
			} 
		}
	}
}

Positives

Negatives

egui

egui is a Rust library and builds natively with Glium (Or Glow) and WASM for the web. For native, It supports MacOS, Linux, Windows.

Out of the Rust GUI libraries available I think this one is my personal favourite. It’s self-described as easy to use and difficult to make mistakes. For someone like myself—who’s more of a visitor to the Rust language—it’s music to my ears.

It’s actively maintained, with a new release out literally an hour ago as of the creation of this sentence.

Here’s a snippet taken from one of the examples along with the newly added context menu support (Right clicking UI elements).

egui::CentralPanel::default().show(ctx, |ui| {
	// The central panel the region left after adding TopPanel's and SidePanel's

  ui.heading("eframe template");
  ui.hyperlink("https://github.com/emilk/eframe_template");
  ui.add(egui::github_link_file!(
      "https://github.com/emilk/eframe_template/blob/master/",
      "Source code."
  ));
  let response = ui.add(Label::new("Test Context Menu").sense(Sense::click()));
  response.context_menu(|ui| {
      if ui.button("Open").clicked() {
          ui.close_menu();
      }
      ui.separator();
      if ui.button("Cancel").clicked() {
          ui.close_menu();
      }
  });

  egui::warn_if_debug_build(ui);
});

Image.png

Positives

Negatives

Electron

Image.png

I’ve built two and a half apps with Electron so it’s fair to say I have experienced first hand the positives and negatives of the platform. Electron is a tool that puts web technologies on the desktop via Chrome. With Electron you’ll most likely be writing every part of the app with Javascript or Typescript, although it’s certainly possible to switch this up, for example, 1Password recently switched their desktop app to Electron with a Rust backend.

I’ve used Electron with Ember Electron and with Quasar (Vue.JS). I'll talk more about both individually below, but as a general overview, Electron is fantastic and easy to recommend so long as you can put up with its shortcomings

Positives

Negatives

Ember Electron

Image.png

Ember is one of my favourite Javascript frameworks. I’ve built many web projects with it so it was natural for me to try a desktop app with it, too. My apps, Snipline 1 and 2, are both built with Ember Electron so I have a reasonable amount of experience with it.

All of the positives and negatives from the Electron section still apply here, so I’ll comment specifically of the Ember Electron add-on.

With Ember Electron 2, it was tricky to update the Electron dependency, but with the release of Ember Electron 3 the Electron Forge dependency was updated . This means that Electron can be kept up to date separately to Ember Electron. Since Electron gets updated quite regularly it's a much welcome update.

Activity is much slower now with Ember Electron, with the latest release of 3.1.0 back in May, and the community is very small compared to the other available choices. As much as I enjoy the stack, I could not recommend it unless you want to turn an existing Ember app into a desktop app, or are already very productive with Ember.

Quasar

Image.png

Calling Quasar an Electron wrapper is selling it short. It provides many of the benefits of Ember JS, such as file directory conventions and a CLI, but it also adds support for mobile apps, SPAs and it’s own UI framework. Take a look at all of the reasons that make Quasar great on their Why Quasar? page.

I’ve built one desktop app with Quasar for an internal company project, and overall it was a pleasant experience. I much prefer Tailwind CSS to Quasar UI, and there’s nothing stopping you using both except for the additional dependency.

As with Ember Electron, you get all of the benefits of Electron with Quasar, and building the app is as simple as running a command

quasar build -m electron

One difference from Ember Electron is the build module. Ember Electron uses ‘Electron Forge’ whereas Quasar gives you two choices, Packager or Builder. Personally, I’ve used Builder and had no issues besides the teething problems of getting auto updating working on Windows.

Regarding activity, Quasar is very active, with an update to the main repo just for days ago as of writing and plenty recently before that. There are many contributors, and the documentation is great. I think that if you’re familiar with Vue.JS and Vuex then you’re in safe hands using Quasar.

Flutter

Image.png

One of the most impressive things about Flutter is the breadth of devices it supports. From mobile, desktop, to embedded devices. Similar to Compose, it uses Skia to render the UI, so while you’re getting native performance you’re most likely not going to get a native look, at least not out of the box.

Unlike Compose, I was pleasantly surprised when I followed an Android tutorial to build a Windows app and it just worked. Of course, it looked like an Android app, with the default Material theme, but there's nothing to stop you tweaking the theme per device. Take a look at this blog post by Minas Giannekas on how he built Shortcut Keeper and how he themed it for each platform. Truly impressive.

Image

There’s also a large community and ecosystem surrounding Flutter, so you’re unlikely to run out of learning resources.

But Flutter isn’t without it’s shortcomings. There’s a long list of issues in their Github repo, which also speaks for the popularity of the library. Much of the ecosystem is focused on mobile, which means if you wish to make an app work across mobile, desktop, and web, you may have to provide your own functionality for the latter two environments.

There are also complaints that the development of Flutter outpaces the plugins surrounding it. You may need to stay on an older version of Flutter because of plugin compatibility issues.

Positives

Negatives

Reactive Native for Windows

Image.png

Since I’ve included a Vue.JS and an Ember JS library, I thought it would only be fair to include a library for React developers, too. React Native is a popular solution for building native apps for iOS and Android and uses Objective-C and Java under the hood for each platform respectively.

For Windows, it renders with Universal Windows Platform (Or UWP for short) which means you really are getting native controls rendered. I couldn’t find any information of how the React Native for MacOS renders, although I’d imagine it’s doing something similar to iOS.

Here’s a quick snippet that I tried starting from the base RNW project.

Screenshot 2022-01-01 165853.png

import React, { useState } from 'react';
import type {Node} from 'react';
import {
	SafeAreaView,
	ScrollView,
	StatusBar,
	StyleSheet,
	Text,
	useColorScheme,
	View,
	Alert,
	Modal,
	Pressable
} from 'react-native';
import {
	Colors,
	DebugInstructions,
	Header,
	LearnMoreLinks,
	ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';

const Section = ({children, title}): Node => {
	const isDarkMode = useColorScheme() === 'dark';
	return (
		{title} {children} 
	);
};
const App: () => Node = () => {
	const isDarkMode = useColorScheme() === 'dark';
	const [timesPressed, setTimesPressed] = useState(0);
	const backgroundStyle = {
		[backgroundcolor: isDarkMode ? Colors.darker : Colors.lighter,
	};
	const buttonStyle = {
		[padding: '20px',](padding: '20px',)
	}
	return (
		<SafeAreaView style={backgroundStyle}>
			<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
			<ScrollView
			contentInsetAdjustmentBehavior="automatic"
			style={backgroundStyle}>
				<Section title="React Native for Windows"></Section>
				<Pressable
					onPress={() => {
						setTimesPressed((current) => current + 1);
					}}
				style="{({pressed}) => [
				{
				backgroundColor: pressed ? 'rgb(210, 230, 255)'
				: 'black',
				padding: 10,
				textAlign: 'center'
				},
				styles.wrapperCustom
				]}>
				{({ pressed }) => (
					<Text style={() => [ { ...styles.text, textAlign: 'center' }]}>
						{pressed ? 'Pressed!' : `Count: ${timesPressed}`}
					</Text>
				)}
				</Pressable>
			</ScrollView>
		</SafeAreaView>
	);
};

const styles = StyleSheet.create({
	sectioncontainer: {
		margintop: 32,
		paddinghorizontal: 24
	},
	sectiontitle:
		fontsize: 24,
		fontweight: '600',
	},
	sectiondescription: {
		margintop: 8,
		fontsize: 18,
		fontweight: '400',
	},
	highlight: {
		fontweight: '700',
	},
});

export default App;

In terms of community, you have the foundation of the mobile RN community to work with, but as with other ecosystems in this article, you probably aren’t going to find much plugin support for desktop at the moment.

Positives

Negatives

SwiftUI

Image.png

Having released 2 apps and another on the way, SwiftUI is another tool I have plenty of experience with.

SwiftUI has been designed by Apple to work well on each of their platforms. There are many 'Widgets' that can be shared across each platform so you can write code once and have it run on most devices. For example, context menu's on an iOS device are triggered from a long press, where as on a Mac it's triggered from a right click.

// Taken from the useful app, SwiftUI Companion   
struct ExampleView: View {
   var body: some View {
     Text("Press, hold and release")
       .foregroundColor(.white)
       .padding(15)
       .background(RoundedRectangle(cornerRadius: 8).fill(Color.blue))
       .contextMenu {
         Button("Open") {  print("open...") }
         Button("Delete")  {  print("delete...") }
         Button("More info...") {  print("more...") }
     }
   }
}

A personal favourite feature of mine, which I’ve yet to see in other GUI frameworks, is data binding between multiple windows. Using the @AppStorage property wrapper, you can update a value in one window and have it's value easily sync in another. This is really useful for preferences which are generally in their own window in MacOS apps.

Here’s a truncated example of the power and simplicity of SwiftUI for Mac apps.

import SwiftUI

@main
struct RsyncinatorApp: App {
  @AppStorage('showVisualHints') private var showVisualHints = true
  
  var body: some Scene {
    WindowGroup {
      ContentView()
    }
    
    #if os(macOS)
    Settings {
      SettingsView()
    }
    #endif
  }
}

struct SettingsView: View {
  private enum Tabs: Hashable {
    case general, advanced
  }
  var body: some View {
    TabView {
      GeneralSettingsView()
        .tabItem {
          Label("General", systemImage: "gear")
        }
      .tag(Tabs.general)
    }
    .padding(20)
      .frame(width: 375, height: 150)
  }
}

struct GeneralSettingsView: View {
  @AppStorage("showVisualHints") private var showVisualHints = true

  var body: some View {
    Form {
      Toggle("Show visual hints", isOn: $showVisualHints)
    }
    .padding(20)
      .frame(width: 350, height: 100)
  }
}

Here’s the Preferences window that’s generated. If you’re familiar with Mac apps, you should recognise the general layout with the tabbed sections at the top. All this is laid out for you.

Image.png

One major show stopper for many people is that it doesn’t build for Windows and Linux. I also feel it’s only just becoming a real solution as of it's 3rd major release which adds much needed functionality. Functionality such as search and focus states weren't properly supported before so you'd have to write it yourself. There are also bugs that crop up and it's down to Apple's discretion as to when these get fixed.

The community and packages surrounding SwiftUI tend to focus on mobile, however, there are still a reasonable amount of resources for MacOS. If you're interested, take a look at this official tutorial for MacOS for getting started.

Positives

Negatives

Tauri

Image.png

Tauri is another fairly new library. It’s a web wrapper and you can use whichever web framework you prefer. There’s an officially supported plugin for Vue.JS, but it is simple enough to add your own. I've had it working with both Ember JS and Svelte.

It’s first major difference from Electron is that it uses your Operating System’s web browser rather than bundling Chrome. This results in fairly tiny file sizes, but at the cost of having to debug issues on different platforms.

The second major difference is that Tauri uses Rust. With Electron you pass messages from main and renderer with Node and Javascript, whereas with Tauri you pass events from the frontend and backend with Javascript and Rust, respectively.

Here’s a snippet from the Tauri documentation of communicating between the two.

import { getCurrent, WebviewWindow } from '@tauri-apps/api/window'

// emit an event that are only visible to the current window
const current = getCurrent()
current.emit('event', { message: 'Tauri is awesome!' })

// create a new webview window and emit an event only to that window
const webview = new WebviewWindow('window')
webview.emit('event')
// the payload type must implement `Serialize`.
// for global events, it also must implement `Clone`.
#[derive(Clone, serde::Serialize)]
struct Payload {
  message: String,
}

fn main() {
  tauri::Builder::default()
    .setup(|app| {
      // listen to the `event-name` (emitted on any window)
      let id = app.listen_global("event-name", |event| {
        println!("got event-name with payload {:?}", event.payload());
      });
      // unlisten to the event using the `id` returned on the `listen_global` function
      // an `once_global` API is also exposed on the `App` struct
      app.unlisten(id);

      // emit the `event-name` event to all webview windows on the frontend
      app.emit_all("event-name", Payload { message: "Tauri is awesome!".into() }).unwrap();
      Ok(())
    })
    .run(tauri::generate_context!())
    .expect("failed to run app");
}

I’ve built and released one app with Tauri and it was fairly painless for a simple app. I used Svelte for the web framework and each installer came out at less than 5MB.

Image

For larger apps, I would most likely struggle to implement certain functionality. The getting started guides are easy enough to follow, but once I started trying to add more functionality I found the overall documentation lacking. There’s also fewer features than Electron which is to be expected since the platform is not as mature and the community not as large.

It supports adding CLI’s to your app which I think is a very cool feature that’s not often built into GUI libraries. You can also embed external binaries which can be very useful if you need to use a command-line tool for functionality in your app. It also supports auto updating for each platform (With Linux supporting AppImage).

Positives

Negatives

GUI Library Overview

I thought it would be beneficial to have a casual overview of the differences between platforms, including differences in community size and support.

Releases in the past 6 months gives some indication of activity on each project, and includes beta, dev, and RC releases. This information is taken from each project’s git repository and is checked between 1st July 2021 and 1st January 2022.

As SwiftUI is not open source and other than at WWDC where major changes are announced, we do not get a run-down of changes between Xcode versions, it’s difficult to compare. We do know however that SwiftUI is backed by Apple and appears to be the recommended way of making apps for the Apple ecosystem moving forward.

SwiftUI is also the only platform out of the list that does not support Windows/Linux. It does however have support for iOS, iPadOS, Apple Watch, and Apple TV. If you’re in the Apple ecosystem, it’s definitely something to consider.

Framework/LibraryLanguage(s)NativePlatform SupportContributorsReleases in past 6 monthsInitial release dateStable release?
ComposeKotlin💻🪟🐧🤖64512nd April 2021
eguiRust💻🪟🐧89430th May 2020
ElectronJavascript💻🪟🐧108111312 Aug 2013
React Native for WindowsJavascript/Typescript💻🪟🤖📱1804923 Jun 2020
FlutterDart💻🪟🐧🤖📱9572827th Feb 2018
TauriRust + Javascript💻🪟🐧114418th December 2019

Features

Not all frameworks have every feature. If you’re looking to make an application that relies on specific things such as webcam support then you’ll need to check if it works or you’ll have to code it yourself.

Note that, my Google-foo may fail. I have tried looking through documentation and various resources for each library but unfortunately it’s not always easy to find if a solution exists.

Additionally, these features may get added after this article is published, so do your own research, too!

Here’s a key for the tables below.

For theming and light/dark mode I’ll be looking at native support for the Operating System’s features. Web wrappers also generally have features that you can use from the browser, e.g. webcam support via JS, which I mention in the table.

Automatic updates for Linux are only available for Electron and Tauri via AppImage. Unfortunately most libraries don’t support over the air updates or only partially support it, and in this case you’ll have to either implement it yourself, or simply prompt the user to install the next update manually by checking a web-hook that you set up and manage.

Framework/LibraryContext MenusWindow MenusMultiple Windows/Window ManipulationWebcam/MicrophoneAutomatic updatesTheming, Light and Dark modeTray
Compose❌ (issue)🎓(link)
egui✅ (basic)❓(issue)🎓(link)
Electron📦 (plugin)✅ (Via JS)💻🪟🐧✅ (link)
Flutter📦 (1, 2)📦 (plugin)🎓(link)
React Native for WindowsMicrosoft Store
SwiftUI✅ (Using AppKit)Mac App Store, Sparkle
Tauri❌ (JS library work around)(Via JS)💻🪟🐧✅ (Via CSS)

Accessibility

There’s many different levels of accessibility so I thought it would be worth investigating.

When looking at font size, I’m referring to ability to use the Operating System’s font scaling. Most tools are able to implement their own font scaling if they wanted to — or with a bit of additional code.

Interestingly, I tried testing this with Compose on Windows and the font refused to scale up. egui and Flutter worked fine, and browser based libraries will use the web browsers native font scaling.

Framework/LibraryVoice overKeyboard shortcutsTooltipsOS font size scalingTab focusing/cycling
Compose✅ - Mac Only, Windows planned
egui❌ (issue)❌ (issue)
Electron🎓 (link)✅ (Chromium handles this)
Flutter❓(link)✅(link 1, 2)🎓 (link)
React Native for Windows❌ (issue)
SwiftUI✅ (MacOS Montery+)
Tauri✅ (Via JS)✅(Via JS)

Final Recommendations

When choosing a library for building a desktop app, I think you have to ask yourself which category your application falls into:

  1. Personal project to solve your own issue
  2. Small scope software with few updates or released as feature complete
  3. Projects targeting developers
  4. Product to be distributed and available to as many people as possible with frequent updates (E.g. a SaaS)
  5. Enterprise - stability and maintainability at utmost importance

For personal and feature complete software, I’d suggest going for the one that appeals the most to you, assuming it has the features you need.

For most other projects, you're most likely going to want to have automatic updates available. That is, unless you want to respond to every support request with 'Can you update to the latest version please’.

It's a real shame that it removes many of the otherwise great libraries from the running. If you can get away with it, you could instead implement a prompt that tells users to download a newer version manually when it’s available. Still, OTA updates are almost a requirement for desktop software today.

There is also a niche for software that only targets Apple devices. Plenty of developers go this route, just take a look at Sketch, Panic, Craft docs, as a few examples. It certainly simplifies development, and if you're already in the Apple ecosystem it's great to scratch your own itch. If this sounds like your situation then SwiftUI is a great choice.

I really like all of these libraries, but Electron is the solution that's least likely to bite you with it's large community, ecosystem and feature set. That said, I'm eager to see the other solution grow in the future.

If you have any thoughts or suggestions for tools I should check out. Please feel free to comment! You can reach me on Mastadon, Twitter, Dev.to, Micro.blog, or comment directly on the original article.

Spread the word

Share this article

Like this content?

Check out some of the apps that I've built!

Snipline

Command-line snippet manager for power users

View

Pinshard

Third party Pinboard.in app for iOS.

View

Rsyncinator

GUI for the rsync command

View

Comments