Slint ♥ .NET

Disclaimer: short on time, I wrote this article with heavy help from AI. I reviewed it, and the code and claims are checked against the bindings.

Write your UI once in Slint's declarative language, and your logic in C#. The new community .NET bindings bring typed C# classes, hot reload of both UI and code, native binaries for six desktop platforms, and now Android and iOS too.

Yuriy Holembyovskyy · October 2026 · 1.19.0-preview.10

C# developers have plenty of UI frameworks to choose from. WPF and WinUI stay on Windows. MAUI, Avalonia, and Uno each bring their own XAML dialect and their own trade-offs. So why add another one?

Because Slint is different in a few ways that matter. It's a small, native toolkit written in Rust, which runs on desktops and all the way down to microcontrollers. You describe the UI in .slint files, a compact language built for UIs, with reactive properties and a live preview in your editor. And the same .slint files already work from Rust, C++, JavaScript, and Python.

Now they work from C# too. I built .NET bindings for Slint, and this post shows what they can do.

Hello, Counter

A Slint app for .NET is an ordinary console project with one package. Create it:

dotnet new console -o MyApp
cd MyApp
dotnet add package yurkinh.slint.net --prerelease

Describe the window in Slint:

ui/app-window.slint

import { Button, VerticalBox } from "std-widgets.slint";

export component AppWindow inherits Window {
    in-out property <int> counter: 42;
    callback request-increase-value();
    VerticalBox {
        Text { text: "Counter: \{root.counter}"; }
        Button {
            text: "Increase value";
            clicked => { root.request-increase-value(); }
        }
    }
}

Tell the build about the file:

MyApp.csproj

<ItemGroup>
  <SlintFile Include="ui/app-window.slint" />
</ItemGroup>

And write the logic:

Program.cs

using MyApp;

internal static class Program
{
    [STAThread]
    private static void Main()
    {
        using var app = AppWindow.Create();
        app.OnRequestIncreaseValue(() => app.Counter++);
        app.Run();
    }
}

dotnet run opens the window. Notice what you didn't write: no string property names, no casts, no binding boilerplate. AppWindow, Counter, and OnRequestIncreaseValue are real C# members, generated from the .slint file at build time. Rename the property in Slint, and the C# compiler tells you every place that needs to change.

From preview.10, a template sets all of this up in one step:

dotnet new install yurkinh.slint.net.Templates
dotnet new slint -o MyApp
cd MyApp
dotnet run

The project it creates already turns on live preview for Debug builds, so dotnet watch reloads the UI and the code, and Native AOT for Release builds, so dotnet publish builds a native executable. --style picks the widget style, from Fluent to Cupertino.

slint-android and slint-ios do the same for phones, and slint-xplat creates all three at once: a solution with a desktop, an Android, and an iOS project that share one ui/ folder and one Shared/ folder of C#. Each project still generates its own classes, so a property you add in Slint shows up on every platform with the next build.

dotnet new slint-xplat -o MyApp --platforms desktop --platforms android

How It Fits Together

The bindings follow the same design as Slint's Python and Node.js integrations, with one C#-shaped addition at build time:

At build time .slint files→ slint-compiler -f csharpMSBuild, <SlintFile>→ typed C# classesrecords, enums, On… / Invoke…
At run time your C# code→ Slint.dllLibraryImport, SafeHandles→ libslint_dotnetSlint interpreter and renderers, in Rust

The native library is a thin C interface over the Slint interpreter, the same engine behind the Python and Node.js bindings. The C# side uses source-generated LibraryImport calls and safe handles, so the library is compatible with trimming and Native AOT. At startup, the generated code loads your .slint file and checks that it still matches the classes it was generated from. If someone changed the file without rebuilding, you get a clear exception instead of a mystery.

It Feels Like C#

Slint's types map to the types you'd pick by hand. An int is an int, a duration is a TimeSpan, a struct becomes a record, and an enum becomes an enum. Struct field defaults carry over, so new TodoItem() starts with the values that the .slint file declares.

var todos = new ListModel<TodoItem>();
app.Todos = todos;
todos.Add(new TodoItem { Title = "Write the blog post" });
todos[0] = todos[0] with { Done = true };   // the UI updates

