Celeste.Wpf 0.2.0-alpha.1

This is a prerelease version of Celeste.Wpf.
dotnet add package Celeste.Wpf --version 0.2.0-alpha.1
                    
NuGet\Install-Package Celeste.Wpf -Version 0.2.0-alpha.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Celeste.Wpf" Version="0.2.0-alpha.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Celeste.Wpf" Version="0.2.0-alpha.1" />
                    
Directory.Packages.props
<PackageReference Include="Celeste.Wpf" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Celeste.Wpf --version 0.2.0-alpha.1
                    
#r "nuget: Celeste.Wpf, 0.2.0-alpha.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Celeste.Wpf@0.2.0-alpha.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Celeste.Wpf&version=0.2.0-alpha.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Celeste.Wpf&version=0.2.0-alpha.1&prerelease
                    
Install as a Cake Tool

Celeste

Celeste is a UI library for WPF applications that need to look current without adopting the Fluent or Material design language. It restyles the controls WPF already ships, adds the few it never did, and drives everything from one set of semantic color tokens that can be swapped at runtime.

The gallery's Display tab, cut down the middle: the light theme on the left, the dark theme on the right, showing badges, progress bars, card elevation, and a card with a footer

Both halves are the same screen. Every picture here comes out of the sample — ./eng/capture-screenshots.ps1 regenerates them.

What it gives you

  • Light and dark themes built from the same token names, switchable while the app runs. ThemeManager.Apply(ApplicationTheme.System) follows the Windows app theme and keeps following it.
  • Restyled built-in controls: Button, ToggleButton, TextBox, PasswordBox, CheckBox, RadioButton, ComboBox, ListBox, TabControl, Slider, ProgressBar, ScrollBar, ToolTip, Label, Separator.
  • Controls WPF does not have: Card, Badge, ToggleSwitch, ProgressRing, Avatar, an ImageView that loads and decodes pictures off the UI thread, a MasonryPanel that fills the shortest column, and a Sidebar that collapses to an icon rail or slides in over the content.
  • Button variantsCeleste.Button.Primary, .Secondary, .Destructive, .Outline, .Ghost, .Link — that all share one ControlTemplate. Defining a new variant means setting three brushes, not copying a template.
  • A type scale and layout tokens (spacing, radii, control heights) exposed as XAML resources, so your own controls can sit on the same grid as the library's.

Installation

Every release so far is a prerelease, and NuGet hides those unless you ask for them:

dotnet add package Celeste.Wpf --prerelease

Pin the exact version instead once you depend on it — 0.x releases may break the API, which is what the version number is telling you.

Then merge the two dictionaries into App.xaml. Order matters only for readability — control styles reference color tokens through DynamicResource.

<Application x:Class="MyApp.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:celeste="https://celeste-ui.dev/wpf"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <celeste:ThemesDictionary Theme="System" />
                <celeste:ControlsDictionary />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

Apply Celeste.Window to your window. Every control below it inherits the theme's font, foreground, and background:

<Window x:Class="MyApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:celeste="https://celeste-ui.dev/wpf"
        Style="{StaticResource Celeste.Window}"
        Title="My app" Width="600" Height="400">

    <celeste:Card Header="Workspace" Description="Everyone with the link can view this."
                  Margin="24">
        <StackPanel>
            <TextBox celeste:ControlHelper.PlaceholderText="Workspace name" />
            <celeste:ToggleSwitch Content="Public" Margin="0,12,0,0" />
        </StackPanel>
        <celeste:Card.Footer>
            <Button Content="Save" Style="{StaticResource Celeste.Button.Primary}"
                    HorizontalAlignment="Right" />
        </celeste:Card.Footer>
    </celeste:Card>
</Window>

Switching themes at runtime

using Celeste.Wpf.Theming;

ThemeManager.Apply(ApplicationTheme.Dark);    // pin dark
ThemeManager.Apply(ApplicationTheme.System);  // follow Windows, and keep following it

ThemeManager rewrites the ThemesDictionary you merged in App.xaml. Controls repaint immediately because every built-in style references color tokens through DynamicResource. If your own XAML uses StaticResource for a Celeste brush, it will keep the color it was given at load time.

Buttons

A plain <Button> is the secondary variant. The other five are keyed styles, so a button asks for its variant by name:

<Button Content="Save" Style="{StaticResource Celeste.Button.Primary}" />
<Button Content="Delete" Style="{StaticResource Celeste.Button.Destructive}" />

