On Monday, Apple released the new version of its operating system, macOS 26 Tahoe. With it comes a bunch of new features, apps, and the big design overhaul, called “Liquid Glass.” There have been quite a few discussions on the pros and cons of this new design language, which I don’t want to go into here.
Instead, I want to talk about icons. Because those also got a facelift with the new update. Since I had less to do this week, I took my newly-gained freedom to sit down after work and try to get Zettlr’s app icon up to speed to conform with this new logo design.
What I imagined being a quick exercise in testing out Apple’s new Icon Composer — an app that does make creating icons very easy — turned out to take my entire evening. The main reason for this is that I don’t use XCode. After all, the app is cross-platform, and while I am happy to go the extra mile to accommodate macOS, Windows, and Linux, there is a limit to how much I can do.
It turns out that, while redesigning the entire app icon process, Apple also decided to clean it up a bit, and so now they added this subtle requirement for having XCode installed. In this article, I’ll quickly go over what developers have to do without having to keep XCode installed because they – for example – rely on CI pipelines to automatically build their apps on computers without XCode.
Creating macOS Icons — Then and Now
First, a short excursus for what has changed that suddenly makes adding custom icons to macOS apps so much less straight-forward than previously.
For the past decades, all operating systems had relatively simple mechanisms to defining software icons. On Windows, it has been and will always be .ico
-files, Linux opted for PNG images, and macOS for the longest time had .icns
-files.
Essentially, .icns
-files are just Zip-archives of the same icon in various sizes. To make an app icon look as good as possible across our vastly differing screen sizes, Apple decided to invent this icon format to store various pre-calculated sizes in one place. That’s pretty neat!
However, with Liquid Glass, they made some changes. Specifically, and Apple themselves said this in a training video, there are two issues that make having a set of static images less desirable with the new iteration of Apple’s operating systems. First, the icon should be the same on all platforms, including iOS, iPadOS, watchOS, visionOS, macOS, and whichever OS I just forgot to list. All of these devices come with different screen sizes and layouts for icons. Having several icons for each of these platforms would increase the size of this small icon file by a large amount. Second, and more crucially, however, the new Liquid Glass design rests on a lot of graphical fidelity and effects. We are talking about background filters, specular lighting, blurring, and various amounts of color tinting. Trying to fit all of that into static images would just be insane.
So they opted for a new format, aptly named .icon
. This is not a file, but rather a folder that contains two things: The vector graphics that make up the icon, and a file describing certain properties of the various icon layers. For example, app developers can choose the amounts of translucency for certain parts of their icon, how the different layers blend in with each other, and so on.
However, this .icon
-folder just contains some instructions on how to create the icon — it’s not yet the icon itself that your app can use. That is why it’s now a bit more difficult to add such a new icon to your app, especially if you don’t use XCode. For XCode users, nothing has changed; they can continue to import their icons into XCode and the software will make sure that the icon ends up in the correct place.
For everybody else — this includes Electron developers, Tauri developers, React Native, and anyone who develops for macOS but without XCode — this process is now much more involved.
Creating a Liquid Glass Icon (Almost) Without XCode
From a bird’s eye perspective, the new workflow for creating icons outside XCode looks like this:
- Use the Icon Composer to generate the
.icon
-file. - Run the
actool
utility to compile it into anAssets.car
file - Place the
Assets.car
-file into theContents/Resources
folder of your app bundle and sign it. Make sure that it’s signed. - Add the entry
CFBundleIconName
to your app’s.plist
-file.
Step 2 — actool
— is what actually introduces the dependency on XCode. For whatever reason, this tool comes preinstalled on macOS, but requires XCode to work. So unfortunately, to create a Liquid Glass icon you will need to have access to a Mac with XCode installed on it. However, once you have created the Assets.car
-file, you can ditch the Mac. I recommend you do step 1 and 2 at the same time, and then you can figure out steps 3 and 4 later, depending on your tool chain.
To get started, you will need to create an icon using Apple’s Icon Composer. This is not part of this guide, so I recommend consulting Apple’s resources to learn more, starting with this informative video.
Once you have the .icon
available, you’ll need to compile it into an Assets.car
-file. This file is an “asset catalog” and it is essentially very similar to the old .icns
-files. However, unlike the .icns
-files, it does not just contain a set of images, but additional instructions and materials such as gradients, colors, and basically all the things that macOS needs to properly render an icon in the new Liquid Glass UI.
To generate such an asset catalog, we need the actool
mentioned above. It is essentially a small utility that enables compilation of these catalogs, and XCode uses it when you develop, e.g., iOS or macOS apps.
Apple intended the tool for compiling apps via XCode, but there is nothing preventing us from running the tool without any code project. Here’s a handy Bash-script that I have written which you can adapt to take in an .icon
-file, and spit out an Assets.car
file. Note that it will generate a .plist
-file as a side result, and you cannot disable this behavior. But you can just remove that file afterwards.
#!/usr/bin/env bash
ICON_PATH="./resources/icons/Zettlr.icon"
OUTPUT_PATH="./resources/icons"
PLIST_PATH="$OUTPUT_PATH/assetcatalog_generated_info.plist"
DEVELOPMENT_REGION="en" # Change if necessary
# Adapted from https://github.com/electron/packager/pull/1806/files
actool $ICON_PATH --compile $OUTPUT_PATH \
--output-format human-readable-text --notices --warnings --errors \
--output-partial-info-plist $PLIST_PATH \
--app-icon Icon --include-all-app-icons \
--enable-on-demand-resources NO \
--development-region $DEVELOPMENT_REGION \
--target-device mac \
--minimum-deployment-target 26.0 \
--platform macosx
rm $PLIST_PATH
Take note of the filename of your icon, because actool
will quietly assume this to become the name of your icon in the asset catalog. Since asset catalogs contain all assets of an app within a single file, it is important to tell macOS which of the many assets is the actual icon.
At this point, you should have your icon, and your icon inside an asset catalog that you can now use to build your app with whatever tool chain you use.
Adding the Icon to Your App
So, how do we get this icon into the app? For this we will have to do two things: First, add the Assets.car
into your app’s Resources
-folder and sign it, and second, add the key CFBundleIconName
with the string value Icon
(or whatever your icon has been named) to the app’s .plist
-file.
Let’s start with adding the Assets.car
-file. If you’re using Electron Forge to build the app, adding the catalog to your app requires a single line of code. Inside your Forge configuration, you simply need to add the path to your .car
-file to the property packagerConfig
→ extraResource
. This ensures that Electron will pick up the file at the correct time, copy it into the Resources
-directory, and sign it.
For Tauri, it’s almost as simple. Here, the property is called bundle
→ resources
, and this should also place the file correctly. For any other tool chain, there should be an analogous way of performing this step.
Next, you need to tell macOS that there’s a new icon in your app. You do this by modifying your app’s .plist
.
Again, if you use Electron Forge, it is extremely simple. Add the key CFBundleIconName
either to your .plist
-file, if you make use of it, or provide it directly as a key to your extendInfo
-property in the packagerConfig
.
If you place it into a .plist
-file, it should be contained under the top-level <dict>
, and look like this:
<key>CFBundleIconName</key>
<string>Icon</string>
A minimal example for Forge would look like this:
// In file forge.config.js
module.exports = {
packagerConfig: {
extraResource: [ './path/to/Assets.car' ],
extendInfo: {
CFBundleIconName: 'Icon'
}
// Alternatively: extendInfo: './path/to/info.plist'
}
}
For Tauri, the process looks essentially the same and is fully documented here.
Final Thoughts
While it took me an entire night to figure this process out, which, in the end, turned out to be relatively benign, I hope that these instructions will help you do the same but faster.
Just a final note: Don’t just throw out your existing .icns
-file. There are still many out there with Macs that don’t run the newest macOS, and if you remove the .icns
-file, the app will break for all of these people. For the coming years, be prepared to ship both the .icns
-file and the new Liquid Glass version.