8 Tips for Creating a Native Look and Feel in Tauri Applications
Tags: tauri, rust, react, webview
Published: 2022-10-07 11:31:25

I recently released [Jomai](https://jomai.app), a Markdown-focused desktop search app made in Tauri.
Apps made with Tauri is, of course, native app. But its UI part runs on WebView, so its look and feel tend to be like a web app. I designed Jomai to behave as much as possible like a native app, so I will summarize that here.
Jomai is currently available only for macOS, so this article is also intended for macOS.

Environment:
- Tauri: 1.1.1
- Platform: macOS

## Contents
* [Get rid of the beep sound on keystrokes for non-input ](#item-1)
* [Make text in UI not selectable](#item-2)
* [Set mouse cursor to default](#item-3)
* [Do not scroll the entire screen](#item-4)
* [Suppress bounce scrolling across the screen](#item-5)
* [Prepare your own menu](#item-6)
* [Prepare your own keyboard shortcuts](#item-7)
* [Support dark mode](#item-8)

## Get rid of the beep sound on keystrokes for non-input fields <a id="item-1"/>
A beep sounds when keystrokes are made while the focus is on a non-input item other than `<input>` or `<textarea>`. This issue occurs in WebView on macOS and has a GitHub issue, which has not been resolved as of October 6, 2022.
[https://github.com/tauri-apps/tauri/issues/2626](https://github.com/tauri-apps/tauri/issues/2626)

You can suppress the beep sound by calling `preventDefault()` on `keydown` event, but this will also disable all input to `<input>`, etc., so it is necessary to suppress it selectively.

Jomai uses the following code.
```typescript
export const useKey = (callback: UseKeyCallback) => {
  const onKeydown = useCallback(
    (event: KeyboardEvent) => {
　　　 // callback should return true if processed
      const consumed = callback(event.code, {
        ctrl: event.ctrlKey,
        shift: event.shiftKey,
      });

      const e = event.composedPath()[0];
      if (
        !(e instanceof HTMLInputElement || e instanceof HTMLAreaElement)
        || consumed
      ) {
        // preventDefault() if it is not an input item
        // also preventDefault() if it was handled by a callback
        applyBeepSoundWorkaround(event);
      }
    },
    [callback],
  );

  useEffect(() => {
    window.addEventListener('keydown', onKeydown);
    return () => {
      window.removeEventListener('keydown', onKeydown);
    };
  }, [onKeydown]);
};
```

It solves most cases of the problem to call `preventDefault()` if the event source is not an `HTMLInputElement` or `HTMLAreaEelement`. However, when the focus is on one of these input items and the keyboard shortcut is used to move the focus to a non-input item (yes, it isn't very easy), there will be the sound. So I've done more to fix the problem.

## Make text in UI not selectable <a id="item-2"/>
While it is normal to be able to select text on a web page, this is generally not the case in native apps. To avoid unintentional text selection when trying to manipulate the UI, use `user-select: none` at the base of DOM elements such as `<body>`.

```css
body {
  user-select: none;
}
```

## Set mouse cursor to `default` <a id="item-3"/>
Since the previous section has limited the operation of UI elements to button pressing, etc., let's express it that way for the mouse cursor as well.

```css
body {
  cursor: default;
}
```

`cursor: default` prevents I-beam when hovering text, for example. The `cursor` attribute should be explicitly specified if the UI element is manipulatable, such as a button.

```css
button {
  cursor: pointer;
}
```

## Do not scroll the entire screen <a id="item-4"/>
When content is larger than the size of the screen, it's usual for web pages to scroll the entire screen. In native apps, it is natural to display fixed UI components and make only some scrollable.

In Jomai, tabs and forms at the top of the screen are always visible, and only the content part at the bottom is scrollable.


![scrollable and non-scarollable areas in the app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bcpxn382l39cq70z25u8.png)
See it in action.

![only contents area is scrollable](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yheu6wni6r57dzerhltx.gif)

To achieve this, set `overflow: hidden` for the entire screen, and set `overflow: scroll` and `height` for the scrollable area.

```css
.page {
  overflow: hidden;
}

.contents {
  overflow: scroll;
  height: calc(100vh - 40px)
}
```

## Suppress bounce scrolling across the screen <a id="item-5"/>
Bounce scrolling is that thing that makes a spring-like motion when scrolling to the edge of the screen. By default, the entire screen is scrollable with the bounce motion. See this video.

![bounce scroll](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tae52qpjh4f0kbtiu1ai.gif)

Bounce scrolling is associated with scrollability, so it should be suppressed if the entire screen should not be scrollable.

```css
body {
  overflow: hidden;
}
```

## Prepare your own menu <a id="item-6"/>
Tauri provides a basic menu by default.

![default menus](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l6e7szlvm6jnze09o5dv.png)

If you need customization, you will need to build these menus yourself, which is a bit tedious. See `tauri::Menu::os_default` for reference.

## Prepare your own keyboard shortcuts <a id="item-7"/>
Be careful when using your own keyboard shortcuts on web pages, as they can confuse users, but be proactive about introducing them in native apps.

## Support dark mode <a id="item-8"/>
I am a fan of dark mode and would like to see dark mode support in apps I use regularly.
Tauri has `appWindow.theme()` to get the current theme and `appWindow.onThemeChanged()` to monitor change events. Using these, you can change the app to match the OS theme settings.

Here is an example implementation in React.
```typescript
import { appWindow, Theme } from '@tauri-apps/api/window';

export const useTheme = () => {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  useEffect(() => {
    let unlisten: UnlistenFn | undefined;

    (async () => {
      setTheme(await appWindow.theme());

      unlisten = await appWindow.onThemeChanged(({ payload: theme }) => {
        console.log(`theme changed to ${theme}`);
        setTheme(theme);
      });
    })();

    return () => {
      if (unlisten != null) {
        unlisten();
      }
    };
  }, []);

  return theme;
};
```

## Summary
In this article, I have introduced some tips to make Tauri-made apps more native-like. A little effort will improve the usability of your apps. I hope you find them helpful.

Overall, developing with Tauri has been a great experience, and I look forward to creating many more apps with Tauri.

Please also check out [Jomai](https://jomai.app), which I'm developing using these techniques.