devin logo

tricks to make web apps feel native

← posts

a macos window with the tauri and electron icons inside

tricks to make web apps feel native

open-source, web, tauri

For a few months I’ve been working on a music player for OpenSubsonic servers (Navidrome, Gonic, etc) called tinysub. Rather than a Qt or SwiftUI app, I went with a web app so that this could run on any device easily.

One of the biggest things I focused on while developing this app was performance and bundle size, which Svelte and Vite mostly solved for me. But another thing I focused on was making it look and feel close to a native application, atleast on MacOS.1 So I wanted to write down some notes about my experience, in case it might help someone in the future.

Here’s examples of how it turned out:

example screenshot of the tinysub app running in a tauri window

example screenshot of the tinysub app running in a tauri window, in miniplayer mode

I’m using Tauri to build tinysub, however I’m sure you could probably achieve all the same things with Electron.

(Edit 8/15/2026: I have switched over from Tauri to PWA entirely to simplify the dev and user experience - I don’t see a reason to use Tauri/Electron for this kind of project since it doesn’t interact with the system or require native code. I may write about making convincing PWAs in the future, because you can actually do quite a lot and I find they’re very underrated)

tricks

1. use system font size and family

Browsers typically use 16px font size by default. On MacOS, most native applications typically use around 13px font size by default for most elements. Windows Win32 is probably closer to 12px. Changing the default font size can make a huge difference in how your application looks and feels compared to native, and having a smaller font size also lets you fit much more on the screen.

html {
font-size: 13px;
}

Consider making this user-configurable in your app for accessibility reasons. I’d add a “browser default” setting, using 1rem in the styling, which will use the user’s font size set in their browser.

Unless you are choosing a specific font for your brand, I also recommend using the system font (San Francisco on MacOS, Segoe UI on Windows). Not only does this make your app bundle lighter, since you’re not bundling any fonts, it also looks more consistent with the rest of the operating system. This is the stack I typically use (from modern-normalize):

html {
font-family:
system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji";
}

screenshots showing web app with 16px and 13px fonts, and a native app using default font size for comparison

2. some CSS properties to change

In the HTML element, you should probably disable user-select (and -webkit-user-select for Safari) so that your user can’t select text, which stays consistent with native apps. You should re-enable this for prose elements.

html {
-webkit-user-select: none;
user-select: none;
}

I recommend disabling overscroll so that you don’t get that “elastic scroll” effect on the main window, which is seen when scrolling on trackpads or mobile devices:

html {
overscroll-behavior: none;
}

I recommend hiding the tap highlight on iOS devices, which usually causes a blue tint when you press a button:

html {
-webkit-tap-highlight-color: transparent;
}

3. use proper viewport sizing

Mobile browsers dynamically shift and resize their search and action elements to make more space for the page. The typical vh and vw units do not dynamically change with those elements, often causing the viewport to exceed the browser view area. To mitigate this, use dvh and dvw, which do dynamically change. (for more details, check out this article by Anthony Frehner)

body {
height: 100dvh;
width: 100dvw;
}

4. adjust whitespace for safe-area

Another thing you should consider is adjusting whitespace for safe-area. For example, on an edge-to-edge phone where rounded corners are part of the screen, buttons in a footer/action bar could be impossible to reach if they are located behind those rounded corners, or otherwise uncomfortable to reach if they are too close to the bottom edge of the display.

Luckily, this is very easy to solve, thanks to the CSS env(safe-area-inset-*) variables which dynamically change based on the device you’re viewing on. In this case I went with borders instead of padding so that I could customize the color of the whitespace for each side, such as matching the bottom whitespace with the footer color of my app:

body {
border-top: env(safe-area-inset-top) solid transparent;
border-right: env(safe-area-inset-right) solid transparent;
border-left: env(safe-area-inset-left) solid var(--bg-sidebar);
border-bottom: env(safe-area-inset-bottom) solid var(--bg-footer);
}

On desktop these will typically have no effect but you will notice it on mobile devices:

comparison of safe area and no safe area whitespace on a mobile device

5. use native context menus

In Tauri/Electron apps, I recommend using native context menus, which tend to be more accessible, more consistent, and gives you less code to deal with than making your own custom context menu. In tinysub specifically, I use a custom context menu in the web builds, and native context menus in the Tauri builds.

Here’s the Tauri and Electron documentation to get started, and here’s what it can look like:

