Tastemaker

Tastemaker

Desktop App

UI/UX

Product Design

2026

Tastemaker.site

An Offline Visual Reference Tool

Tastemaker is a cross-platform desktop application for collecting and managing visual references for creative projects. Rather than relying on a search engine algorithm, it lets creatives build their own library of references locally.

It supports images and GIFs with fast search, filtering, and organization, along with a full moodboard creation and file sharing system. It launched in December 2025, followed by two major updates in January and February 2026.

It was designed and developed from the ground up. The user interface, visual design, app architecture, and all core functionality were created by me alone. It has been downloaded by 40,000 creatives worldwide, across design, architecture, film, and more.

This case study references many different aspects of building the product, so feel free to skip around depending on what you are looking for:

Product Design

Overview

Tastemaker opens on the library: everything saved, in one grid, with no feed and no algorithm deciding what surfaces first. Search and filters narrow thousands of references in a keystroke. Because the library lives on the machine rather than on a server, it works exactly the same on a plane as it does at a desk.

The Sidebar

The sidebar is the spine of the app. Collections, tags, and filters all live in one column, so moving between projects never means leaving the view you are working in. It collapses out of the way the moment the work needs the full screen.

Split Screen

Split screen puts the library and a moodboard side by side. References drag from one into the other without switching context, which is the difference between collecting inspiration and actually composing with it.

Moodboard in a Separate Window

A moodboard can be pulled out into a window of its own. On a second monitor the board stays open while the library keeps working beside it, which matches how most creatives already arrange their desk.

Save and Import Moodboard Files

Moodboards save as a single portable file. Sending one to a collaborator or a client means sending one file, and opening it restores the board exactly as it was built, images included. No account, no upload, no link that expires.

Appearance Switching

The entire interface switches appearance on demand. Reference work happens in wildly different lighting, and a tool that feels right at midnight is rarely the one you want at noon.

Lightbox Mode

Lightbox mode drops everything except the image. The interface recedes completely so a single reference can be studied at full size, then dismissed just as quickly.

Quick Capture

Quick Capture lets you save an image the moment you find it: from your browser, a folder, a chat or a screenshot. It is a small capsule toolbar that stays hidden until you need it. Start dragging an image from anywhere on the internet toward the top of your screen, and the capsule slides into view to catch it. Drop the image on it and it is saved straight to Tastemaker, without opening the app or switching windows. You stay where you are, and the image lands in your library.

Drag and drop any image onto a small capsule at the top of your screen.

Paste an image you've copied with a shortcut.

File it right away into a collection, or sort it later from your inbox.

The Launch

I designed the user interface, visual language, motion system, and app architecture from scratch. Before launch, I built everything needed to ship Tastemaker as a complete product: a landing page, a waitlist, Stripe checkout integration, a full documentation wiki, and a launch video.

Updates are released periodically, taking direct feedback from users into account.

Tastemaker screenshot 1Tastemaker screenshot 2Tastemaker screenshot 3Tastemaker screenshot 4Tastemaker screenshot 5Tastemaker screenshot 6Tastemaker screenshot 7Tastemaker screenshot 8Tastemaker screenshot 9Tastemaker screenshot 10Tastemaker screenshot 11Tastemaker screenshot 12

How It Started

As creatives, we rely on visuals to spark ideas. But somewhere along the way, collecting inspiration became distracting. Infinite scroll. AI-generated art. Lost ownership. I wanted something different, so I built Tastemaker, a tool that puts the creator back in control.

I originally designed the app as an internal tool for our studio, Motion Directive. It helped us collect and organize inspiration for our projects and build moodboards to present to clients. The tool felt too useful to keep to ourselves, and when it went public the response was overwhelmingly positive. The launch video hit 30,000 views in its first month, and word spread quickly.

What started as a simple way to collect and organize visual references turned into a full creator tool, with support for extended file types, a proprietary moodboard file sharing system, and a growing user base.