Celeste.Button.Small and Celeste.Button.Large change the metrics — font size, height, padding — and nothing else. WPF styles do not compose, so a large primary button is a style of your own: BasedOn the variant, then repeat those three setters.

For a row of buttons that is a single choice, use Celeste.ToggleButton.Segment. It is a RadioButton wearing the toggle-button appearance, so one option is checked per GroupName and clicking the checked one keeps it checked instead of leaving the choice empty. A ToggleButton is styled too, for a choice that stands on its own.

Text

Celeste defines no implicit TextBlock style — one would override the font a Button passes down to its own content. Font and foreground come from Celeste.Window by inheritance, and the scale is a set of keyed styles:

Style What it is for
Celeste.TextBlock.Body The base. Sets text rendering only, so it looks like inherited text.
Celeste.TextBlock.Muted Secondary text: the same size, the muted foreground.
Celeste.TextBlock.Caption Smaller and muted. Labels under a value, timestamps.
Celeste.TextBlock.Strong Body weight raised to SemiBold.
Celeste.TextBlock.Subtitle A heading inside a section.
Celeste.TextBlock.Title A page or dialog heading, in the display face.
Celeste.TextBlock.Display The largest step, for a number or a single line that is the point of the screen.
Celeste.TextBlock.Code Monospace, with Display formatting so stems stay on the pixel grid.
<TextBlock Text="Workspace" Style="{StaticResource Celeste.TextBlock.Title}" />
<TextBlock Text="Everyone with the link can view this." Style="{StaticResource Celeste.TextBlock.Muted}" />

Label and Separator are styled implicitly and need no key.

A sidebar with a workspace group holding Overview, Reports with a badge of 12, and Members, an account group holding Billing, a disabled Archived item, and Settings pinned to the bottom

Sidebar is a ListBox, so selection is an ordinary binding and Celeste never owns your navigation state. Setting IsCollapsed switches it to an icon rail; the binding runs both ways, so the built-in header chevron and your own layout rules can drive the same property.

<celeste:Sidebar Header="Acme" SelectedItem="{Binding CurrentPage}" IsCollapsed="{Binding IsRail}">
    <celeste:Sidebar.CollapsedHeader>
        <Path Data="M10,0 L20,10 L10,20 L0,10 Z" Fill="{DynamicResource Celeste.Brush.Primary}" />
    </celeste:Sidebar.CollapsedHeader>

    <celeste:SidebarGroupHeader Content="WORKSPACE" />
    <celeste:SidebarItem Content="Overview">
        <celeste:SidebarItem.Icon>
            <Path Data="M1,1 H7 V7 H1 Z M11,1 H17 V7 H11 Z M1,11 H7 V17 H1 Z M11,11 H17 V17 H11 Z"
                  Style="{StaticResource Celeste.SidebarItem.Icon}" />
        </celeste:SidebarItem.Icon>
    </celeste:SidebarItem>
    <celeste:SidebarItem Content="Reports" Badge="12" />
    <celeste:SidebarSeparator />
    <celeste:SidebarItem Content="Archived" IsEnabled="False" />

    <celeste:Sidebar.Footer>
        <celeste:SidebarItem Content="Settings" />
    </celeste:Sidebar.Footer>
</celeste:Sidebar>

Give every item an Icon if the sidebar can collapse: the rail hides labels and moves them into tooltips, so an item without an icon becomes an empty row. Celeste ships no icon set — Celeste.SidebarItem.Icon is a style that only makes your own Path follow the item's hover, selected, and disabled colors.

Header is hidden in the rail too, and CollapsedHeader is what replaces it — a logo mark rather than a name, since the rail is only CollapsedWidth wide. It does not fall back to Header: a wordmark scaled into 56 pixels is unreadable. If you supply neither a mark nor the toggle button, the rail drops the header band instead of leaving it blank.

Arrow keys move between items and step over SidebarGroupHeader and SidebarSeparator. The sidebar sets its own Width from ExpandedWidth and CollapsedWidth, so put it in an Auto grid column rather than sizing it yourself.

Off-canvas and responsive

SidebarHost decides how the pane is presented instead of leaving it to your layout. Auto reads the host's own width: wide is docked, narrower is the rail, narrow takes the pane out of the layout and slides it in over the content on demand.