comparison of system vs native context menu

6. consider a custom titlebar

You may want to consider a custom titlebar. Doing so can allow the titlebar to flow better with your content, or allow you to save space by placing actions in the titlebar itself. Personally, I did not make a custom titlebar for tinysub as I felt it wasn’t necessary, and custom titlebars can often be a bit annoying on Linux.

Here’s the Tauri and Electron documentation to get started on custom titlebars.

(Edit 8/15/26: If you’re making a PWA, the built-in Window Controls Overlay feature makes this very clean and easy to implement cross-platform! I have since implemented that into tinysub)

7. only show window after page is loaded

I recommend disabling window visibility on startup, and then have your app code show the window only once the page is ready. This prevents any flashing or blank screens while your app is loading. For example, for Tauri:

let window = WebviewWindowBuilder::new((app, "main".to_string(), WebviewUrl::External(url)))
.title("tinysub")
.visible(false) // hide window initially, javascript code will unhide later
//...other window properties
export const init = async () => {
//...code to execute before showing the window
if (isTauri) {
const { getCurrentWindow } = await import("@tauri-apps/api/window");
const win = getCurrentWindow();
await win.show();
await win.setFocus();
}
//...code to execute after window is done showing
};

8. window blur/vibrancy

On MacOS and Windows, it’s relatively easy to achieve blur behind sidebars in Tauri with the window-vibrancy plugin. Vibrancy is possible on Linux desktops too, though on Linux, the compositor is what applies the effect, not the application itself, and only certain compositors support blur (i.e. kwin).

Here’s a snippet from my app, though I’d recommend reading the full documentation:

use window_vibrancy::*;
// macos vibrancy (using sidebar material)
#[cfg(target_os = "macos")]
apply_vibrancy(&window, NSVisualEffectMaterial::Sidebar, None, None)
.expect("unsupported platform! 'apply_vibrancy' is only supported on macOS");
// windows vibrancy (using mica material)
#[cfg(target_os = "windows")]
apply_mica(&window, None)
.expect("unsupported platform! 'apply_mica' is only supported on Windows 11");

You will need to enable transparency in the Tauri window builder. After that, make sure to set the body’s background-color to transparent so that you can actually see through the app, and then apply opaque colors to the main app section (in this case the song queue), leaving the sidebar transparent.

let window = WebviewWindowBuilder::new((app, "main".to_string(), WebviewUrl::External(url)))
.title("tinysub")
.transparent(true) // needed to see window-vibrancy effect
//...other window properties
body {
background: transparent;
}
#queue {
background: Canvas;
}

This effect can really help make your app feel more consistent with the rest of the system, though I’d recommend making transparency configurable for Linux users who don’t have a compositor that supports blur. And then that gives you this beautiful transparency effect:

window translucency example

9. colors

In tinysub, I sometimes use OS-specific styling to better match native applications. For example, macOS uses a different sidebar/titlebar border color and custom selection colors. Small details like these can make a noticeable difference in how well an app blends in with the rest of the system.

Where possible, though, I tend to avoid hard-coded colors entirely. Instead, I use CSS <system-color> values and the color-scheme property for light and dark mode. This keeps the CSS simpler and means tinysub needs very little dark-mode-specific styling while still supporting dark mode fully.

<system-color> values can vary between browsers and operating systems. For example, Safari on macOS uses a slightly brighter Canvas color than Chromium, which matches the typical background of native macOS apps.

They can also adapt to the operating system itself. In dark mode, for example, Safari’s Canvas color is black on iOS–better suited for OLED displays—but is gray on macOS.

10: other tricks

For even more tricks on making your app feel native, this article by Vadim Demedes is a great resource. I just wrote this article to add on some other ideas too.

ending notes

Apologies if this article is a little sloppy, these were just notes that I personally wrote for myself while developing tinysub. Hopefully it was useful to you or informational in any way. If you have any suggestions, let me know in the comments and I’ll edit the article accordingly.


Footnotes

  1. When I am referring to “a native look on MacOS”, I am specifically referring to pre-Tahoe design. I don’t intend on implementing Liquid Glass elements into my app as it’s tricky to do in CSS, would be far too heavy, and I am personally not a fan of it regardless. However, the Cupertino theme for Obsidian does an excellent job on emulating the MacOS Tahoe design if you’d like to see an example of that.


comments

wanna comment? reply on bluesky

loading comments