Tastemaker reference 1Tastemaker reference 2Tastemaker reference 3Tastemaker reference 4Tastemaker reference 5Tastemaker reference 6Tastemaker reference 7Tastemaker reference 8Tastemaker reference 9Tastemaker reference 10Tastemaker reference 11Tastemaker reference 12Tastemaker reference 13Tastemaker reference 14Tastemaker reference 15
Tastemaker reference 16Tastemaker reference 17Tastemaker reference 18Tastemaker reference 19Tastemaker reference 20Tastemaker reference 21Tastemaker reference 22Tastemaker reference 23Tastemaker reference 24Tastemaker reference 25Tastemaker reference 26Tastemaker reference 27Tastemaker reference 28Tastemaker reference 29Tastemaker reference 30

Technical Breakdown

What follows is a long stretch of architecture and tradeoffs. It covers the technical decisions behind Tastemaker, and the reasoning behind each one, in about as much detail as a writeup can reasonably hold.

The point is not that the app is complicated. It is that the engineering is easy to miss. Design is what people see first, and it tends to overshadow everything underneath it, but just as much thought and time went into making this run fluidly across very different machines. That work is invisible when it succeeds, so this is where I make it visible. This breakdown took days to write, and it is not the kind of thing anyone recalls on a whim. It came out of a deep dive back through nearly a year of my own decisions, working out how to explain each one.

Overview

Tastemaker indexes folders of images already sitting on disk, turns them into browsable and taggable collections, then lets those images be pulled onto a free-form moodboard canvas that saves, bundles, and reopens straight from the file manager.

Three constraints shaped everything else. It is offline-first: no server, no account, no network dependency, with the filesystem as the source of truth and the database as a disposable index over it. It is non-destructive: the app never moves, renames, or rewrites an original, and the library survives the app being deleted. And it is built for library scale, meaning tens of thousands of images in one collection scrolled at 60fps on a laptop, not a demo folder of two hundred.

The stack: Rust and TypeScript, with a Tauri 2 shell, a React 19 frontend, and SQLite for the index.

Architecture

The app is a single native process. A Rust core owns the database connection, the thumbnail generator, the filesystem watcher, and the file access scope. One or more webviews render the interface, each a real top-level OS window with its own renderer rather than a div pretending to be one. Requests cross the boundary as typed commands, and the core pushes events back the other way: file changes, scan progress, integrity reports, and cross-window messages.

Images are never encoded into those messages or served over a local HTTP server. They travel through a scoped asset protocol that hands the webview a direct handle to a file on disk, so the webview's own decoder does the work off the JavaScript thread. That protocol is scope-gated for security, which means folders are registered when a collection is created, opened, searched, relinked, or referenced by a board, and dropped again once nothing needs them.

01

Rust core, system webview

The shell binds to the operating system's own webview instead of bundling a browser engine, so the shipped artifact is a native executable plus a static frontend rather than a 150MB Chromium runtime.

02

A typed boundary between the two

Rust takes parallel I/O, image processing, and durability. The webview takes rendering and the feel of a gesture. The interface is designed so neither side does the other's job.

03

SQLite as a disposable index

Write-ahead logging, foreign keys, indexed, schema version 5. The filesystem stays the source of truth, so losing the index costs a rescan rather than a library.

04

Data-parallel pipelines

Directory walking, metadata reads, decoding, and resizing all run across cores in a worker pool, entirely off the interface thread.

05

SIMD resizing into WebP

A vectorized Lanczos filter rather than a naive scalar pass, encoded at 768px: small enough to decode a screenful at a time, sharp enough for a wide grid.

06

A React frontend that only renders

It is handed precomputed data and never touches a file to make a layout decision, which is what keeps it out of the way of the work.

Opening 40,000 Images

This is the part worth defending in a code review, and it reduces to a single rule: never decode an image to decide where to put it.