<celeste:SidebarHost x:Name="Shell">
    <celeste:SidebarHost.Pane>
        <celeste:Sidebar Header="Acme" SelectedItem="{Binding CurrentPage}">
            <celeste:SidebarItem Content="Overview" />
        </celeste:Sidebar>
    </celeste:SidebarHost.Pane>

    <DockPanel>
        <Button DockPanel.Dock="Top"
                Command="{x:Static celeste:SidebarHost.TogglePaneCommand}"
                Content="Menu" />
        <ContentControl Content="{Binding CurrentPage}" />
    </DockPanel>
</celeste:SidebarHost>

Celeste never opens the pane on its own — it cannot know where your menu button belongs. Bind one to TogglePaneCommand, which routes, so the button has to sit inside the host. The command only executes while the pane is off-canvas, so the button disables itself once the pane is docked; bind Visibility to ActualDisplayMode if you would rather it disappear.

While the overlay is open, a scrim covers the content, Tab stays inside the pane, and Esc or a click on the scrim closes it and returns focus. Set DisplayMode to Docked, Rail, or OffCanvas to drive the presentation yourself; the breakpoints are then ignored. ActualDisplayMode always reports what you are actually looking at.

Pages and history

NavigationHost holds the page and remembers where you have been. It does not navigate: it watches its own Content, so binding that to a selection is the whole integration and Celeste never becomes your router.

<celeste:NavigationHost x:Name="Pages"
                        Content="{Binding SelectedItem, ElementName=Nav, Mode=TwoWay}"
                        ContentTemplate="{StaticResource PageTemplate}" />

<Button Command="NavigationCommands.BrowseBack" CommandTarget="{Binding ElementName=Pages}" Content="Back" />

Bind Content two ways when the pages come from a selection. Going back writes the previous page into Content, and a one-way binding would leave the sidebar highlighting a page the host is no longer showing.

Back is NavigationCommands.BrowseBack, not a command of Celeste's own, so it arrives with the gestures Windows already assigns. It routes, so put the button inside the host or point it at one with CommandTarget, as above. CanGoBack and BackStackDepth are bindable; GoBack() and ClearHistory() are there for code.

Each page is put back where you left it. Going forward is a fresh look and starts at the top — only Back restores a position. The stack is capped by MaxBackStackDepth (10 by default), because an unbounded stack is an unbounded reference to every page the application has ever shown. If your pages scroll themselves, set IsScrollEnabled="False" so they are not nested inside a second scrolling region; positions are then no longer restored, since the host is no longer what scrolls.

There is no forward stack and no transition between pages.

Pictures

ImageView takes a URI and does the rest: the file is read and decoded on a background thread, at the width the control was laid out at rather than at full resolution, and the result is shared with every other view showing the same picture at the same size.

<celeste:ImageView Source="https://example.com/cover.jpg" AspectRatio="1.5" />

It takes the width its layout gives it and derives its height from the aspect ratioAspectRatio when you set one, the picture's own once it is known, and a square until then. That is what lets it sit in a masonry column, where the width is the column and only the picture knows the height. For a picture that should be its own natural size, set Width and Height, or use a plain Image.

Setting AspectRatio removes the reflow that happens when the real ratio arrives, so it is worth setting whenever you know the shape in advance. Stretch defaults to UniformToFill, which crops rather than distorts.

State reports None, Loading, Loaded, or Failed, and is bindable. A failure also raises ImageFailed with the exception — the library never writes to a log of yours, so that event is the only place the reason exists. Placeholder is whatever should occupy the tile until the picture is there.

http, https, file, and pack URIs work, and a relative URI resolves against the element's base URI the way it does on Image — so Assets/cover.png behaves as you would expect. A resource compiled into an assembly is pack://application:,,,/Assembly;component/path.

Remote pictures go through Celeste.Wpf.Media.ImageLoader, whose HttpClient you can replace at startup with one that carries your handler, proxy, or authentication. Two limits are applied before any memory is committed to pixels: MaxSourceBytes (32 MiB) bounds the file, and MaxDecodedPixels (64 million, about 244 MiB of pixel data) bounds what the file expands into. They are different numbers for a reason — a few hundred kilobytes of compressed data can describe an image tens of thousands of pixels on a side.

Avatars

<celeste:Avatar Source="{Binding PhotoUri}" Initials="NS" Size="Large" />

The initials are the fallback for every case where there is no picture: none set, still loading, or a URI that failed. Celeste will not derive them from a name — which letters stand for a person depends on their language and their name order, and a two-letter rule that reads well in one script mangles another. Size is Small, Medium, or Large; setting Width and Height overrides all three.

