All posts

Why We Built OpenMark with SwiftUI and Liquid Glass

The story behind OpenMark — from the problem of editing markdown on Mac, through 19 rounds of failed WYSIWYG attempts, to the two-view architecture that shipped.

I didn't set out to build a markdown editor. I set out to read one.

The problem was simple: I work with a lot of markdown files. READMEs, docs, notes, blog posts. And the experience of opening a .md file on Mac has always been worse than it should be. Either you get raw text in a code editor, or you deal with a heavy Electron app that takes three seconds to launch. Nothing felt right for what is, fundamentally, a reading and light editing task.

So I started building OpenMark.


The Problem with Existing Options

I've been using Macs since university, and I care about native apps. Not because of tribalism, but because native apps launch faster, use less memory, integrate with the OS better, and respect the platform's design conventions. When I open an Electron app on my Mac, it shows.

The markdown editors I tried before building my own fell into two camps:

The power tools — Obsidian, Nota, Craft. Great for knowledge management, but they want you to put your files in their vault. My files live in git repos, project folders, iCloud Drive, wherever. I don't want to import them into an app's special folder system.

The old-school editors — MacDown, Markdown Editor Pro, various App Store apps. These mostly use the classic split-pane layout: code on the left, preview on the right. It works, but the double-pane layout wastes screen space and feels like a relic.

I wanted something that did one thing: open a .md file and show it to me beautifully rendered. Toggle to edit mode when I need to. That's it.


Starting with SwiftUI

I started writing OpenMark in late 2024. The first question was: native SwiftUI or cross-platform?

The answer was obvious to me: native SwiftUI. I wanted a macOS app that felt like a macOS app. Using SwiftUI meant I'd get native typography, native animations, native window management, native accessibility, and eventually — though I didn't know this yet — Liquid Glass.

The initial prototype was a two-pane SwiftUI view: a TextEditor on one side for editing, a rendered preview on the other. This worked but had the same split-pane problem as every other editor.

I killed the split-pane pretty quickly and pivoted to the single-pane toggle model: one view at a time, full screen, with a smooth transition between them. This is the design OpenMark shipped with.


The 19 Rounds of WYSIWYG

Before landing on the toggle model, I spent an embarrassingly long time trying to build a WYSIWYG editor — one where you'd see formatted text directly as you typed, like Typora.

The approach I kept trying: render the markdown in a WKWebView, then overlay an invisible NSTextView or TextEditor on top of it for editing. The idea was that the web view would show the formatted output and the text view would handle keyboard input and text editing, with the two kept in sync.

This sounds reasonable in theory. In practice, it's a nightmare.

The core problem is that WKWebView runs in a separate process (due to WebKit's security sandboxing). You can't inject text into a WKWebView programmatically without going through the JavaScript bridge, and that bridge has latency — even a few milliseconds is noticeable when you're typing.

I tried 19 different approaches to this synchronisation problem. Threading tricks, debounced updates, scroll sync gymnastics, focus management hacks. Each one solved one problem and created two more. After round 19, I accepted that WYSIWYG inline editing with a WKWebView renderer wasn't going to work the way I wanted it to.

The toggle model was always there as the backup plan. Turned out it was the right plan all along.


Why WKWebView for Rendering?

You might ask: why use a web view for rendering at all? Why not render markdown natively in SwiftUI?

The short answer is that SwiftUI's AttributedString and Text components are not powerful enough for everything I needed. Mermaid diagrams and LaTeX math both require JavaScript to render — Mermaid.js and KaTeX are JavaScript libraries. There's no native macOS equivalent.

I evaluated the alternatives:

  • Caching rendered images of diagrams/math — works, but creates complexity and breaks for dynamic content
  • Using a native markdown renderer like cmark — great for text, but no diagram/math support
  • Shimming JS libraries into Swift — possible, but you end up re-implementing a browser at that point

WKWebView with bundled JavaScript turned out to be the right architecture. The renderer is a local HTML page loaded from the app bundle, with Mermaid.js and KaTeX bundled. No CDN, no network, no configuration. Open the app, open a file, it works.


TextKit 1, Not TextKit 2

The editing view is built on NSTextView with TextKit 1, not the newer TextKit 2. This might seem counterintuitive — TextKit 2 is Apple's recommended path — but the decision was pragmatic.

TextKit 2 had significant bugs and gaps in 2024 and early 2025, particularly around selection handling, undo, and large document performance. TextKit 1 is battle-tested and has none of these issues. The syntax highlighting system I built (custom NSTextStorage subclass for colour-only markdown highlighting) works reliably on TextKit 1.

I'll revisit TextKit 2 when it's more mature. For now, TextKit 1 is stable and fast.


Bundled JS Over CDN

Mermaid.js and KaTeX are both available as CDN links you can drop into an HTML file. The tempting path is <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js">.

I didn't do this, for two reasons:

Privacy. If OpenMark made network requests to a CDN every time it rendered a file, that CDN would see your rendering activity. The files you're reading, how often, when. That's not acceptable for a private tool.

Reliability. An app that requires internet to function isn't truly native. OpenMark works on a plane, in a remote cabin, with WiFi turned off.

Both libraries are bundled in the app. The download is slightly larger, but OpenMark works completely offline, always. This is the right trade-off.


Liquid Glass

When Apple announced macOS Tahoe and Liquid Glass at WWDC 2025, I was about six months into development. The timing was fortunate — I was still in active development, not in the polishing phase.

Adding Liquid Glass to the toolbar was straightforward in SwiftUI: swap a plain background for the appropriate material (ultraThinMaterial with a custom tint). The result looked immediately right. The toolbar floats above the document, adapts to light and dark mode, and responds to what's behind the window.

Building natively for Tahoe from the start was the right call. Electron apps can't do this. Apps that were built before Tahoe and patched for it often get the materials wrong. Being there from day one meant the Liquid Glass design felt native, not bolted on.


What I'd Do Differently

The 19 rounds of WYSIWYG attempts cost me roughly two months. If I were starting today, I'd spend a week on a quick proof of concept, hit the first synchronisation problem, and pivot to the toggle model immediately. The toggle isn't a compromise — it's a different design philosophy, and I think it's actually better.

I'd also set up the rendering pipeline earlier. I spent too long hoping I could avoid WKWebView for rendering and do it natively in SwiftUI. The answer was always going to be a web view for anything involving JavaScript rendering.


Where It Is Today

OpenMark is on the Mac App Store for $9.99. It does exactly what I wanted when I started: open a markdown file, see it rendered beautifully, toggle to edit mode when needed, toggle back.

It's a niche app — not everyone needs it. But for developers, technical writers, and students who work with markdown files on Mac, it's the tool I wanted and couldn't find.

For more on what macOS Tahoe's design language means for native apps, see macOS Tahoe Apps: The Best Native Apps with Liquid Glass Design. And if you're deciding between OpenMark and Obsidian, OpenMark vs Obsidian: When You Just Want a Markdown Editor walks through the comparison.


If you're an indie developer, I hope the build log is useful. The WKWebView sandbox, the TextKit choice, the bundled JS decision — these are the kinds of implementation details that are hard to find written down. Happy to answer questions.

Download OpenMark →