// Views that follow the model's changes
app.OpenTodos = todos.Filter(t => !t.Done).Sort((a, b) => a.Title.CompareTo(b.Title));

Async code works the way you'd hope. While the event loop runs, a SynchronizationContext brings every await back to the UI thread, so a callback can fetch data and update the UI without any marshaling:

app.OnRefresh(async () =>
{
    app.Status = await httpClient.GetStringAsync(url);
});

Globals, windows, timers, images, translations, keyboard shortcuts, and drag and drop are all there. So is the untyped API, for UIs you only know at run time: new Compiler().BuildFromPath(...), then properties and callbacks by name.

Hot Reload, Both Ways

This is my favorite part. Turn on Slint's live reload for Debug builds:

<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
  <SlintLivePreview>true</SlintLivePreview>
</PropertyGroup>

Then run:

dotnet watch

Save a .slint file, and the window reloads with the new UI. Save a .cs file, and .NET Hot Reload applies the change. The app keeps running the whole time, with its models, its property values, and its callbacks intact. Tweak a layout, fix a handler, tweak the layout again, and never lose the state you were testing. It also works from the Hot Reload button of Visual Studio, VS Code with C# Dev Kit, or Rider.

If a reload removes a property your code still sets, the app prints a warning and keeps going. Rebuild when you're ready, and the compiler shows you what to update.

Every Platform, One Package

The package ships the native library for every common desktop target, and for phones:

PlatformRuntime identifiers
macOSosx-arm64, osx-x64
Linuxlinux-x64, linux-arm64
Windowswin-x64, win-arm64
Androidandroid-arm64, android-x64
iOSios-arm64, iossimulator-arm64, as a framework

Publishing works the usual way, including Native AOT:

dotnet publish -c Release -r osx-arm64 -p:PublishAot=true

One Windows detail: the window needs a single-threaded apartment, so mark Main with [STAThread]. Top-level statements can't take the attribute, which is why the sample above writes Main out. Forget it, and you get an exception that says exactly that, instead of a crash deep in the windowing system.

And Now, Android

The same package runs on Android phones and tablets, from preview.7. Slint already supports Android from Rust and C++, so the bindings follow the C++ design: Slint owns the app's entry point and calls your code, so there's no Native AOT trickery and no waiting for .NET 11.

Derive the main activity from SlintActivity, and create your components in SlintMain:

MainActivity.cs

using Slint.Android;

[Activity(MainLauncher = true, ConfigurationChanges = SlintActivity.ConfigurationChanges)]
[MetaData(SlintActivity.LibraryNameKey, Value = SlintActivity.LibraryName)]
public class MainActivity : SlintActivity
{
    protected override void SlintMain()
    {
        using var app = AppWindow.Create();
        app.OnRequestIncreaseValue(() => app.Counter++);
        app.Run();
    }
}

That's the same AppWindow, generated from the same .slint file, as on the desktop. The build packs the .slint files into the APK, and the package brings the native library for arm64-v8a and x86_64. Under the hood, SlintActivity is a NativeActivity: Slint runs its event loop on a thread of its own, and calls SlintMain there, so your callbacks run where Slint's objects live.

The todo example shows how far that goes: its Android version shares all of its C# logic with the desktop version. Adding, sorting, and filtering entries work the same, including the on-screen keyboard.

Android ends apps in the background when it needs the memory, and users expect to find them as they left them. From preview.8, return what to keep from SlintSaveState, and take it back in SlintRestoreState:

protected override byte[]? SlintSaveState() =>
    JsonSerializer.SerializeToUtf8Bytes(todos, TodoJson.Default.SavedTodos);

protected override void SlintRestoreState(byte[] state) =>
    Load(JsonSerializer.Deserialize(state, TodoJson.Default.SavedTodos));

Both run on the Slint thread, so they can read and change the UI directly. SlintPause, SlintResume, and SlintLowMemory report the other activity events.

And iOS, Too

From preview.10, the same package runs on iPhones and iPads, with iOS 15 or later, through Slint's own iOS support: Skia on Metal. Like on Android, Slint owns the entry point. There's no AppDelegate and no UIApplication.Main, just the Main you'd write on the desktop:

Program.cs