Masonry

MasonryPanel puts each child into whichever column is shortest at the time, so tiles of unequal height leave no ragged gap.

<celeste:MasonryPanel MinColumnWidth="220" ColumnSpacing="12" RowSpacing="12">
    <celeste:ImageView Source="one.jpg" />
    <celeste:ImageView Source="two.jpg" />
    <celeste:Card Header="Any content">…</celeste:Card>
</celeste:MasonryPanel>

The column count comes from the panel's own width unless you set Columns: it fits as many MinColumnWidth columns as it can and shares what is left between them, so a resize reflows. Children are measured with an unconstrained height, so each has to decide its own — anything that stretches to fill instead measures to nothing.

Two things to know before using it. Shortest-column placement changes reading order: the third tile can end up beside the first rather than under it, which is the point of the layout and the reason not to use it for content that has to be read in sequence. And nothing here virtualizes: every child is measured and arranged on every pass, which is right for the tens of tiles a gallery view shows and wrong for thousands.

Theming it

Colors are semantic, not literal. Celeste.Brush.Primary is "the brand action color", and the light and dark dictionaries define it differently. To recolor the library, override the tokens after merging Celeste:

<ResourceDictionary.MergedDictionaries>
    <celeste:ThemesDictionary Theme="System" />
    <celeste:ControlsDictionary />
</ResourceDictionary.MergedDictionaries>

<SolidColorBrush x:Key="Celeste.Brush.Primary" Color="#FF7C3AED" />
<SolidColorBrush x:Key="Celeste.Brush.PrimaryHover" Color="#FF6D28D9" />

The full token list lives in src/Celeste.Wpf/Themes/Light.xaml. Every key defined there also exists in Dark.xaml; a test enforces that, because a key present in only one theme becomes a missing brush the moment a user toggles.

For per-control tweaks without retemplating, Celeste.Wpf.Controls.ControlHelper exposes attached properties: CornerRadius, PlaceholderText, and IconContent.

When Celeste is the wrong choice

  • You want Windows 11 Fluent. WPF-UI implements that design language properly, including Mica and the Fluent icon set. Celeste has its own look and will never match Windows chrome.
  • You want Material Design. Use MaterialDesignInXamlToolkit.
  • You need a data grid, docking, ribbons, or charts. Celeste has none of these and is not trying to become a control suite.
  • You are on .NET Framework. The package targets net8.0-windows and net10.0-windows only.

Status

Alpha. The token names and the public control API are still open to change; treat 0.x releases as breaking. What exists is covered by tests that apply every control's template in both themes on a real WPF dispatcher, so the styles load and measure — but they have not been through a wide range of real applications yet.

Not covered yet: Menu / ContextMenu, DataGrid, TreeView, Expander, Calendar / DatePicker, and dialogs. Those controls keep their default WPF appearance in a Celeste application, which is visible and jarring. Contributions in that direction are the most useful ones right now. Two areas are filled in: navigation — Sidebar, a SidebarHost that presents it docked, as a rail, or off-canvas, and a NavigationHost that holds pages and a back stack — and pictures, with ImageView, Avatar, and MasonryPanel.

ROADMAP.md has the order those gaps are being closed in, plus what is deliberately out of scope. CHANGELOG.md has what changed since the last published package.

Repository layout

Path Contents
src/Celeste.Wpf The library. Custom controls in Controls/, styles and tokens in Themes/.
samples/Celeste.Gallery A WPF app showing every control in both themes. dotnet run --project samples/Celeste.Gallery
tests/Celeste.Wpf.Tests Theme parity and template-application tests.

Building

dotnet build
dotnet test
dotnet run --project samples/Celeste.Gallery

Requires Windows and a .NET 10 SDK, 10.0.100 or newer — global.json asks for that version with rollForward: latestFeature, so any later release band works too. The library itself also builds for net8.0-windows.

Contributing

See CONTRIBUTING.md. New control styles need an entry in the gallery and a row in the template-application test; that is what keeps a style from silently breaking in one of the two themes.

License

MIT. See LICENSE.

Product Compatible and additional computed target framework versions.
.NET net8.0-windows7.0 is compatible.  net9.0-windows was computed.  net10.0-windows was computed.  net10.0-windows7.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0-windows7.0

    • No dependencies.
  • net8.0-windows7.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0-alpha.1 53 8/18/2026