Width and height are written into the database the first time a file is scanned, so the grid knows every image's aspect ratio before it has loaded a single pixel. Loading a collection is one indexed query that brings images and their tags back together, with tags arriving through a correlated subquery rather than a round trip per image, so there is no N+1. Ordering falls back through a nullable sort column, which means a manually reordered collection uses its manual order and an untouched one defaults to newest first, with no second code path and no backfill of existing data.

Layout is then pure arithmetic. Column width comes from the container, each item's height is that width divided by its aspect ratio, and items drop into whichever column is currently shortest. Laying out 40,000 images is 40,000 divisions and comparisons, well under a frame, with zero disk access. The laid-out plane is then sliced into fixed horizontal bands, and only the bands touching the viewport, plus a little overscan, are ever mounted.

The result is that decode work is bounded by the viewport rather than by the library. At any moment the webview is decoding a few dozen small files. The other 39,950 are arithmetic.

Ingest

Adding a folder runs a four-stage pipeline, two stages of which are embarrassingly parallel. The tree is walked and filtered to a fixed set of image extensions with metadata read across cores. That result is differenced against the paths already indexed for the collection, so only genuinely new files continue. New files are decoded, resized, and encoded in a worker pool. Then metadata is written to the database.

That diff is what makes every rescan incremental by default. A rescan of a 40,000-image collection that gained three files does three files of work, which is exactly what makes a live filesystem watcher affordable instead of a periodic full reindex.

Thumbnails are 768px WebP, resized through a SIMD-accelerated Lanczos filter rather than a naive scalar pass. WebP for alpha support and meaningfully smaller files at equivalent perceptual quality, and 768px so a thumbnail still looks correct when the grid is wide, without paying full-resolution decode cost. Each cache filename is a hash of the source path, which buys three properties at once: generation is idempotent, an already-processed image is skipped by a cheap existence check, and finding orphans reduces to a set difference. Animated files are special-cased to a static first frame, so an animated grid never costs animation, and the original plays only on demand.

Durability

Because the database is an index rather than the library, the failure posture can be relaxed where it should be and strict where it matters. Originals are never touched, so losing the index costs a rescan.

Schema migrations are deliberately conservative. A backup is taken before any migration runs, and the migration itself runs inside a transaction. Every migration so far has been additive, a nullable column and an index, so existing rows are never rewritten and an old database opened by a new build is correct the instant the migration commits. Default behavior is preserved by query design rather than by backfilling data.

On top of that sits an integrity layer: a check at startup, a health panel that surfaces the result on demand, and a repair routine that clears orphaned rows and invalid paths. The app logs problems and keeps running rather than refusing to start, because an index that can be rebuilt should never hold a library hostage.

Tags are the exception, because they are the only user-authored content that cannot be recovered from the filesystem, so they get three tiers of redundancy: the live database copy, a sidecar file written inside each collection so tags travel with the pixels when a folder moves to another machine, and explicit snapshot export and import with a report of what linked and what was not found.

Multi-Window

The moodboard is the second surface, and one component runs in three configurations. As a separate OS window with its own renderer, its own camera, and its own undo stack, so the gallery keeps scrolling while a board is composed on a second monitor. As a docked panel inside the main window, with resize clamped at both ends so neither surface can be squeezed out of existence. And as the application's entry point.

That last configuration is the interesting one. The board format is registered with the operating system, so double-clicking a file launches the process. A single-instance guard means a second double-click forwards its argument into the already-running process instead of starting a competitor that would fight over the same database file. The launch argument is parsed before the app builder even runs, staged into application state, and replayed to the moodboard window once it exists, so a board can open straight from the file manager without passing through the gallery at all.

One genuine platform constraint shaped the interaction model: HTML5 drag and drop is not reliable inside these webviews. Dragging is instead a custom mouse-driven context with an activation threshold and a portalled ghost element, which turned out better anyway, since the same drag is consumed both by the grid for reordering and by the docked board for dropping.

Interaction