internal static class Program
{
    private static void Main()
    {
        using var app = AppWindow.Create();
        app.OnRequestIncreaseValue(() => app.Counter++);
        app.Run(); // Slint starts the app here, and Run doesn't return
    }
}

The app's Info.plist names Slint's scene delegate, and the build does the rest: it embeds Slint as a framework, and puts the .slint files into the app bundle. dotnet new slint-ios sets it all up, and dotnet new slint-xplat creates a desktop, an Android, and an iOS project that share one UI and one folder of C#.

Native AOT works on devices, so the app ships as one native executable and Slint's framework, with no Mono runtime inside. On an iPhone 13, the LOLs test reaches about 205,000 per second, the same as on Android. iOS can end an app in the background at any time, and since UIKit's notifications arrive on the Slint thread, the todo example saves its list when it leaves the screen, with the same code as on Android.

The todo example on iOS: a text field, an Add New Entry button, and a list of todos with checkboxes, in the iPhone simulator
The todo example, in the iPhone simulator.

Draw With OpenGL

Sometimes a UI needs pixels that no widget draws: a shader effect, a 3D view, or video from a decoder. From preview.9, C# can draw them with OpenGL, below or on top of the Slint scene, or into a texture that an Image shows.

new BackendSelector().RequireOpenGL().Select();
using var app = App.Create();
DemoRenderer? renderer = null;
app.Window.SetRenderingNotifier((state, api) =>
{
    switch (state)
    {
        case RenderingState.RenderingSetup:
            // GL loads glDrawArrays and friends with api.GetProcAddress.
            renderer = new DemoRenderer(new GL(api));
            break;
        case RenderingState.BeforeRendering when renderer != null:
            // Renders the cube into a texture, and returns Image.FromBorrowedOpenGLTexture of it.
            app.Texture = renderer.Render(app.SelectedRed, app.SelectedGreen, app.SelectedBlue,
                app.RequestedTextureWidth, app.RequestedTextureHeight);
            app.Window.RequestRedraw();
            break;
    }
});
app.Run();

The C# versions of Slint's opengl_underlay and opengl_texture examples do exactly this. They call OpenGL through C# function pointers, so they need no OpenGL package, and pick the GLSL version at run time, since macOS only has desktop OpenGL.

The OpenGL underlay example: an animated grid of diamonds that a shader draws below Slint's text and checkbox
A shader below the scene
The OpenGL texture example: a lit blue cube in a Slint Image, above sliders for its color
A cube rendered into a texture

WGPU is a different story: Slint hands its WGPU device over as Rust objects, which C# can't use, so the wgpu_texture and Bevy examples stay Rust only.

Charts With Microcharts

A UI toolkit is only as useful as the libraries that work with it, so I tried one that .NET developers already know. Microcharts draws bar, line, point, donut, radar, and gauge charts with SkiaSharp, for MAUI and the native iOS and Android views. Its charts don't care where their pixels go, so a Slint view for them takes about a hundred lines: a ChartRenderer draws the chart at the size of a Slint Image, in physical pixels, and hands Skia's pixels over with Image.FromPremultipliedRgba8, new in preview.10, without converting them.

SlintTimer.Install(); // chart animations step on the Slint thread
using var app = AppWindow.Create();
var renderer = new ChartRenderer(app.Window, image => app.SalesChart = image)
{
    Chart = new BarChart { Entries = entries },
};
app.OnSalesChartResized(renderer.Resize);
app.Run();

The same gallery runs on the desktop, Android, and iOS, with the charts animating in, and Slint's layout and scrolling around them. Microcharts itself needed a single change: its core project no longer turns on MAUI, which it never used, but which made Android load MAUI at startup. It's in the slint branch of my fork, on its way to Microcharts as a pull request, so that Microcharts.Slint ships with Microcharts' other packages.

Microcharts on Slint on Android: a bar chart, a point chart, and a line chart in white cards
Android
Microcharts on Slint on iOS: the same gallery in the iPhone simulator
iOS

Down to Embedded Linux

Slint runs on boards without a desktop too: its linuxkms backend draws straight to the display and reads input with libinput. The same package covers it, so publish for the board and start the app on its console:

dotnet publish -c Release -r linux-arm64 --self-contained
sudo SLINT_BACKEND=linuxkms ./MyApp

Before reaching for real hardware, I tried it in an arm64 Kali Linux virtual machine in UTM on my Mac. Every sample ran with the software renderer, and typing into the todo example worked through libinput. The OpenGL renderers failed at the first frame, though: Slint handed the virtual GPU a buffer layout that its driver doesn't take. With a small fix in Slint, in preview.10, Skia, FemtoVG, and the OpenGL underlay all render on the console.

The printer demo on the Linux console, drawn with the software renderer
The printer demo, software renderer
The todo example on the Linux console, with an entry typed through libinput
A todo typed through libinput
The OpenGL underlay on the Linux console, rendered with Skia's OpenGL renderer
The OpenGL underlay, Skia OpenGL

Samples You Can Run Today

Most examples and demos of Slint now have a C# version in their dotnet folder, right next to the Rust, C++, Node.js, and Python ones: 27 of them, from the todo app to a weather app with live forecasts. Each one is a regular project you can open, run, and change.

dotnet run --project demos/weather-demo/dotnet

Porting them was also a good test of the bindings. Whenever a sample needed something C# didn't have yet, the bindings got it: sending key presses to a window for an on-screen keyboard, carrying a C# object through drag and drop, building images from pixels or downloads, and saving keyboard shortcuts. The plotter draws its 3D chart and the image filter its effects in plain C#, without a library.

The newest one is a port of Jonathan Peppers' LOLs per second, the test he used to profile .NET MAUI: rotated "lol?" labels in random colors, as fast as the UI takes them. It runs on the desktop and on Android, in two modes, one that creates an element per lol and one that reuses a fixed set, like the original's canvas version.

dotnet run --project examples/lols/dotnet -- elements

Every sample also starts in CI on macOS, Linux, and Windows, and the Android todo starts in an emulator. That smoke test paid for itself on its first run: it caught seven samples that crashed on Windows because of how MSBuild resolves paths with ../, a bug that would have hit your apps too.

Head to Head on a Real Phone

How does Slint compare with the native options, and with Flutter, on the same phone? I built the original LOLs per second apps from Jonathan's repository for .NET 10, in Release, wrote a Flutter version with the same modes, pacing, and counting as the Slint port, and ran them all on a Motorola edge 60 neo with Android 16.

One label per lol

Each app adds 5,000 rotated labels, keeps the newest 500 on screen, and sleeps a millisecond after each one (MAUI sleeps two, as in the original).

AppWhat draws a lolLOL/s
JavaTextView625
.NET for AndroidTextView, through a Java helper587
.NET MAUILabel271
Fluttera keyed, rotated Text widget880
Slint for .NETa Text element per row of a ListModel821
The Java app with 5,000 lols
Java, 624.59
The .NET for Android app with 5,000 lols
.NET for Android, 587.29 (label covered)
The .NET MAUI app with 5,000 lols
.NET MAUI, 271.16
The Slint for .NET app with 5,000 lols
Slint for .NET, 820.71
The Flutter app with 5,000 lols
Flutter, 880.07

Flutter and Slint come out on top, but read it carefully. The originals post every label to the UI thread on its own, while the Flutter and Slint versions queue them and apply a frame's worth at once, which is the natural way to feed either one. And the one-millisecond sleep caps every app: 821 and 880 are mostly how long a millisecond's sleep really takes on this phone. In fact, they tie: in one later session, Slint ranged from 824 to 926 and Flutter from 727 to 879 over three runs each. Feeding Slint a frame's worth of labels at once, the trick that pays off in the canvas round below, changes nothing here, because the UI thread is waiting on the sleeps anyway.

The fastest drawing: a canvas

The original's canvas branch drops the views: a background thread queues 100,000 lols, and one custom view draws the latest 500 each frame. Flutter's fastest drawing works the same way, with a CustomPainter that draws the latest 500 each frame. Slint's canvas mode is its equivalent: a fixed set of 500 rows, whose properties change, so Slint never creates an element and just redraws.

AppWhat draws the lolsLOL/s
.NET for Android, canvas branchone View, Canvas.DrawText in OnDraw161,656
Flutter, canvas modeone CustomPainter158,000 to 170,000
Slint for .NET, canvas mode500 reused Text elements170,000 to 204,000
The .NET for Android canvas app with 100,000 lols
.NET for Android canvas, 161,656 (label covered)
The Slint for .NET canvas mode with 100,000 lols
Slint for .NET canvas, 169,693
The Flutter canvas mode with 100,000 lols
Flutter canvas, 153,266, from the first round