The canvas applies the same discipline as the grid, but to interaction instead of data. The camera lives in a ref rather than in state and writes a transform straight onto a single content layer, so panning a board never re-renders the item list. Gestures are a state machine that paints positions directly onto DOM nodes while a drag, resize, draw, or crop is in flight, which is compositor work rather than layout. React is touched exactly once, on release.

Moving 200 selected images therefore costs one render and one undo entry, not one per frame. Undo is per-gesture rather than per-frame, with a coalescing key that merges rapid related edits, like an opacity drag or a held arrow key, into a single step.

The File Format

A moodboard is a versioned document, and the format's interesting property is that one extension covers two different containers. Linked boards are plain JSON holding canvas state, items, and paths: small, diff-able, and always in sync with the library, which is the right choice for personal work on one machine. Bundled boards are archives carrying every referenced image under a generated asset id, self-contained enough to send to someone who has never seen your filesystem.

The loader identifies which is which by reading the file's magic bytes rather than trusting its extension, so renaming a file cannot lie to it. Extraction is content-addressed and cached by a hash of the manifest, so reopening the same board reuses the previous extraction and two different boards can never collide.

Versioning is additive and forward-tolerant. New item types and per-image properties are optional, so an older document loads into a newer build unchanged, and a document from a newer version than the build still loads with a warning rather than being rejected outright. Failures are specific: a bundle missing an asset names the asset and the id it was referenced by, and a separate validation pass can report missing images before a board is loaded, rather than rendering holes and leaving the user to guess.

Cross-Platform

Cross-platform here means more than compiling for three targets. Paths are validated rather than assumed, against invalid characters, reserved device names, length limits, network paths, and non-ASCII content, then normalized to platform-native form. Relinking and duplicate detection both compare normalized paths, so the same folder reached by two different spellings is never imported twice.

Storage follows platform convention, with the database, thumbnails, caches, and logs in the correct per-OS application data directory rather than beside the executable. Webview quirks are handled where they belong: the slideshow timer runs in the Rust core and emits ticks to the frontend specifically because one platform's webview throttles JavaScript timers in backgrounded windows, and a slideshow that stutters when it loses focus is a broken slideshow.

A collection whose parent folder has moved can be relinked, singly or in bulk from a parent directory with a preview of exactly what will change, rewriting stored paths and re-registering file access and watchers without re-scanning or losing tags.

Trade-offs

One shared database connection sits behind a mutex. That is simple and correct, and viable because the expensive work, walking and decoding and resizing, is parallel while the writes themselves are short. It would need a connection pool before it needed anything else if concurrent writes grew. For the same reason, creating collections in batch is sequential even though scanning inside each one is parallel, because parallel creation would contend on that single connection.

Search is still a LIKE-based query. Full-text search is the planned upgrade, and the honest reason it has not happened yet is that the index has been fast enough that it has never been the bottleneck.

The canvas artboard stays light while the surrounding interface is theme-aware. Item text colors are stored per item, so a dark artboard would make every existing board unreadable. Dark chrome over a light canvas is the correct compromise, and the one most canvas tools have landed on.

Takeaway

The speed is a consequence of data modeling rather than of optimization. Store the aspect ratio at ingest, make layout arithmetic, virtualize the plane, and decode cost stops scaling with the library. None of this was a slow design that got tuned afterward, which is the difference between performance as architecture and performance as cleanup.

The other half is deciding what belongs on each side of a language boundary. Rust takes parallel I/O, image processing, and durability. The webview takes rendering and the feel of a gesture. Most of the decisions above fall out of that one split, and the ones that do not fall out of a second: treat the user's files as something you are a guest in, never the thing you own.

Tastemaker is obsessively optimized and still improving. Updates reach users on a one to three month lead time, and that gap is deliberate rather than slow: I use this tool every day, and I want to have lived with a change for a long stretch before anyone else has to. It is solo developed and solo maintained, so every release gets tested well past the point most people would call finished. Some would call that overkill. When you are the only person who can catch a regression, it is just the job.

(More Projects)

MyCliq.co Platform