My first version of the Slint port lost this round, at about 95,000 per second, and measuring showed why. Flutter's canvas just keeps each lol in a Dart list and repaints once per frame, while my port told Slint about every single lol: 100,000 calls into the native side, which kept the UI thread busy for seconds. The fix was to do what a canvas does: store each lol in a small custom model, and tell Slint once per frame which of its 500 rows changed. That doubled the rate, and put Slint ahead of Flutter or level with it over several runs. It also made every model notification in the bindings allocation-free, a smaller win of about 3%. The original .NET canvas counts differently again: it counts a lol when the background thread queues it, before anything draws, while the Flutter and Slint versions count it when the UI thread has applied it. Either way, both draw at the screen's refresh rate, and what you see each frame is the latest 500 lols. As Jonathan wrote about his canvas branch, this measures how fast you can feed a renderer, not how fast it renders.

And the often-quoted Flutter figure of 12,255 per second? It comes from dopetest_flutter, a different test without any sleeps, which adds one widget per event-loop turn. Updated to today's Flutter, it averages 8,647 per second on this phone over 20 seconds, peaking around 10,750.

dopetest_flutter after 20 seconds
dopetest_flutter, 8,647 average

Try It on Your Phone

Numbers from one phone only go so far, so every version is in one place, updated to build with today's tools: the slint branch of my lols fork has the Java, .NET for Android, MAUI, and Flutter versions, and a script that builds them, runs each on your phone, and prints the rates. Point it at a checkout of the bindings to include Slint:

SLINT_REPO=~/src/slint scripts/compare-android.sh

If your numbers tell a different story, I'd like to hear it.

Tested Like the Other Languages

Slint has a shared suite of over 960 test cases in .slint files, which each language binding runs. The .NET driver generates C# for every case, builds them all into one runner, and runs each case in its own process. All of them pass on macOS, Linux, and Windows.

Cases can also carry C# test blocks that exercise the generated API directly, the way the Rust, C++, JavaScript, and Python blocks do. Writing the first ones paid off right away. They caught two bugs before any user did: struct field defaults that the generated records ignored, and integer conversions that rounded instead of truncating like Slint. Both are fixed in preview.4.

Today, 112 cases carry C# blocks, for models, focus, callbacks, globals, animations, and timers. They found two more gaps: .slint code couldn't add rows to a C# list model, and a global exported under two names got a class for only one of them. Both are fixed in preview.7.

What's New in the Previews

VersionHighlights
preview.1The first preview: generated C#, properties, callbacks, models, timers, and async code
preview.2Hot reload of .slint files
preview.3Window API, brushes and typed sizes, translations, model adapters, keys, styled text, and drag and drop
preview.4The latest Slint with editions, struct field defaults, saturating integers, a clear [STAThread] error, and live reload that survives removed members
preview.5Project files with <SlintProject>, so your app compiles with its Slint edition and paths, and faster incremental builds
preview.6C# versions of the examples and demos, events sent to a window, images from pixels and downloads, drag payloads, and saved shortcuts
preview.7Android with SlintActivity, the LOLs per second example, rows that .slint code adds to C# models, classes for global aliases, and a fix for .slint files outside the project on Windows
preview.8Saved and restored state on Android, the other activity events, and the latest Slint, with experimental features chosen by name
preview.9Drawing with OpenGL through rendering notifiers and borrowed textures, model ranges, and fewer native calls per struct
preview.10iOS, dotnet new templates for the desktop, Android, iOS, and all three in one solution, Image.FromPremultipliedRgba8 for SkiaSharp and Microcharts, and the OpenGL renderers on embedded Linux with display drivers without modifiers

The full list is in What's New.

What It's Not, Yet

Try It

If you build something with it, I'd love to hear about it. And if you've been waiting for a light, native, cross-platform UI toolkit for C#, give it an afternoon.

This is a personal post. Slint for .NET is a community project, not affiliated with or endorsed by SixtyFPS GmbH, the makers of Slint. Slint is a trademark of SixtyFPS GmbH.