Rethinking Adaptivity

TL;DR: The current adaptive widgets have significant problems and have all been replaced and deprecated. You may want to port your apps, the migration guide is here.

Over the past year, I’ve been working on replacing the old adaptive widgets in libadwaita, and as of a few days ago the last pieces have landed.

The Problems

(kgx:356880): Gtk-WARNING **: 16:38:18.039: Allocating size to AdwFlap 0x1e25c50 without calling gtk_widget_measure(). How does the code know the size to allocate?

(epiphany:91138): Gtk-WARNING **: 10:49:35.638: Allocating size to EphyWindow 0x15f8b70 without calling gtk_widget_measure(). How does the code know the size to allocate?

Ever seen an app print a warning like that on resize? If you’re unlucky, also flicker at the same time. This happens when a widget causes a resize during another resize. That’s one of the things that GTK prohibits, and yet almost every adaptive app currently does it, and most of the time it only works by accident.

Take AdwLeaflet, for example. People often use it for managing a sidebar. That’s fine, because leaflet will handle its own resize correctly. However, sidebar also implies other changes, like showing/hiding window controls and a back button. All of these changes cause resizes, and we do them when already inside a leaflet’s resize. Oops.

To an extent libadwaita widgets work around that, e.g. by emitting property notifications when they’re least likely to cause problems, but it’s never perfect. The simple truth is that this system is inherently fragile, depending on a number of things like whether the widget you’re changing is allocated before or after the leaflet (which is an implementation detail).


Another problem with the current approach is that while we provide premade widgets for specific cases, like a sidebar turning into stack navigation, any other kinds of adaptivity are basically unsupported.

For example, maybe your app has a header bar with a lot of widgets, and you want to move some of them into a separate bottom bar on mobile? There’s no premade widget for this, so the only way you can do it is by overriding the size_allocate() virtual function on some widget (typically the window) yourself and doing it there.

This is very finicky and easy to get wrong, and once again you’re setting yourself up for warnings and flicker — the Epiphany warning above is from doing exactly this, for example.

And it can’t even be abstracted as a widget either.

And really… a lot of apps need to do things besides what leaflet and friends provide. Even the libadwaita demo has to (ab)use GtkFlowBox for its style classes demo, while any adaptive app using AdwTabBar has to deal with size_allocate(). And the apps that don’t need tricks like that often have to compromise their design to make sure they don’t use anything unsupported. For example, at one point there was a GNOME Software design using a sidebar at desktop sizes and a view switcher at mobile sizes. That’s not possible with existing widgetry, so it never shipped.

Transition Animations

Widgets like AdwLeaflet provide an animation when switching between modes. This is cool, but also it causes the following bug when starting an app:

Notice the sidebar being briefly visible and then disappearing? This shouldn’t happen. And yet, it does and it’s not fixable.

Even when the transitions work, they are inconsistent. Sure, the leaflet itself animates, but the back button you add? It doesn’t. Even if you use a GtkRevealer to animate the button too, you’ll have to show/hide it at some point to avoid double spacing in the header bar if you have any other buttons next to it. And you can’t use a revealer for window buttons anyway.

Natural width

One thing the current approach (mostly) avoids is apps explicitly defining the size thresholds for layout changes. Instead, all of the existing adaptive widgets use natural widths of child widgets as implicit thresholds. While it avoids magic numbers, it doesn’t work very well in practice:

  • The widths can change on the fly. For example, this often happens in Settings, where you may very well have the sidebar collapse or reappear when you switch between different panels:

    Another example is Fractal, where a similar thing was happening because of long room topics. Because of that, AdwLeaflet actually defaults to using minimum widths as its threshold and not natural widths.

  • Natural widths are hard to control. Apps have no direct control over them, and often have to resort to tricks, like putting labels into GtkScrolledWindow with scrolling disabled to prevent natural width from propagating. (That’s what Fractal did to work around long room topics at first, for example)
  • If your application does more than just show/hide the sidebar — say, toggle the sidebar and also move a view switcher from the top of the window to the bottom, these changes will not happen at the same time. In fact, they may even happen in different order depending on the locale, font, etc., leading to hard to debug issues.
  • Since we’re adding and removing back buttons and window controls, the threshold actually changes between wide and narrow states. If an app does the opposite and removes widgets on narrow sizes, it can even lead to infinite loops:
    • The widgets are too wide, folding the leaflet.
    • Leaflet is now folded, removing buttons.
    • The widgets are narrow enough that they can fit side by side now, unfolding the leaflet.
    • The leaflet is unfolded, adding the buttons back.
    • The widgets are too wide, folding the leaflet.
    • … and so on.
  • Finally, there are cases where we always want narrow layout, even when the widgets fit otherwise. For example, on mobile we always want view switchers to be at the bottom, and yet there are quite a few cases where they would still fit at the top. And so, AdwViewSwitcherTitle has a hack where it monitors the window’s width and forcefully disables its view switcher when it’s below or equal to 360px, on top of artificially limiting its natural size to try and control the threshold.

While fixed thresholds present a different set of problems and are very much a tradeoff, at the end of the day you’re designing your layout to fit into a fixed resolution screen anyway.

All of that is already really bad, but the problems go even further.



Let’s look at AdwLeaflet. Most apps use this widget in one of the following two ways:

  1. The leaflet has three children: a sidebar, a separator and content. The separator is marked as non-navigatable. The leaflet has swipe to go back enabled, but not swipe to go forward. Sidebar and content both have header bars, with window buttons hidden in the middle when unfolded, and content has a back button when folded, that goes back to sidebar.
  2. The leaflet has the can-unfold property set to FALSE, and is used basically as a GtkStack with swipe gestures.

However, what is the actual purpose of this widget? Well, it acts as a GtkBox when unfolded and as a GtkStack when folded. As in, it has an unspecified number of children, shows all of them when unfolded and one of them when folded. That’s it.

For the first case we always want a separator. However, AdwLeaflet cannot automatically add it because it doesn’t know if it’s going to be used for a sidebar or not. There’s also no way to know what even is a sidebar: it’s just a leaflet child, so there’s no way to automatically style it. It can’t add a back button for you for the same reason, and so on.

Now let’s look at the second case. It works fine if you have 2 pages and just want to switch between them. You will still have to manage the back button on the second widget manually, but otherwise it’s okay — basically the same as GtkStack.

Now what if we want to have page 1 that leads to page 2 and page 3, both of which lead back to the page 1? Well, now we run into a problem.

If we simply add all three pages into the leaflet, going back from page 3 will lead to page 2 and not page 1.

There are two possible workarounds:

  1. Toggle the page visibility dynamically.
  2. Use a GtkStack with pages 2 and 3, and add that to the leaflet along with page 1.

Neither approach is particularly great.

So, AdwLeaflet is not very well suited for either case. Or more specifically, it tries to be low level and generic like GtkBox and such, while it’s primarily used for much more specific cases.


AdwFlap is not nearly as bad, as at least it has a fixed number of clearly defined children. But even then it can be used for multiple things that aren’t sidebars, like bottom sheets (even though I’m not aware of any apps actually using it as such now that Console switched to AdwTabOverview) or even for handling the window titlebar in fullscreen.

This means that it’s still too generic to be nice to use in any of these cases:

  • Just as with leaflet, you have to add a separator yourself. Many apps do it wrong and have it on the sidebar instead of a separate child.
  • The sidebar has no background because what if your use case needs it to be transparent?

And so on.

Or to summarize it: just like AdwLeaflet, flap is too generic. It tries to be a box/overlay widget and not, say, a sidebar widget.


AdwSqueezer is mostly used for handling view switchers in header bars. It contains multiple children and shows the largest one that can fit. So other than doing unsupported things for its most common use case, it works fine.

It still has a problem though. Since you put a view switcher and a title inside a squeezer, the header bar will center both at once. If the view switcher is too wide to fit, we show the window title, and since the whole widget is not centered at this point, the title will be off-center:

AdwHeaderBar has a centering-policy property to work around this, but it also introduces another problem:

What we actually want is this though:

But that cannot be accomplished when the view switcher and the title are in a single widget (squeezer’s natural width is that of its largest child — in this case, the view switcher — and if it doesn’t fit, the squeezer is still expanded to fit the remaining space, then the title is centered within that and not within the header bar), but we rely on that to make it adaptive. Not good.

OK, What Now?

So now that we’ve established that the existing widgetry has significant problems, what can we do differently?

Well, lets look at how other platforms handle it.

  • The Web has CSS and media queries. One can define a media query that matches a range of screen sizes (as well as many other things), and CSS rules inside its block would only apply within that range. Since CSS controls layout, people can rearrange their layout using media queries.

    Most web frameworks just use media queries or provide a similar API. This is often referred to as breakpoints.

  • Windows/UWP has AdaptiveTrigger and visual states. One can create a visual state that activates depending on the window size and toggle properties on the widgets in that window using setters.

  • Android has an elaborate resource system that allows for defining different layouts (UI files), dimensions or really any values only for specific screen size buckets or size ranges (as well as many other conditions), and when those conditions change (e.g. when the screen changes orientation), the toolkit just rebuilds the whole UI. Additionally, apps can factor out the common parts of their UI into different fragments, and then the activity (window) layouts can just arrange them in different ways, avoiding duplication.

    (This may be outdated though, I wasn’t able to find any information about how Android does adaptive layouts nowadays and my own knowledge is from Android 4.x, so about a decade old at this point)

  • UIKit uses a constraint layout, and constraints can be marked to only apply depending on screen orientation or size. While it avoids specific size values, instead preferring to target specific devices, iOS has the benefit of only running on a few select devices and can afford that. And before that system was introduced, apps could (and still can) just have different storyboards for different devices or screen sizes.

    Additionally, UIKit provides controllers like UISplitViewController that rearrange their layout automatically.

So… it’s all over the place, but let’s try to come up with something that combines the best parts of the above and hopefully avoid their pitfalls.


First, we’ll need a way to do arbitrary changes to the UI based on the window size, there’s no way around that.

So let’s implement a window that can do arbitrary layout changes for its contents.

That’s hard, but not impossible. It involves temporarily hiding the contents, applying changes and showing it again on the next frame. To avoid flicker, a snapshot of the child from the last frame can be shown while it’s hidden. It also involves a special case for when mapping the child for the first time.

A hard problem here is sizing. Widgets like leaflet know what their layout will be when it’s folded and unfolded (as long as apps don’t add random back buttons or do other changes, anyway) and can expose correct minimum and natural sizes regardless of their current state. This is important because otherwise you wouldn’t be able to resize them to smaller sizes in the first place.

But we’re talking about arbitrary changes, so there’s no way to predict the size for our window unless it’s already at the smallest possible state. So, the only thing we can do is to disable the minimum size entirely and require apps to specify it, and clip the child if it doesn’t actually fit (or alternatively scale it down).

Another problem is transitions. Since we don’t know what exactly will change between different states, there’s no way to animate it beyond a global crossfade. But considering transitions were already inconsistent, I don’t think it’s a big loss.

And since libadwaita already has AdwWindow and AdwApplicationWindow, we can just reuse them and have all of this logic in there.

Now that we have this functionality we need to figure out good API for control it.

CSS in GTK doesn’t support media queries. And even if it did, they wouldn’t be useful, since CSS in GTK doesn’t control layout beyond widget margins/paddings/borders, minimum widths/heights and sometimes spacing. The overall concept is still attractive though, but we can’t use CSS for this.

However, we can still use UI files, like the UWP system does. What if our widget was defining “visual states” with a size-based condition and property setters? Following the web, let’s call them breakpoints.

GtkBuilder supports custom tags, so we can actually replicate this system in its entirety.

It looks like this:

<object class="AdwWindow">
  <property name="width-request">360</property>
  <property name="height-request">200</property>
  <property name="child">
    <!-- ... -->
    <object class="AdwBreakpoint">
      <condition>max-width: 500px</condition>
      <setter object="my_object" property="some-property">value</setter>
      <setter object="my_object" property="some-property">value</setter>
      <!-- ... -->
      <setter object="my_object" property="some-property">value</setter>

Pretty close, isn’t it? Of course, only being able to change properties is rather limiting, so each breakpoint also has apply and unapply signals.

Now we need to figure out how to deal with child overflow. In an ideal world, apps will just pick the widths where it doesn’t happen, but in practice that won’t work. Even if the widgets fit in English locale, it may not necessarily fit in German. Or with Large Text enabled.

This process isn’t really any different from just adapting an app for small screen sizes. You need it to fit into 360 pixels horizontally, at any cost. So, if you have a long label and you can’t make it shorter or rearrange your UI, you wrap it, ellipsize it or use a smaller font or letter spacing.

One addition on GTK side here is the GtkButton:can-shrink property (as well as GtkMenuButton:can-shrink). Buttons are the main source of labels outside of apps’ control, and now there’s a an easy way to deal with them.

And to deal with different text scales, we can introduce support for units other than pixels to breakpoints.

Currently three units are supported: pixels, points and scale-independent pixels. The last unit is lifted directly from Android and is basically points, but normalized to match pixels at default text scale. See the docs for the exact values.

Finally, in some cases it can be nice to be able to use breakpoints on a more localized area of the UI, for example, a specific page. And so, all of this machinery is also available as a standalone widget: AdwBreakpointBin.

Replacing Squeezer and AdwViewSwitcherTitle

Next, we need to figure out how to adapt existing patterns for this new system.

Some of them, like bottom toolbars or hiding tabs on mobile are doable as is and don’t need any additional support — you can simply toggle their visibility from your breakpoint.

And that includes view switchers: simply add an AdwViewSwitcher into your header bar, a hidden AdwViewSwitcherBar at the bottom of the window, and a breakpoint that removes the view switcher and shows the view switcher bar on narrow sizes.

One problem is that unfortunately AdwViewSwitcher defaults to the narrow policy, while for header bar you really want wide instead, so apps have to change it manually.

The new system also avoids the misaligned title problem, since in narrow state we remove the view switcher entirely and let header bar use its default title, so the view switcher’s width doesnt’t get in the way.

This was the only place libadwaita was using AdwSqueezer for, so we don’t need that widget anymore either.

Replacing Leaflet

AdwLeaflet, on the other hand, cannot be replicated with breakpoints alone and needs a replacement widget. For example, because it provides things like gestures for stack navigation, unlike GtkStack and similar widgets.

Rethinking leaflets has led to a surprising number of other improvements along the way, so let’s run through them.

Toolbar View

Long ago, before libadwaita 1.0, there was this mockup:

Unfortunately, at the time it wasn’t really possible to implement it, not least because of leaflet limitations. However, if we’re redoing it anyway, might as well implement that style, especially when newer apps like Amberol and Emblem use a similar style anyway.

But the flat headers present a problem. What about scrolling content? This has been a long-standing problem with flat header bars, and in fact the .flat style class for header bars has been deprecated along with the old widgets.

Thankfully, GtkScrolledWindow has a mechanism for displaying undershoot indicators, and libadwaita has new style classes for controlling them.

However, manually managing them can be tricky. Sure, it’s easy when you just have a header bar and add the .undershoot-top style class to scrolled window, but what about bottom bar in an app like Contacts? Manually managing .undershoot-bottom in sync with the reveal animation is tricky, and in fact not even possible if you’re using GtkActionBar without wrapping it into another GtkRevealer.

But what if we had a widget for managing it instead? And so that’s what AdwToolbarView does. It replaces GtkBox for cases like this, providing a slot for content, and allowing for adding one or multiple top and bottom bars. Then, it manages the undershoot styles on scrolled windows inside it.

However, having a widget just for that would be silly, so it also does a number of other things:

  • It allows for visually merging multiple toolbars together and remove their border and vertical spacing, making it possible to implement yet another mockup that wasn’t possible before:

  • For the raised style (like on the screenshot above) it makes it possible to have a shadow instead of a border. This is yet another thing that wasn’t possible before, because while GtkWindow’s titlebar is special cased to be drawn before content, that’s not the case in GtkBox, and the shadow would end up behind content. In AdwToolbarView we can make sure the content is drawn last and avoid this problem.
  • It automatically makes all toolbars draggable, including GtkSearchBar etc.
  • It allows extending the content behind top or bottom bars, and to reveal or hide toolbars with an animation. This means that it can be used instead of AdwFlap to manage fullscreen titlebar, and we don’t need to worry about that use case when replacing flap anymore.

Header bars are now also white in light variant instead of darker grey — mostly so that there’s more contrast against their shadow and it looks better with utilty panes using the new styling. The grey color header bars were previously using is now reserved for sidebars and utility panes instead.

Navigation View

The first of the leaflet replacements is AdwNavigationView.

One of the things I wanted to avoid was having a single widget for both sidebars and stack navigation. This is a large part of what made AdwLeaflet so cumbersome, and it was this way to avoid code duplication, as well as due to historical reasons: leaflet first came to exist back when everyone just used GtkStack for this, and provided a version that can also do sidebars, then gradually got improved to be more attractive than GtkStack even for stack navigation.

In fact, libhandy 1.x provides a variant of leaflet that never expands, called HdyDeck. While in theory it was a good idea to separate these two widgets, they had almost identical API, so it still didn’t make any sense. As such, for libadwaita I merged them back and essentially replaced deck with the can-unfold property.

So, as shown earlier, leaflet API is not well suited for this task. So, once again, let’s look at other platforms for inspiration.

Some of them don’t have any nice ways to handle this pattern — for example, in UWP it’s surprisingly involved. Others do, however, such as UIKit and its UINavigationController. In fact, let’s go ahead and use it as inspiration because it’s very well thought out.

A noticeable difference between UINavigationController and AdwLeaflet is that the former has a stack-like API. As in, stack the data structure, not GtkStack (wow, this is confusing, not helped by the fact UIKit calls a stack what GTK calls a box 😵‍💫). So, you have a navigation stack, push pages into it and pop pages off it, and it’s dynamic. For comparison, leaflet has a statically ordered list of children, and you move back and forward in that list.

However, we can’t make AdwNavigationView 100% dynamic: we still need some way to use it from UI files. It took a lot of iteration, but at the end I came up with the following scheme:

  1. Pages can be pushed directly into the navigation stack using push(). Once they are popped, they will be automatically removed.
  2. Pages can be statically added using add() or from UI files. Then, they can be pushed and popped and will stay in the widget and can be reused later. The first added page becomes the root of the navigation stack.
  3. If a page was pushed without having been added and then later add() is called for it, it won’t be autoremoved.
  4. If remove() is called for a page currently in the navigation stack, it won’t be automatically removed and is simply marked for removal, same as if add() had never been called.
  5. The whole navigation stack can be replaced using replace(), including removing any pages currently in the navigation stack if they are marked for autoremoval.

In other words, add() and remove() act a bit like pinning and unpinning the pages. The navigation stack itself is fully dynamic.

Pages also have tags and all of the functions for managing navigation stack have versions that work with tags, and pushing pages this way requires adding them first.

Tags also allow for another thing: GActions for managing the navigation stack. AdwNavigationView exposes two actions: navigation.push and navigation.pop. The first one takes a page tag as parameter, and this allows defining fairly complex structures, such as the one found in AdwAboutWindow entirely from UI files, without any code.

Unlike AdwLeafletPage, navigation view children — AdwNavigationPage — are widgets instead of auxiliary objects. Not only that, they are also derivable, so it’s possible to use templates with them. That’s why the tags are called tags and not names, otherwise there’s a name clash with GtkWidget:name.

Historically, GNOME apps have managed their back buttons themselves, no matter whether they are using GtkStack, HdyDeck or AdwLeaflet. But isn’t that silly? AdwLeaflet manages shortcuts and gestures for going back to the previous page, but not back buttons? Meanwhile UINavigationController automatically provides back buttons, so why can’t we?

Well, no reason really. While our situation is a bit different since header bars are just a widget instead of a part of the navigation controller, header bars already automatically show window controls for the window they are in. And so… AdwHeaderBar can now also automatically show a back button if it’s inside an AdwNavigationView. This is opt-out, same as for window buttons.

AdwHeaderBar has also learned to pull titles out of navigation pages same as how it does with window titles, since changing titles on header bars is a bit painful in GTK4, even with AdwWindowTitle. And since we have titles, they also double as accessible labels, as well as tooltips for back buttons. In fact, if we wanted to add labels to back buttons, like in iOS or elementary OS, we have means to do so now as well.

AdwNavigationView also supports a way to go forward, unlike UINavigationController — using the get-next-page signal. Meanwhile, AdwNavigationPage has signals for tracking its visibility, which can be used to create and destroy its contents on demand. With this apps like Nautilus can hopefully use this widget for content navigation, while with leaflet it just wasn’t feasible.

Finally, while AdwNavigationView hopefully reduces the number of situations where people have to nest them, it still works with nesting, including actions and back button integration. This will come in handy for the next widget.

Navigation Split View

And finally, now that the navigation case is well covered and flat header bars are doable in a robust way, we can reimplement sidebars with a widget called AdwNavigationSplitView.

Compared to AdwNavigationView, this widget is very simple. It has two children: sidebar and content, both of which are required to be AdwNavigationPage. By default, it shows them side by side. When the collapsed property is set to TRUE, it just reparents both into a navigation view. And that’s pretty much it… Add a breakpoint to your window, toggle the collapsed property from it and you have an adaptive sidebar.

Well OK, it does a few more things, but this is still a stark contrast with how complex AdwLeaflet was.

For example, AdwHeaderBar gains yet another feature. When used inside AdwNavigationSplitView, it automatically hides redundant window buttons, hopefully getting rid of bugs like this for good:

Actually, two features. The other one is a property for hiding the title — it’s common to have header bars without titles for split layouts, and having to put an invisible widget into the header bar just to get rid of its title is not ideal.

AdwNavigationSplitView also provides its own navigation.push and navigation.pop actions, so that they still work when not collapsed. They work exactly same as in AdwNavigationView.

Instead of inheriting the push/pop API of AdwNavigationView, split view provides a single show-content boolean property.

Since the sidebar child is now clearly defined, we can style it and implement that mockup from long ago, and if people use toolbar views, they get automatic undershoot shadows.

Moreover, there’s a special styling for triple pane layouts:

This can be achieved just by nesting two split views together, see the docs for more details.

Finally, it implements dynamic sizing for sidebars. This is probably the most complex part of this widget: instead of using the sidebar’s natural width it attempts to give the sidebar a specified fraction of the total width, which is additionally limited by a minimum and a maximum value (as well as the minimum width of the sidebar child). On top of that, the minimum and maximum widths can use the same units as breakpoints, and in fact AdwNavigationSplitView defaults to using the sp unit, so that sidebars scale with Large Text.

Replacing Flap

Unlike leaflet, AdwFlap doesn’t need a ground-up overhaul. We’ve already covered the fullscreen toolbar use case with AdwToolbarView, and flap is basically never used for bottom sheets — and even if it were, we’re going to have a specialized bottom sheet widget later anyway. So, the only case we need to care about is overlay sidebars and utility panes (which are basically the same thing as sidebars, but with a single header bar, so the widget doesn’t need to care about this distinction).

So, AdwOverlaySplitView is actually heavily based on AdwFlap, trimming it down and simplifying it API to match AdwNavigationSplitView. It also enjoys the same styling, dynamic sizing and AdwHeaderBar window button integration:

Yes, it looks exactly same as AdwNavigationSplitView when it’s not collapsed. What did you expect? Unlike that widget though, it does allow moving the sidebar to the right, as well as showing and hiding it, same as flap. (AdwNavigationSplitView doesn’t allow either because it would break the navigation model)

Unlike AdwNavigationSplitView, it doesn’t require children to be AdwNavigationPage, though it doesn’t prevent it either — it will work fine and you may still want to do that because of header bar title integration.

It also removes touchpad swipes. While it still has swipes on touchscreen, combining edge swipes on touchscreen with swipes anywhere on touchpad has never worked well and it has caused no end of problems for apps like Loupe. With touchpad swipes being partially broken in GTK4, it’s just not worth it. AdwNavigationView still supports touchpad swipes, but this widget does not.


The old widgets have been deprecated and apps using libadwaita from main are encouraged to migrate to the new widgetry. Libadwaita also provides a migration guide for doing so.

The adaptive layouts page also uses the new widgets now.


Dynamic Layouts

Even with all of that, there’s always room for improvement.

For example, while breakpoints allow rearranging the UI by setting properties, it’s still fairly limited and one needs widgets like split views for more complex layout changes. And while anything is possible with enough work, doing complex layout changes is rather painful (though less so than with the old widgets) and it has to be done programmatically.

However, we can take a bit of inspiration from Android, with its resource system and rebuilding the UI to change screen orientation etc.

As such, another thing I’ve been prototyping is a yet-to-be-named widget that does a similar thing to what AdwNavigationSplitView is doing, but generically:

  • You provide it multiple different UI files, each of them contains tagged slots.
  • You also provide it children with matching tags.
  • It creates UI from one of the UI files and inserts the children into those slots.
  • Which layout it uses is controlled by a property.

When you change its value, it unparents the tagged children, destroys the rest of the layout, creates the other layout and reparents the children there.

To better explain it, let’s take a look at this mockup of Loupe:

Here properties are presented in a sidebar at desktop sizes, and in a bottom sheet at mobile sizes. We obviously don’t have a widget that transforms between a sidebar and a bottom sheet (nor do we have a bottom sheet widget, but we will in future, so let’s assume we do), so there are two ways of implementing it:

  • Have both a sidebar and a bottom sheet, each containing a copy of the properties view, and only have one of them visible at a time. In case of Loupe, it will work fine, since the properties view is reasonably small, but it doesn’t scale in the general case.
  • Reparent them manually. This works, but is pretty fiddly and can’t be done declaratively.

With this new widget though? Define two layouts: one with a split view with two slots as its content and sidebar children, the other one a bottom sheet view with the same two slots. Add two children: an image view and a properties view, matching those slots. Then toggle the layout from your breakpoint. That’s it.

One downside of how Android does it is that apps have to thoroughly save widget state before rebuilding the UI and restore it afterwards, or it will be lost. But, since we’re reusing both widgets, there’s no need to save state here, it will just work.

We can still go a step further and do even that declaratively for state not covered by the children within the slots: have a list of object/property/tag triplets in each layout, and map each of them to the matching object/property on the other layout.

There are a few obstacles to solve here, but I have a working prototype, and maybe can even finish it this cycle.


Speaking of bottom sheets, one thing that’s still missing from libadwaita is support for adaptive dialogs. That’s coming too, but probably not this cycle. It needs cooperation with the compositor — especially for portals, where the dialog and its parent window are in different processes and can’t communicate. But we have a design and a tentative plan for how to get there.

I’d like to say thanks to:

  • Nahuel for pointing me towards UWP AdaptiveTrigger and answering UWP questions.
  • Chris for porting a lot of apps to try out and test the new API.
  • Tobias Bernard and Sam Hewitt for UI design and answering my countless questions about tiny and unimportant details.

As always, thanks to my employer, Purism, for letting me work on all of this.

Libadwaita 1.3

Another cycle, another release. Let’s take a look at what’s new.


Screenshot of AdwBanner

AdwBanner is a brand new widget that replaces GtkInfoBar.

Jamie started implementing it before 1.2 was released, but we were already in the API freeze so it was delayed to this cycle instead.

While it looks more or less the same as GtkInfoBar, it’s not a direct replacement. AdwBanner has a title and optionally one button. That’s it. It does not have a close button, it cannot have multiple buttons or arbitrary children. In exchange, it’s easier to use, behaves consistently and has an adaptive layout:

Wide screenshot of AdwBanner. The button is on the right, the title is centered.

Medium-width screenshot of AdwBanner. The button is on the right, the title is left-aligned as it wouldn't fit centered.

Narrow screenshot of AdwBanner. The title is center-aligned, the button is centered below it.

Like GtkInfoBar, AdwBanner has a built-in revealer and can be shown and hidden with an animation.

There are situations where it cannot be used, but in most of those cases GtkInfobar was already the wrong choice and they should be redesigned. For example, Epiphany was using them for the “save password” prompt, and is using a popover for that now.

Tab Overview

A work-in-progress grid-based tab overview A work-in-progress carousel-based tab overview

AdwTabOverview is a new tab overview widget for AdwTabView that finally makes it possible to use tabs on mobile devices without implementing a mobile switcher manually.

Back when I wrote HdyTabView, I mentioned a tab overview widget in the works, and even had demo screenshots. Of course, basically everything from that demo got rewritten since then, and the carousel for narrow mode got scrapped completely, but now we’re getting into The Ship of Theseus territory.

This required a pretty big rework of AdwTabView to allow tabs to have thumbnails when they are not visible, and in particular it does not use a GtkStack internally anymore.

By default the selected tab has a live thumbnail and other thumbnails are static, but apps can opt into using live thumbnails for specific pages. They can also control the thumbnail alignment in case the thumbnail gets clipped. Thumbnails themselves are currently not public, but it might be interesting to use them for e.g. tooltips at some point.

Overview is not currently used very widely – it’s available in Console, and there is a merge request adding it to Epiphany, but I didn’t have the energy to finish the latter this cycle.

Tab Button

Screenshot of AdwTabButton. It's showing two tab buttons. One displays 3 open tabs, the other one 15 and has an attention indicator

AdwTabButton is much less interesting, it’s just a button that shows the number of open tabs in a given AdwTabView. It’s intended to be used as the button that opens the tab overview on mobile devices.

Unlike tab overview, this widget is more or less a direct port of what Epiphany has been using since 3.34. It does have one new feature though – it can display an indicator if a tab needs attention.


You might have noticed that widgets like AdwViewStack, AdwTabView or AdwEntryRow were not accessible in libadwaita 1.2.x. The reason for that is that GTK didn’t provide public API to make that possible. While GtkAccessible existed, it wasn’t possible to implement it outside GTK itself.

This cycle, Lukáš Tyrychtr has implemented the missing pieces, as well as fixed a few other issues. And so libadwaita widgets are now properly accessible.

Animation Additions

One of AdwAnimation features is that it automatically follows the system setting for disabling animations. While this is the expected behavior in most cases, there are a few where it gets in the way instead.

One of these cases is in apps where the animations are the app’s primary content, such as Elastic, and so I added a property that allows a specific animation to ignore the setting.

The animation in the demo is now using it as well, for the same reason as Elastic, and in the future it will allow us to have working spinners while animations are disabled system-wide as well.

A screenshot of an animation graph from Elastic

Elastic also draws an animation graph before running the actual animation, and while for timed animations it’s relatively easy since easing functions are available through the public API, it wasn’t possible for spring animations. It is now, via calculate_value() and calculate_velocity().

All of this uncovered a few bugs with spring animations – for example, the velocity was completely wrong for overdamped springs, and Manuel Genovés fixed them.

Unrelated to the above, a common complaint about AdwPropertyAnimationTarget was that it prints a critical if the object containing the property it’s animating is finalized before the target. While it was easy to avoid in C, it was nearly impossible from bindings. And so we don’t print that critical anymore.

Other Changes

  • Christopher Davis added a way to make AdwActionRow subtitle selectable.
  • Matt Jakeman added title-lines and subtitle-lines properties to AdwExpanderRow, matching AdwActionRow.
  • AdwEntryRow now has a grab_focus_without_selecting() method, matching GtkEntry.
  • The API to set an icon on AdwActionRow and AdwExpanderRow are now deprecated, since they were mostly unused. Apps that need icons can add a GtkImage as a prefix widget instead.
  • AdwMessageDialog now has the async choose() method, matching the new GTK dialogs like GtkAlertDialog. The response signal is still there and is not deprecated, but in some cases the new method may be more convenient, particularly from bindings:

    private async void clicked_cb () {
        var dialog = new Adw.MessageDialog (
            "Replace File?",
            "A file named “example.png” already exists. Do you want to replace it?"
        dialog.add_response ("cancel", "_Cancel");
        dialog.add_response ("replace", "_Replace");
        dialog.set_response_appearance (
        var response = yield dialog.choose (null);
        if (response == "replace") {
            // handle replacing
  • Corey Berla added missing drag-n-drop related API to AdwTabBar to make it work properly in Nautilus.
  • Since GTK now allows to change texture filtering, AdwAvatar properly scales custom images, so they don’t appear pixelated when downscaled or blurry when upscaled. This only works if the custom image is a GdkTexture – if your app is using a different GdkPaintable, you will need to do the equivalent change yourself.
  • Jason Francis implemented dark style and high contrast support when running on Windows.
  • Selected items in lists and grids are now using accent color instead of grey, same as Nautilus in 43. Sidebars and menus still look the same as before.

New Dependencies

The accessibility additions, scaled texture render nodes in AdwAvatar, as well as mask render nodes (that I didn’t mention because it’s an internal change) and deprecation fixes mean that libadwaita 1.3 requires GTK 4.10 instead of 4.6.

As always, thanks to all the contributors, and thanks to my employer, Purism, for letting me work on libadwaita and GTK to make this release happen.

Introducing Elastic

Elastic is a new spring animation editor app.

Screenshot of Elastic

Ever since 1.0, libadwaita has had spring animations. These animations aren’t controlled with a duration and an easing function, but instead with physical properties: damping ratio (or optionally damping), mass, stiffness and initial velocity, as well as epsilon. While this allows for a lot of control over the animation, it can be pretty hard to understand if you’re not familiar with physics involved, and to be truly useful it needs an editor.

So, Elastic is that editor. It provides a way to tweak each parameter, explains what they do, allows to preview the animation in various ways, and generates the code to create that animation.

Screenshot of the animation graph in Elastic


This app has a pretty convoluted history.

At one point of time, before 1.0, me and Tobias wanted to rework libadwaita demo and Tobias made new mockups for it. At about the same time, I was experimenting to see how feasible spring animations would be and needed some playground. Since it was understood that this will eventually be in libadwaita, Tobias made this design a part of the new demo:

Spring animation demo mockups

So I implemented it about as closely as I could at the time, but since then a few things happened:

  • We decided that the new demo was too design-focused to work well as, well, a demo (one of the purposes of which is using it as a test case for libadwaita itself). So, instead, we decided to make the new design a separate app called Patterns. That app is stalled for other reasons as it needs features that libadwaita doesn’t provide at the moment. Hopefully, next cycle we should have everything needed to finish it.
  • We decided that the spring animation page was too involved for a demo and was better off separately. So I split that code out and ported it to Vala instead of C.
  • As part of his GSoC project, Manuel Genovés implemented proper spring animations instead of a hacky prototype, and I ported the editor to that.

And then… everyone kinda forgot about it – Patterns was still blocked on other things, Manuel implemented a simpler animation demo for libadwaita and this one was left out.

Fast forward two years, Tobias was reviewing an app in Circle and needed a way to play with spring animations. We realized we never did anything with the editor, so I turned it into a proper app, fixed the remaining bugs and made it generate code instead of just previewing animations. We named it Spring Editor (imaginative, I know) and my impression was that design tools were covered by the new app inclusion process, so I submitted it to incubator.

It turned out more complicated than expected, so I decided to ignore it and try and get it into GNOME Circle instead. So now the Spring Editor name is too generic, so the app got renamed again, this time to Elastic (thanks Christopher Davis for that name) and it’s on Flathub now.

Anyway, the app is finally out, it’s on Flathub, so have fun. :)

Download on Flathub

UPDATE: It’s in Circle now.

Libadwaita 1.2

So, half a year after 1.1, libadwaita 1.2 has been released.

While it doesn’t contain everything I had planned (since I ended up being mostly unavailable for about half of the cycle for reasons outside my control), it still has a bunch of additions, so let’s take a look at the changes.

Entry Rows

First, we have a widget that was planned for 1.0, but didn’t make it because it still needed work. This cycle I took time to finish and land it.

Originally implemented by Maximiliano, AdwEntryRow is a new type of boxed list rows, containing an inline entry. Its title doubles as the entry placeholder.

Entry rows in Libadwaita 1.2 demo

Entry rows can require confirmation via an apply button shown when its contents are edited. Otherwise, it’s similar to GtkEntry API, and can have prefix and suffix widgets like AdwActionRow.

There is also a companion widget AdwPasswordEntryRow, mirroring GtkPasswordEntry.

Message Dialogs

AdwMessageDialog with heading: Save Changes?, body text: Open document contains unsaved changes. Changes which are not saved will be permanently lost., and buttons: Cancel, Discard, Save. The Discard button has destructive appearance, the Save button has suggested appearance

AdwMessageDialog is a new widget that replaces GtkMessageDialog. On the first glance, it looks more or less the same. However, it has a few important differences.

Adaptive Layout

GtkMessageDialog tends to overflow off the screen very easily. AdwMessageDialog doesn’t because it’s fully adaptive: when scaling down the window, it first restricts its own size to the parent’s size:

AdwMessageDialog with heading: Save Changes?, body text: Open document contains unsaved changes. Changes which are not saved will be permanently lost., and buttons: Cancel, Discard, Save. The Discard button has destructive appearance, the Save button has suggested appearance. When the window is shrunk down to mobile size, it becomes narrower

And if that’s not enough to fit, it arranges its buttons vertically:

AdwMessageDialog with heading: Save Changes?, body text: Open document contains unsaved changes. Changes which are not saved will be permanently lost., and buttons: Cancel, Close without Saving, Save As…. The Close without Saving button has destructive appearance, the Save As… button has suggested appearance. When the window is shrunk down to mobile size, it rearranges buttons vertically

Additionally, it always uses the vertical arrangement if the dialog would end up ridiculously wide otherwise, even when there is enough space to display it like that.


The second important thing is that it’s not a GtkDialog subclass and, as such, has completely new API.

  • GtkMessageDialog inherits the response API from GtkDialog. It relies on integer IDs that are provided by applications, except when they aren’t – the GtkResponseType enum provides a few predefined responses. This is a very C-centric API (as it assumes enum values being interchangeable with integers), and it’s not overly convenient to use. AdwMessageDialog replaces it with simple string IDs that are always provided by applications.

    This also allowed the response signal to be detailed, using the response ID as the detail.

    In addition, AdwMessageDialog is automatically closed on response, and doesn’t need to be closed manually. As such, common code like this:

    static void
    dialog_response_cb (GtkDialog       *dialog,
                        GtkResponseType  response,
                        MyObject        *self)
      if (response == GTK_RESPONSE_ACCEPT)
        my_object_foo (self);
      gtk_window_destroy (GTK_WINDOW (dialog);
    g_signal_connect (dialog, "response",
                      G_CALLBACK (dialog_response_cb), self);

    can be replaced with just:

    g_signal_connect_swapped (dialog, "response::accept",
                              G_CALLBACK (my_object_foo), self);
  • Just as AdwMessageDialog doesn’t have predefined responses and leaves that to apps, it doesn’t use GtkButtonsType. It seems counterproductive to have API to easily create buttons like “OK”, “Cancel”, “Yes” and “No” when we’ve been discouraging labels like this for just about two decades now, doesn’t it?
  • No direct widget access. GtkMessageDialog required apps to poke into its widgetry to pack additional widgets, add style classes to its buttons and so on. AdwMessageDialog can have a single extra child, displayed below the body text and managed with the extra-child property, and natively supports changing button appearance instead.

    AdwMessageDialog from GNOME Text Editor, with heading: Save Changes?, body text: Open document contains unsaved changes. Changes which are not saved will be permanently lost., and buttons: Cancel, Discard, Save. The Discard button has destructive appearance, the Save button has suggested appearance. It has an additional child under the body text - a boxed list with one unsaved file called "123"

  • While the intention of GtkMessageDialog is to use its primary and secondary text, it also displays the window title, and many applications get it wrong, ending up with three labels or wrong styling:

    GtkMessageDialog from LibreOffice Writer. It has three labels: title: Save Document? primary text: Save changes to document "Untitled 1" before closing? secondary text: Your changes will be lost if you don't save them.

    GtkMessageDialog from Rnote. It has title: Quit Application, and primary text: Any unsaved changes will be lost. Do you want to quit anyways?, but no secondary text, so both labels appear wrong.

    Primary text also changes its styles to look like a heading or body text, depending on whether secondary text is set.

    AdwMessageDialog does not do any of that. It just has heading and body, that’s it.

  • Finally, it’s derivable. While making GtkMessageDialog final in GTK4 was consistent with most of the other GTK widgets, it has generally resulted in vastly more convoluted code for apps with complex dialogs – for example, the save dialog in GNOME Text Editor with its list of open files.

So now app developers have no excuses for shipping unpolished message dialogs anymore. 😉️

About Window

Another widget originally planned for 1.0 that didn’t make it was Adrien’s AdwAboutWindow, replacing GtkAboutDialog. This cycle I finished it up and merged it.

The new window is adaptive and natively provides a lot of details people were doing hacks with GtkAboutDialog for before:

  • Legal information for dependencies and other components;

    A screenshot of legal information from GNOME Maps, listing information for map data provider, map tile provider and search provider

  • A list of acknowledgements in addition to credits — for example, for crowdfunding backers;
  • A place to list recent changes in the application;
  • Links to the application’s website, issue tracker, support forum, as well as any additional links, as opposed to a single link in GtkAboutDialog;
  • Debugging information, along with easy ways to save or copy it.

    Screenshot of debugging information from GNOME Text Editor's AdwAboutWindow

Christopher Davis has ported core apps using libadwaita to AdwAboutWindow, so it’s also pretty consistently used by now.

Tab View and Tab Bar Additions

AdwTabView and AdwTabBar have seen quite a few changes this cycle.

The most important change is with shortcut handling. AdwTabView provides a lot of shortcuts for switching and reordering tabs, for example, Ctrl+Tab to switch to the next tab, or Alt+1Alt+0 to switch to the first 10 tabs.

In Libhandy, these shortcuts were added via the shortcut-widget property, with the idea that you set it to your toplevel window unless you have multiple tab views.

In Libadwaita 1.0, that property was removed, and instead the shortcuts were switched to the GTK4 shortcut engine, using global scope (really MANAGED scope, but doesn’t matter). This means that they were available for the whole window automatically.

However, this also brought issues. For example, the Ctrl+Tab shortcut stopped working, because GTK defines that shortcut itself, for changing widget focus. Similarly, GTK4 port of the VTE library handles all input on the CAPTURE propagation phase, meaning none of the tab view shortcuts could work at all in the GTK4 port of GNOME Console.

Meanwhile in both libhandy and libadwaita 1.0.x-1.1.x, apps that want to disable some of the shortcuts had to do pretty terrible hacks to achieve it, for example, registering the same shortcuts on the toplevel window and redirecting input to the tab contents or whatever widget needs to handle them.

So, to fix that, AdwTabView has switched its shortcuts to the CAPTURE phase as well, and added an easy way to enable or disable shortcuts.

Other than that, AdwTabPage now has a property to set the tooltip for its indicator icon, and AdwTabBar tabs similarly has a tooltip on the tab close buttons – not customizable this time.

Toast Additions

  • Kévin Commaille added a way to have custom widgets as toast titles. This allows, for example, to have avatar+name pills inside toasts in Fractal.
  • Emmanuele Bassi added a helper function for creating a toast with formatted title.
  • Jamie added AdwToast::button-clicked signal to make it possible to have toast buttons without actions.
  • Jonathan Blandford made it possible to add or dismiss toasts multiple times to make them easier to use. Dismissing toasts that have already been dismissed does nothing now, while adding toasts that have already been added resets the timeout for toasts that are currently shown, or bumps them forward in the queue otherwise. Previously doing that resulted in criticals being printed.

Other Additions

  • AdwPropertyAnimationTarget has been added to simplify animating object properties.
  • AdwAnimation can now change targets on the fly.
  • Sophie Herold added a property to allow disabling Pango markup in boxed list row titles (as well as subtitles in case of AdwActionRow etc).
  • AdwSplitButton can now have a separate tooltip for its dropdown button.
  • AdwViewStack has gained a helper function to add a child along with a title and an icon.
  • JCWasmx86 added plumbing to AdwStyleManager to allow GNOME Builder to launch apps with light/default/dark color scheme, as well as in high contrast. This has also been backported to Libhandy, and is available in 1.8.0 (which doesn’t have a blog post as it’s the only noteworthy change there).


AdwTabBar in Libadwaita demo 1.2

A few styles have been tweaked. In particular, AdwTabBar has seen big changes, mainly to improve contrast in dark variant and make it clearer which tab is selected.

A comparison between AdwTabBar in Libadwaita 1.1 and 1.2, in dark style

The lack of full height separators has also allowed to remove the distracting light borders from header bars and search bars in favor of dark borders using @headerbar_shade_color and add a proper backdrop style consistent with header bars.

In addition, GtkActionbar and AdwViewSwitcherBar now have the same style as header bars, search bars and tab bars, while boxed lists use dark separators.

Alarms page from GNOME Clocks with Libadwaita 1.2

The .large-title style class is now documented as deprecated and apps should use .title-1 instead.

There are also a few smaller changes, for example:

  • Toasts without buttons are smaller now, instead of taking a fixed width.
  • AdwActionRow child spacing is now smaller, matching toolbars and the new AdwEntryRow.
  • AdwExpanderRow arrows have changed orientation to look less like rows that open subpages on click.
  • Lots of fixes, some of them have been backported to libadwaita 1.1.x as well.

As it happens, quite a lot didn’t make it because it needed more work, or the timing just didn’t work out. This also means that there’s a lot to look forward to in the next release though, so stay tuned.

Thanks to all the contributors, and thanks to my employer, Purism, for letting me work on Libadwaita and GTK to make this release happen.

Libadwaita 1.1, Libhandy 1.6

Libadwaita 1.1 and Libhandy 1.6 are now released to match the upcoming GNOME 42.

Libadwaita 1.1

Since Libadwaita 1.0 was released just a few months ago, 1.1 doesn’t contain a lot of features, but still has a few.

Header Suffixes

Christopher Davis implemented header suffixes for AdwPreferencesGroup, allowing to put a widget next to the group’s title and subtitle:

Settings 42, Appearance panel, a screenshot of a preference group title and suffix widget. The title says "Bakground", the suffix widget says "Add Picture…" and has a "+" icon.
The “Add Picture…” button is a header suffix

Selectable Titles

Niels De Graef has added API to make the title of an AdwActionRow selectable.

An action row in Contacts, showing a personal email "", a part of the address is selected

Better Cross-platform Support

Chun-wei Fan made Libadwaita build with the MSVC compiler, meanwhile Christian Hergert implemented support for the system dark mode when running on macOS.

Misc Changes

Libadwaita 1.1 also includes a bunch of bug fixes, though most of them are also in the 1.0.3 point release.

Libhandy 1.6

UPDATE: there’s 1.6.1 now, fixing a bug with HdyStyleManager.

Since Libhandy hasn’t had a release along with Libadwaita 1.0, the 1.6 release can be summarized as bringing it up to par with Libadwaita. As such, the two big new features in Libhandy 1.6 are:

New Docs

Maximiliano ported the documentation to gi-docgen and significantly cleaned it up. The new documentation can be found here.

A screenshot of the libhandy docs in a GTK4 Epiphany build

Style Manager

Libhandy 1.6 contains a backport of the AdwStyleManager class, allowing GTK3 applications to use the dark style preference in GNOME 42.

Unlike in Libadwaita, HdyStyleManager is not initialized unless the application explicitly uses it, and defaults to the FORCE_LIGHT color scheme instead of PREFER_LIGHT. This means that applications have to actively opt-in to support the preference, and nothing changes for existing applications.

File Roller and Files following the dark preference
File Roller and Files following the dark preference

Initializing HdyStyleManager also changes how the high contrast mode is handled to make it consistent with the regular style as well as Libadwaita: it does not make dark applications unconditionally light. Instead, it follows the application and system color schemes the same way as it does for the regular style. This way applications can be both dark and high contrast at the same time, i.e. the preferences can work together. Even though it’s not exposed in Settings, the prefer-light color scheme can be used to ask apps to still be light, regardless of whether high contrast is enabled.

Eye of GNOME and Boxes with the light color scheme
Eye of GNOME and Boxes with the light color scheme
Eye of GNOME and Boxes with the light color scheme and high contrast enabled
Eye of GNOME and Boxes with the light color scheme and high contrast enabled
Eye of GNOME and Boxes with the default color scheme and high contrast enabled
Eye of GNOME and Boxes with the default color scheme and high contrast enabled

Misc Changes

It also backports a lot of bug fixes from Libadwaita, and features a much cleaner and significantly faster CI pipeline. The development branch has been renamed to main.

Libadwaita 1.4 and 1.6 CI pipelines.  The 1.2 one took an hour and 26 minutes, the 1.4 one took 10 minutes

Overall neither release is very exciting, but they provide a closure to this cycle. Thanks to all the contributors!

Libadwaita 1.0

Libadwaita 1.0 Demo

Libadwaita 1.0 has been released, just at the end of the year.

Libadwaita is a GTK 4 library implementing the GNOME HIG, complementing GTK. For GTK 3 this role has increasingly been played by Libhandy, and so Libadwaita is a direct Libhandy successor.

You can read more in Adrien’s announcement.

What’s New

Since Libadwaita is a Libhandy successor, it includes most features from it in one form or another, so the changes are presented compared to it.

If you’ve been following This Week in GNOME, you may have already seen a large part of changes.

Updated Stylesheet

Probably the most noticeable change is the reworked stylesheet.

For the past 7 years, the Adwaita style has been a part of GTK. Now it’s a part of Libadwaita instead, while the GTK style has been renamed to Default.

Since we have this opportunity, the stylesheet has been completely redesigned with several goals in mind:

Modernizing the style

GNOME designers have long wanted to do this, and the GTK 4 Default style contains a few changes in that direction compared to the GTK 3 version of Adwaita, and GNOME Shell has been using a similar style as well. Libadwaita takes it much further. You can read more about it Allan Day’s blog post.

The changes are not fully compatible with GTK Default and may require changes on the application side when porting. I’ve also blogged in detail about the biggest breaking change: the updated header bar style.

Runtime recoloring

Ever since Adwaita started using SCSS, it couldn’t really be recolored at all without recompiling it. This created big problems for applications that wanted to do that.

For example, GNOME Web makes its header bar blue in incognito mode. This may sound simple, but involves copy-pasting large chunks of Adwaita into the app itself and making many small changes everywhere to adjust it, as well as using SCSS for it because the original style is SCSS. More recently, GNOME Console and Apostrophe started doing the same thing – copy-pasted from Web, as a matter of fact. This approach means the style is messy and extremely hard to keep up to date with Adwaita changes – I have updated this style for the 3.32 style refresh and never want to do this again.

Another approach applications like Contrast are using (were using with GTK 3, anyway), is copying the whole stylesheet from GTK, and using libsass to recompile it in runtime. This worked – it’s much more maintainable than the first approach, but fell apart when libsass got deprecated.

Meanwhile, the elementary OS stylesheet has been doing recoloring just fine with nothing but @define-color – and so Libadwaita does exactly that, it exposes all of the colors it uses (31 as of the moment of writing) as named colors. The new colors are also documented and will be treated as a proper API.

It also drops all of the formerly used PNG assets, so the colors can affect the elements that used them.

It also reworks the high contrast variant to use the same colors when possible to make sure that changing color for the regular style also works with high contrast.

Another thing the new stylesheet does is simplifying how it handles colors in general. The new simplified style comes in very handy here.

For example, many parts of the UI are now derived from the text color and change with it automatically, and widgets that don’t absolutely need to define their own text color don’t do that anymore, so it can propagate. Where possible, transparency is used instead of mixing or hardcoding colors.

All in all it means that simple custom styles like this one:

/* Solarized popovers */
popover > arrow,
popover > contents {
  background-color: #fdf6e3;
  color: #586e75;

actually work correctly and with no glitches, in both light and dark variants, as well as high contrast style. Try doing that with GTK 4 Default and compare the results:

Dark Variant Contrast

The dark variant of Adwaita has historically been intended to be used as a lights-out, low distraction appearance for media apps – video players, image viewers, games – and not as a general purpose dark style. As such, it has pretty low contrast and can be hard to see at times.

A primary example is the accent color – historically, Adwaita has never really had a proper accent color as a named color – and many applications have been using @theme_selected_bg_color – a background color used for selected text and list items – as an accent color for text and icons. Not only does it not have a good enough contrast to be used as text color, the dark variant dims it even further, to make this background color less distracting – so while it’s not too bad in the light variant, it falls apart with dark.

Libadwaita fixes that – it makes the accent brighter (made possible by not using it in contexts where it can be distracting), and introduces a second color to be used for cases like this. This second color does vary between the light and dark variants, and this allows it to be much brighter in dark variant, and darker in light variant, so it’s suitable for text.

It also changes many other things. The window background is now darker, while elements like buttons and boxed lists are lighter, GtkSwitch and GtkScale sliders are light, etc.

Style Classes

Button styles: regular (no style), flat, suggested action, destructive action, a few custom colored buttons, circular, pill, osd
Various button styles

The updated stylesheet includes many new style classes for app developers to use, in a lot of cases codifying existing patterns that applications have been using via custom styles, but also adding new things.

Some highlights:

  • .pill makes a button large and rounded – in other words, makes it a pill button
  • .flat can now be used with GtkHeaderBar
  • .accent colors a label into the accent color (using the correct color as per the above section)
  • .numeric makes a label use tabular figures
  • .card makes a widget have the same background and shadow as a boxed list.

And speaking of boxed lists, the old .content style class from Libhandy has finally been renamed to .boxed-list, matching the HIG name.

The available style classes (both existing and new) are now documented, and Libadwaita demo now includes a sample of each of them.

Refactoring and cleanups

Adwaita has historically been a big SCSS file containing most styles, another file containing complex mixins for drawing buttons, entries and other widgets, and a few more files for colors.

Libadwaita splits all of that into small manageable files. It removes the complicated mixins, because the new style is simple enough that they aren’t needed. It removes tons of unused and redundant styles, some of which were leftovers from early GTK 3 days, and so on. And, of course, the new style itself allows making styles significantly simpler.

The end result is a much more maintainable and less arcane stylesheet.

Dark Preference

Settings, light
A work in progress dark preference in Settings. Dark version

I’ve blogged about this in much more detail a few months ago, but in short, Libadwaita includes API to support the new cross-desktop dark style preference, as well as streamline the high contrast mode handling.

This has also been backported to Libhandy and will be available in the next release.

While the Libhandy version is strictly opt-in, Libadwaita flips the switch and follows the preference by default, unless the application opts out. This means that any new applications will support the preference by default – and that supporting it is an expected step when porting an application from GTK3 and Libhandy. The documentation now also includes a guide on how to handle application styles.

Many third party applications have already adopted it by now, and there has been good progress on supporting it in the core GNOME applications – though at the moment it’s unlikely that all of the core applications will support it in GNOME 42. If you maintain a core app, it’s a perfect time to start supporting it in order to avoid that ?️.

Libadwaita GtkInspector page

A new GtkInspector page is also available to help testing the style and high contrast preferences.


Like GTK 4 itself, Libadwaita features new documentation using the awesome gi-docgen generator by Emmanuele Bassi.

The docs themselves have been reworked and expanded, and feature new generated screenshots, which all come in light and dark versions to match the documentation pages:


Toast saying "'Lorem Ipsum' Deleted", with an Undo button

While in-app notifications aren’t a new pattern by any means, we’ve never really had a ready to use widget. Sure, GdNotification exists, but it leaves a lot of decisions to applications, e.g how to deal with multiple notifications at once, or even what notifications should contain – essentially it only provides the notification style, a close button and a timeout.

A big feature that made GdNotification attractive was the ability to animate its visibility before GTK had a widget for that purpose. Now GtkRevealer exists (which our new widget ironically doesn’t use), and most apps currently use that to re-implement in-app notifications from scratch. This has lead to major inconsistencies between apps, and situations like this:

GNOME Boxes, two undo notifications, awkwardly stacked
GNOME Boxes, two undo notifications

To help fixing this, Maximiliano has implemented a new widget to replace them. Its API is very streamlined, and is modeled after notifications. The widget part is not a notification, but rather a notification area that toasts (which are just generic objects and not widgets) are added into. If multiple toasts are added in a quick succession, they are queued based on their priority.

A big difference from GNotification though is that toasts are mutable – and can be changed after they have been shown. This is useful when using toasts as undo bars, for example.


Libadwaita animation demo

Manuel Genovés has implemented an animation API as part of his GSoC project. Unfortunately not everything that was planned has been implemented, but we have basic timed animations and spring animations.

Timed animations provide simple transitions from one value to another one in a given time and with a given curve. They can repeat, reverse their direction, and alternate with each iteration.

Spring animations don’t have a fixed duration, and instead use physical properties to describe their curve: damping ratio (or optionally just damping), mass, stiffness, an initial velocity and an epsilon to determine when to stop it. The fact they have a variable initial velocity makes them perfect to animate deceleration after performing a gesture:

AdwLeaflet, AdwFlap and AdwCarousel all use spring animations now, and AdwSwipeTracker provides the final velocity after a swipe is finished, instead of pre-calculated duration.

Unfortunately, due to time constraints, none of the above widgets support overshoot when animating. Since they use a critically damped spring by default (meaning it takes the shortest possible time to reach the end and doesn’t overshoot unless the velocity is very high), it’s not really visible unless you swipe really hard, and it can be fixed after the initial release without any API changes.

Unread Badges

An example of an unread badge

AdwViewSwitcher and related widgets now can display unread badges and not just needs-attention dots. This means they don’t use GtkStack anymore, but a new widget called AdwViewStack. For the most part, it’s a drop-in replacement, although it does trim down the API not necessary for this use case.

Thanks to Frederick Schenk for implementing this!


Nahuel has implemented AdwApplication – a GtkApplication subclass that automatically initializes Libadwaita when used. It also automatically loads styles from GResource relative to the application base path. For example, if your application has org.example.App application ID, it will automatically load /org/example/App/style.css. It also loads style-dark.css, style-hc.css, and style-hc-dark.css, allowing to add styles for dark or high contrast styles only.

Helper Widgets

Libadwaita provides a few widgets to simplify common tasks:

  • GtkHeaderBar in GTK4 does not provide a direct way to set a title and a subtitle, and just shows the window title by default. If you want to have a subtitle or to simply display a title that’s different from the window title – for example, for split header bars – the recommended way to do that is to construct two labels manually. That can be tedious and easy to get wrong. AdwWindowTitle aims to help with that. It can be used as follows:

    <object class="GtkHeaderBar">
      <property name="title-widget">
        <object class="AdwWindowTitle">
          <property name="title">Title</property>
          <property name="subtitle">Subtitle</property>
  • AdwBin is a widget that uses GtkBinLayout, has one child, provides API to manage it, implements GtkBuildable accordingly, implements GtkWidget.compute_expand(), and unparents the child in GObject.dispose(). Applications can subclass it instead of GtkWidget without worrying about those things. It can also be used directly without subclassing it.
  • AdwSplitButton provides an easy way to create a, well, split button that will use the correct appearance in a header bar or a toolbar.
  • AdwButtonContent can be used to create a button with an icon and a label without needing to manually set up the button mnemonic:

    <object class="GtkButton">
      <property name="child">
        <object class="AdwButtonContent">
          <property name="label">_Open</property>
          <property name="icon-name">document-open-symbolic</property>
          <property name="use-underline">True</property>

API Cleanups

Large parts of the API have been streamlined. Check out the migration guide for more details.

Some highlights:

  • AdwHeaderBar provides separate properties for controlling window buttons at the 2 ends of the header bar, instead of one controlling both sides. This can be used to implement split header bar layouts and removes the need for HdyHeaderGroup.
  • Ever since HdyWindow we’ve had an easy way to support leaflet swipes spanning both the window and the titlebar without HdyHeaderGroup. All of the features HdyWindow and HdyWindowHandle provided have been added into GTK itself, and have been available since 4.0. In acknowledgement of that, Libadwaita doesn’t include a HdySwipeGroup equivalent.

    It still includes AdwWindow and AdwApplicationWindow as helpers, but it’s very easy to achieve the same result with a GtkWindow.

    <object class="GtkWindow">
      <property name="titlebar">
        <object class="GtkBox">
          <property name="visible">False</property>
      <property name="child">
        <!-- ... -->
  • AdwComboRow has been completely overhauled and is now basically a GtkDropDown clone. This means it uses the same list item factories, allows setting the models from UI files, etc. One thing it doesn’t provide is binding enums – to replace that, Libadwaita includes AdwEnumListModel.
  • AdwAvatar removes the old complicated image loading API and instead just allows setting a GdkPaintable as a custom image.
  • AdwLeaflet now supports the back/forward mouse buttons and keyboard keys, as well as Alt+arrow shortcuts, in addition to swipe gestures, and the properties controlling that have been renamed to reflect the addition.

What hasn’t made it

About Window

Work-in-progress about window

Even though it was featured in Allan Day’s blog post a few months ago, the new About window Adrien has been working on hasn’t made it into Libadwaita 1.0. There were still unresolved design and API questions, and we decided to wait until the next release to have time to polish it instead of rushing it.

Color API

While overriding colors via @define-color is far simpler than it was before (essentially, copying the entire style of the widgets you want to change, with different colors), it’s still not as easy as it could be.

For example, if an application wants to override its accent color, it needs to override 3 colors. One of them (@accent_color) exists pretty much only for contrast, and also differs between light and dark variants. In an ideal world, this color would be calculated automatically based on @accent_bg_color.

Chris has been working on a programmatic API to manage colors, and that should improve this situation a lot. As with the about window, though, it wasn’t ready in time for 1.0.

Touchpad Swipes

A known regression compared to Libhandy is that swipes in widgets such as AdwLeaflet only work on touchscreen, but not touchpad, if the pointer is above a scrolling view. This is something I really hoped to fix before 1.0, but it’s a surprisingly complex issue. It needs extensive GTK changes in order to be fixable, involving essentially replacing and deprecating the existing API for dealing with scrolling, and it’s not something that should be rushed.

Thanks To

  • Adrien Plazas, Christopher Davis, Frederick Schenk, Manuel Genovés, Maximiliano
  • GNOME design team
  • GTK developers
  • Frederik Feichtmeier, nana-4 and other members of the community

I also want to thank my employer, Purism, for letting me and others work on Libadwaita and GTK to make this happen.

Happy holidays and happy hacking!

Dark Style Preference

TL;DR GNOME will have a system-wide dark style preference in 42. Please update your apps to support it, see the instructions.

Dark style preference in GNOME
Dark style preference in GNOME 42. Separate dark and light screenshots

Lately, I’ve been working on having a proper dark style preference in GNOME. It’s a frequently requested feature, but also hard to get right. elementary UX architect Cassidy James Blaede did a good write-up about this, please read it if you haven’t yet (or watch his GUADEC talk if you prefer a video).

That was more than two years ago. Since then, elementary OS has started shipping an elementary-specific implementation designed in a way that it could be standardized later without many changes. While I could introduce another preference in GNOME, it was a good excuse to standardize it instead.

How does it work?

The idea is more or less exactly same as what elementary OS is doing: there’s a system preference, a tri-state with values: no-preference, prefer-dark, prefer-light. All 3 are just hints — apps are free to follow or not follow them as appropriate. The no-preference and prefer-dark values are exposed in settings, while the prefer-light value is reserved for future use.

Style: default; dark. Preferred visual style for system components. Apps may also choose to follow this preference. Schedule: Disabled; Sunset to Sunrise; Manual. From: 20:00, To: 06:00
Style preferences in elementary OS 6.0

The new preference is defined in the settings portal as the org.freedesktop.appearance.color-scheme key.

xdg-desktop-portal-gnome already implements this key (in the main branch, not in 41), and there are work-in-progress elementary and KDE implementations.

This way it’s not tied to any particular desktop or toolkit — any application can access the settings portal via DBus and read the preference — so applications like Firefox have a canonical location to access the preference from instead of trying to guess it from GTK theme name. Being in the portal also means it’s accessible to Flatpak apps without opening any sandbox holes.


Manually accessing the portal is a bit tedious, and so we wrap it into an easy to use API. Here’s where things differ more from the original elementary preference.

elementary exposes the preference in Granite (right now it only works with the elementary preference, there’s a pull request to make it use the portal), and leaves everything else to apps, so using the API looks like this:

var gtk_settings = Gtk.Settings.get_default ();
var granite_settings = Granite.Settings.get_default ();

gtk_settings.gtk_application_prefer_dark_theme = (
    granite_settings.prefers_color_scheme == DARK

granite_settings.notify["prefers-color-scheme"].connect (() => {
    gtk_settings.gtk_application_prefer_dark_theme = (
        granite_settings.prefers_color_scheme == DARK

It works, but that’s quite verbose, and most apps do either that or prefers_color_scheme != LIGHT, and maybe vary other parts of the UI such as GtkSourceView color schemes.

For GNOME the API is in libadwaita and libhandy, and is vaguely inspired by CSS. The equivalent Vala code using libhandy looks like this:

Hdy.StyleManager.get_default ().color_scheme = PREFER_LIGHT;

The other color schemes are FORCE_LIGHT, PREFER_DARK and FORCE_DARK, as follows:

system \ app force-light prefer-light prefer-dark force-dark
prefer-light light light light dark
default light light dark dark
prefer-dark light dark dark dark

There’s also a read-only boolean property dark that corresponds to whether the UI currently appears dark.

There are also properties to check if the system has a system-wide color scheme preference, to allow apps to keep their dark style switches when running on GNOME 41, but drop them for 42; and, somewhat unrelated, a property to check if the system is currently using high contrast mode instead of it being a theme.

Libadwaita also provides a GtkInspector page to test all of this without changing the system preferences, though there’s no libhandy equivalent.

Libadwaita inspector page

The libadwaita API is already available in alpha 3, the libhandy one is not released yet.

Another difference is — the dark style preference is supported by default if you’re using libadwaita. While in libhandy the default is keeping the previous behavior — apps that were always light remain always light – libadwaita goes ahead and makes following the preference the default. Since it’s not API-stable yet, it’s an acceptable behavior break, same way as macOS and iOS support it automatically when building against new enough platform libraries, but don’t do it otherwise in order to keep existing apps working.

When porting from GTK3 and libhandy to GTK4 and libadwaita, apps are expected to start supporting this or otherwise opt out. When already using older versions of libadwaita, apps are expected to start supporting this when updating to alpha 3. When using libhandy, apps don’t get the support by default, but can explicitly opt in.


Another thing libhandy and libadwaita do when switching appearance is they try very hard to block the CSS transitions that would usually occur. These transitions can take a long time and are inconsistent between widgets. For widgets with custom drawn content such as WebKitWebView this can’t work at all, so no point in trying.

An approach that yields much better results is doing the transition on the compositor side — then it works for any content automatically:

It’s still not perfect: GTK3 apps can take a pretty long time when doing this, and it will be noticeable: for example the GTK4 Patterns window on the video changes its appearance immediately while Settings and Web lag behind. The video was recorded in a VM though, and the transition should be smoother on bare metal.

When is it coming?

Most likely GNOME 42 – most of the plumbing has already landed, the main remaining things are:

  • The actual switch in Settings — I made a very simple UI for testing, shown in the video above; but it’s obviously not good enough to be used as is;
  • A switch in gnome-shell, similarly to how Android and iOS have one in Quick Settings or Control Center respectively;
  • The aforementioned compositor-side transition; I have a mostly working implementation shown in the video, but it still needs some work;
  • Day/night scheduling;
  • Synchronizing wallpaper with the preference when possible – for example, forcing the night version for the default timed wallpaper.
  • Supporting it from the applications.

How can you help?

The main area that needs work is the last item.

For core apps, we have an initiative, the goal is to port as many of them before 42 as possible.

The instructions from the initiative also apply to third party apps, although they can update at their own pace.

This applies to non-GTK apps as well — the instructions have an example of accessing the portal manually and following the preference from an SDL app.

Finally, if you maintain a GNOME-adjacent website, it would be nice if it followed the system preference. Some of the webpages do that, e.g. the GNOME 41 release notes or GTK documentation, but most don’t. Incl. this blog, yes.

So, let’s make it happen. For now, happy hacking.

I want to thank elementary folks for kick-starting all of this — it’s unlikely it would have happened otherwise.

Cleaning up header bars

Examples of app header bars after the redesign

You might have noticed that a lot of icons in GNOME 41 have been redrawn to be larger. They are still 16×16, but take up more of the space within the canvas. Compare:

Examples of symbolic icons in GNOME 40 and 41

This was a preparation for a larger change that has just landed in the main branch in libadwaita: buttons in header bars and a few other widgets have no background now, matching this mockup.

For example, this is how the recent GTK4 branch of GNOME Software looks like now:

GNOME Software using GTK4 and libadwaita

Making the style feel lighter and reducing visual noise is a major goal for the style refresh we’re doing for libadwaita. While we’ve done lots of smaller changes and cleanups across the stylesheet to bring us closer to that goal, this is probably the highest-impact part of it due to how prominent header bars are in GNOME apps.

This is not a new idea either — pretty much everyone else is doing it, e.g. macOS, Windows, iOS, Android, elementary OS, KDE.

In fact, we’ve been doing this for a long time in view switchers. So this just extends it to the whole header bar.

However, if applied carelessly, it can also make certain layouts ambiguous. For example, a text-only button with no background would look exactly same as a window title. To prevent that, we only remove background from buttons that we can be confident won’t look confusing without it — for example, buttons containing a single icon.

While we avoid ambiguous situations, it also means that apps will need changes to have consistent buttons. In my opinion this is a better tradeoff: since the API is not stable yet, we can break behavior, and if an app hasn’t been updated, it will just get inconsistent look and not accessibility issues.


The exact rules of what has background and what doesn’t are as follows:

The following buttons get no background:

  • Buttons that contain icons (specifically the .image-button style class).
  • Buttons that contain an icon and a label, or rather, the .image-text-button style class.
  • Buttons with the .flat style class.
  • UPDATE: Any GtkMenuButtons with a visible arrow (the .arrow-button style class).
  • UPDATE: Any split buttons (more on that later).

Flat button examples: icon buttons; buttons with icons and text

The following buttons keep their default appearance:

  • Text-only buttons.
  • Buttons with custom content.
  • Buttons with .suggested-action or .destructive-action style classes.
  • Buttons inside a widget with the .linked style class.
  • A new addition: buttons with the .raised style class, as inspired by the elementary OS stylesheet.

Raised button examples: text buttons, linked buttons, custom buttons, suggested and destructive buttons

The appearance of GtkMenuButton and AdwSplitButton (more on that later) is decided as if they were regular buttons.

Icon-only, icon+text and text-only arrow and split buttons

UPDATE: menu buttons with visible arrows and split buttons don’t have a background anymore regardless of their content.

Icon-only, icon+text and text-only arrow and split buttons, updated. Text buttons are flat too now

This may look a lot like the old GtkToolbar, and in a way it is. While GTK4 doesn’t have GtkToolbar, it has the .toolbar style class to replace it, and this style is now shared with header bars and also GtkActionBar.

Special cases

While the simple icon-only buttons are easy, a lot of applications contain more complex layouts. In that case we fall back to the default appearance and apps will need changes to opt into the new style for them. Let’s look at some of those patterns and how to handle them:

Menu buttons with icons and dropdown arrows

A menu button with an icon, GTK3

This case works as is if you use the standard widgets — namely, GtkMenuButton with an icon set via icon-name and always-show-arrow set to TRUE.

A menu button with an icon

The only reason this case is special is because always-show-arrow is relatively new, having only been added in GTK 4.4, so a lot of apps will have custom menu buttons, or, if porting from GTK3, GtkMenuButton containing a GtkBox with an icon and an arrow. Since we don’t remove backgrounds from buttons with custom content, both of them will have backgrounds.

Text-only buttons

A button with text, GTK3

This is the most common case outside icon-only buttons. For these buttons the solution, rather counter-intuitively, is to add an icon. Since the icon has a label next to it, it doesn’t have to be very specific, so if an action is hard to describe with an icon, an only tangentially related icon is acceptable. If you still can’t find anything fitting — open an issue against the GNOME icon development kit.

With GTK widgetry, the only way to create such buttons is to pack a GtkBox inside, and create the icon and the label manually. Then you’ll also have to add the .image-button and .text-button style classes manually, and will need to set the mnemonic-widget property on the label so that mnemonics work.

Since this is tedious and parts like connecting the mnemonic are easy to miss, libadwaita now provides AdwButtonContent widget to do exactly that. It’s intended to be used as a child widget of GtkButton, GtkMenuButton or AdwSplitButton (more on that below), as follows:

<object class="GtkButton">
  <property name="child">
    <object class="AdwButtonContent">
      <property name="icon-name">document-open-symbolic</property>
      <property name="label" translatable="yes">_Open</property>
      <property name="use-underline">True</property>

A button with an icon and text

If it’s a GtkMenuButton, it would also make sense to show a dropdown arrow, as follows:

<object class="GtkMenuButton">
  <property name="menu-model">some_menu</property>
  <property name="always-show-arrow">True</property>
  <property name="child">
    <object class="AdwButtonContent">
      <property name="icon-name">document-open-symbolic</property>
      <property name="label" translatable="yes">_Open</property>
      <property name="use-underline">True</property>

A menu button with an icon and text

UPDATE:Menu buttons with visible arrows don’t have background by default anymore, the step above is not necessary.

Note: the child property in GtkMenuButton is fairly new, and is not in a stable release as of the time of writing. It should land in GTK 4.6.

Notice we didn’t have to add any style class to the buttons or to connect mnemonics like we would have with GtkLabel. AdwButtonContent handles both automatically.

Split buttons

Split buttons in GTK3: with text and icon

This is a fairly rare case, but also a difficult one. Historically, these were implemented as 2 buttons in a .linked box. Without a background, it’s easy to make it look too similar to a regular menu button with a dropdown arrow, resulting in an ambiguous layout.

While existing split buttons will keep their background thanks to the .linked style class, we now have a way to make consistent split buttons – AdwSplitButton.

Examples of split buttons

Whether they get a background or not depends on the content of their button part, while the dropdown part follows the suit – they will have background if it has only a label, will have no background if it has an icon, and will keep their default appearance outside header bars or toolbars. If it has no background, a separator is shown between them and they gain a shared background when hovered, pressed, or the dropdown is open:

They can be adapted the same way as regular buttons — via AdwButtonContent:

<object class="AdwSplitButton">
  <property name="menu-model">some_menu</property>
  <property name="child">
    <object class="AdwButtonContent">
      <property name="icon-name">document-open-symbolic</property>
      <property name="label" translatable="yes">_Open</property>
      <property name="use-underline">True</property>

A split button with an icon and text

UPDATE: split buttons with text or custom content don’t get background by default anymore, so the step above is not necessary.

Meanwhile, buttons like the list/grid selector in Files are as simple as:

<object class="AdwSplitButton">
  <property name="menu-model">some_menu</property>
  <property name="icon-name">view-list-symbolic</property>

A split button with an icon

Otherwise, AdwSplitButton API is mostly a union of GtkButton and GtkMenuButton – the button part can have a label, an icon or a custom child, an action name and target, and a clicked signal if you prefer to use that. Meanwhile, the dropdown part has a menu model or a popover, and a direction that affects where the popover will be shown, as well as where the dropdown arrow will point.

Finally, in a lot of cases layouts that were using split buttons can be redesigned not to use them – for example, to use a simple menu button for opening files like in Text Editor instead of a split button in Apostrophe).

Linked buttons

Linked buttons, GTK3

With visible frames, linking buttons is a nice way to visually group them. For example, we commonly do that for back/forward buttons, undo/redo buttons, mode switching buttons. We also use multiple groups of linked buttons to separate them from each other.

For the most part linked buttons can, well, stop being linked. For example, back/forward buttons like this:

Linked buttons

can become this:

Unlinked buttons

However, when multiple linked groups are present, just unlinking will remove the separation altogether:

Unlinked buttons without spacing, not grouped

In that case, additional spacing can be used. It can be achieved with a GtkSeparator with a new style class .spacer:

<object class="GtkSeparator">
    <class name="spacer"/>

Unlinked buttons with spacing, grouped

Action dialog buttons

A dialog, GTK3

This special case is less special than other special cases (or more special, if you prefer), in that apps don’t need to handle it, but I’ll mention it for the sake of completeness.

The two primary buttons in an action dialog or a similar context (for example, when changing display resolution in Settings, or the Cancel button in selection mode) should keep their current style — that is, they don’t have icons and keep their background. Meanwhile any extra buttons follow the new style.

In most situations this will already be the case so no changes are needed.

A dialog


There will undoubtedly be cases not covered here. The .flat and .raised style classes can always be used to override the default appearance if need be.

Finally, not everything has to have no background. For example, the remote selector in Software is probably best kept as is until it’s redesigned to also make it adaptive.

And in rare cases, the existing layout just doesn’t work and may need a redesign.

Bundled icons

In addition to all of that, if you bundle symbolic icons, there’s a good chance there are updated larger versions in the icon library. It would be a good idea to update them to match the new system icons.


Let’s update a few apps. App Icon Preview and Obfuscate should show off most of the edge cases.

App Icon Preview

The version on Flathub is still using GTK3 as of the time of writing, but it’s GTK4 in main. So let’s start from there.

App Icon Preview, before libadwaita update

App Icon Preview, New App Icon dialog

App Icon Preview has 2 windows, each with its own header bar — the main window and the "New App Icon" dialog.

After the libadwaita update, the dialog hasn’t changed, meanwhile the main window looks like this:

App Icon Preview, no adjustments

It has a custom split button, as well as a text-only Export button when a file is open.

First, let’s replace the split button with an AdwSplitButton:

<object class="GtkBox">
    <object class="GtkButton">
      <property name="label" translatable="yes">_Open</property>
      <property name="use_underline">True</property>
      <property name="tooltip_text" translatable="yes">Open an icon</property>
      <property name="action_name"></property>
    <object class="GtkMenuButton" id="recents_btn">
      <property name="tooltip_text" translatable="yes">Recent</property>
      <property name="icon_name">pan-down-symbolic</property>
    <class name="linked"/>

This will become:

<object class="AdwSplitButton" id="open_btn">
  <property name="label" translatable="yes">_Open</property>
  <property name="use_underline">True</property>
  <property name="tooltip_text" translatable="yes">Open an icon</property>
  <property name="action_name"></property>

Since we’ve changed the class and the GtkBuilder id, we also need to update the code using it. Hence this:

pub recents_btn: TemplateChild<gtk::MenuButton>,

becomes this:

pub open_btn: TemplateChild<adw::SplitButton>,

and the other recents_btn occurences are replaced accordingly.

App Icon Preview, using AdwSplitButton

UPDATE: after the menu button and split button change, the Open and Export buttons don’t get background anymore, so only the previous step is necessary.

Now we need to actually remove the background. For that we’ll add an icon, and it’s going to be just document-open-symbolic.

So we’ll remove the label and use-underline and instead add an AdwButtonContent child with the same lines together with icon-name inside it:

<object class="AdwSplitButton" id="open_btn">
  <property name="tooltip_text" translatable="yes">Open an icon</property>
  <property name="action_name"></property>
    <object class="AdwButtonContent">
      <property name="icon_name">document-open-symbolic</property>
      <property name="label" translatable="yes">_Open</property>
      <property name="use_underline">True</property>

App Icon Preview with an icon on the open button

Now, let’s look at the Export button. It needs an icon as well, but adwaita-icon-theme doesn’t have anything fitting for it. So instead, let’s check out Icon Library (which doesn’t have a lot of edge cases itself).

While it doesn’t have icons for export either, it has a share icon instead:

Icon Library showing the share icon

So that’s what we’ll use. We’ll need to bundle it in the app, and let’s rename it to export-symbolic while we’re here. Now we can do the same thing as for the Open button:

<object class="GtkMenuButton" id="export_btn">
  <property name="always_show_arrow">True</property>
    <object class="AdwButtonContent">
      <property name="icon_name">export-symbolic</property>
      <property name="label" translatable="yes">_Export</property>
      <property name="use_underline">True</property>

App Icon Preview with an icon on the export button

So far so good. See the merge request for the complete changes.


This app has only one header bar, but it can change its state depending on if there’s a file open:

Obfuscate, with no open file, before libadwaita update

Obfuscate, with an open file, before libadwaita update

After building with newer libadwaita, we see there’s quite a lot to update.

First, we need to add an icon to the Open button. It’s done exactly the same way as in App Icon Preview, so I won’t repeat it.

Obfuscate, with no open file, adapted

Instead, let’s look at the other buttons:

Obfuscate, with an open file, after libadwaita update

Here we have two groups of linked buttons — so .linked is used both to group related actions together, and to separate the 2 groups.

So, first we need to unlink those buttons. Since it’s just removing the 2 GtkBox widgets and instead putting the buttons directly into a header bar, I won’t go in details.

Obfuscate, with an open file, with unlinked buttons, no spacing

However, now we’ve lost the separation between the undo/redo group and the tools. So let’s add some spacing:

<object class="GtkSeparator">
    <class name="spacer"/>

And the end result is the following:

Obfuscate, with an open file, with unlinked buttons and spacing

This information is also present in the libadwaita migration guide, and will be in the stylesheet documentation once all changes are finalized.

For now, happy hacking!

UPDATE (on the same day as published): Menu buttons with visible arrows and split buttons don’t get background by default anymore. The steps and examples have been updated accordingly.

Reinventing tabs

GNOME Web 40

In GNOME 40, Epiphany will feature a new tab bar. This isn’t just a restyling of the old one, but a ground-up rewrite. But why was this needed?

Static vs dynamic

GNOME apps use tabs in a lot of different ways, but they can be roughly divided into two categories. I will refer to them as static and dynamic tabs.

Static tabs

They are completely immutable, each tab contains a title label and they usually don’t scroll. They are usually used in dialogs, but, for example, can also be used in a ribbon toolbar. Modern GNOME apps often use GtkStackSwitcher or HdyViewSwitcher instead, but tabs are still quite common.

Static tabs in various dialogs
Static tabs in various dialogs

Dynamic tabs

These tabs are used in multi-window apps such as web browsers, text editors, file managers or terminal emulators. They are opened and closed by the user, they can be reordered, dragged between windows or into new windows by dropping them on desktop. They need scrolling. They always have a title and a close button, sometimes an icon and/or a spinner. They hide when only one tab is present. They can be closed by middle clicking, they have context menus. Sometimes they can be pinned or have an extra icon, for example an audio playback indicator. Sometimes they need confirmation on closing.

Top to bottom: gedit, GNOME Terminal, Nautilus, GNOME Text Editor, Devhelp, Sysprof, Epiphany

While static tabs are pretty uniform across GNOME, dynamic tabs in every application look and feel very different. For example:

  • GNOME Text Editor has close buttons on selected and/or hovered tabs only, other apps have them on all tabs.
  • Some apps allow tabs to shrink to a very small size before scrolling, others start scrolling earlier.
  • Devhelp doesn’t scroll at all and just overflows tabs off the window.
  • Epiphany and GNOME Terminal have popovers showing all tabs; GNOME Terminal is using GtkMenu while Epiphany is using a GtkPopover.
  • While not visible on the screenshot, only Epiphany and gedit support dragging tabs between windows.
  • It’s possible to drag files onto tabs in Epiphany and Nautilus. In Nautilus, hovering over a tab while dragging a file will automatically switch to that tab, but not in Epiphany.
  • In gedit and GNOME Terminal it’s not possible to focus the active tab by clicking it again.
  • In GNOME Text Editor and Epiphany, moving to another tab when focusing the active tab will select the page instead.

But why such a difference?


All of those apps are using a widget called GtkNotebook. It implements a generic tabbed view and shows tabs on one of the sides, and each tab has a label that can be replaced with an arbitrary widget.

By default, GtkNotebook doesn’t scroll, has a simple title label on each tab, doesn’t expand tabs, doesn’t allow any kind of drag-n-drop.

In other words, it’s geared squarely for static tabs. It doesn’t mean it’s impossible to use it for dynamic tabs, but you will have to manually implement:

  • A tab layout with a close button, a title, an icon, and maybe a mute indicator, and set a reasonable minimum width for scrolling.
  • Shortcuts such as Ctrl+Tab or Alt+[1 – 9] for switching between tabs.
  • Context menu handling, including the keyboard shortcut and a long press gesture for touchscreens.
  • Middle click closing.
  • Tab opening/closing logic — for example, which tab to select when the active tab is closed.
  • If you need to drop data on tabs, a handler for that. You also need to be careful to not accidentally break the hover timeout in process, like Epiphany does in 3.38.x.
  • Hiding when only one tab is present.

Of course, every app ends up implementing these things slightly differently. And then on top of that there are issues that cannot be fully fixed from the app side, such as:

  • Tab scrolling is discrete. This looks confusing and introduces other issues:
    • Dynamic tabs can have a lot of tabs. Pressing arrows to scroll one tab at a time doesn’t really scale for this.
    • Related, it’s not possible to take a tab and reorder it over long distances, you’ll have to reorder it a few tabs forward, press the scroll buttons a few times, reorder it again etc.
    • When opening a new tab, it can end up offscreen with no indication whatsoever.
    • Minimum tab width changes depending on the window size.
  • There are no pinned tabs.
  • The UI in general isn’t very polished — for example, there’s no animation when opening or closing tabs.

Some of that can be papered over though:

  • Epiphany fakes pinned tabs via force reordering them at the beginning, using a compact layout, not expanding them and not allowing to close them, but they can still be scrolled away and the fact the tab width changes based on the window size affects these tabs especially badly:

    Pinned tabs in Epiphany 3.38

    Pinned tabs in Epiphany 3.38

  • It manually highlights arrows when a tab has been opened offscreen.
  • It provides a popover to quickly switch between tabs when they don’t fit on the screen. This doesn’t help with reordering though.
  • elementary’s DynamicNotebook implements a resize delay after closing a tab, but it’s not very reliable, for example it jumps when the scroll arrows disappear. It also can only work when tabs aren’t expanded like in GNOME apps.

Additionally, GtkNotebook contains both view and tabs, and they are inseparable, so it’s impossible to have tabs in fullscreen, autohiding with the header bar, or to put them in the header bar, away from the content.

Finally, it’s not adaptive and Epiphany currently has an in-tree implementation of a mobile tab switcher that every app that wants an adaptive tabbed UI would have to copy.

A rewrite

HdyTabBar demo in libhandy 1.2

So, with all of this, I believe the cleanest way forward is a completely new widget, implementing specifically dynamic tabs. While we’re there, we can also separate the tab view and tab bar into separate widgets just like GtkStack and GtkStackSwitcher are separate.

I started implementing it shortly before GUADEC 2020, and had mostly completed it by the end of September, when I joined Purism. In January me and Adrien finally took the time to finish, review and land it.

The result of this are two widgets called HdyTabView and HdyTabBar, available in the recently released libhandy 1.2.

HdyTabView contains a GtkStack and provides an API more suitable for tabs: for example, pages are strictly ordered, there’s API for tab reordering, but not transition or child names, etc. Additionally, because each page has a large amount of metadata, it uses GTK 4-like page objects (HdyTabPage) even in GTK 3, instead of child properties, which would be pretty annoying to use without property bindings etc.

Meanwhile, HdyTabBar is a tab bar that connects to a HdyTabView and shows its pages, implementing this design.

What it provides

  • Tabs have an icon, a title, a tooltip, a close button, a context menu, and they can show a loading spinner instead of the icon.
  • Pinned tabs. They are compact, placed at the beginning of the tab bar and don’t scroll along with the rest of the tabs; they also affect the behavior of shortcuts such as Ctrl+Home/End.
  • Unread indicators, including when the unread tab is scrolled offscreen.

  • Indicator icons: every page can have one indicator icon on its tab, optionally clickable. Epiphany currently uses it as an audio playback indicator/mute button. Indicators are located on the left side of the tab in order to not conflict with close buttons.

    Indicator icons in libhandy 1.2 demo

    When the tab is pinned, the indicator is displayed instead of the icon and not next to it like previously in Epiphany. To make sure the tab can be easily selected, it’s only clickable if the tab is already selected, so the first click selects the tab, the second click activates the indicator.

  • Autohide when only one tab is present.
  • When closing tabs with a pointer, they don’t fill the empty space immediately, instead they resize so that the next tab’s close button moves exactly where the last one was, allowing to close multiple tabs in a row without moving the pointer.
  • Automatic positioning for new tabs, though there is also API to insert tabs in arbitrary positions if needed.
  • Fully working drag-n-drop: tabs can be reordered, moved between windows, dropped on desktop to create a new window, tabs can accept arbitrary data being dropped. If a tab bar only has one tab, it will be automatically shown when a drag starts and hidden when it ends, so it’s still possible to drop another tab into it.
    Tabs can also be dropped on the tab view; in that case they are appended at the end of the tab bar.
  • Touchscreen support: drag the tab bar to scroll it, long press and drag to reorder a tab, longer press to open context menu.
  • Close confirmation API.
  • Tab opening, closing, reordering, drag-n-drop, showing and hiding of the tab bar are all animated.
  • Shortcuts:
    • Ctrl+Tab and Shift+Ctrl+Tab — switch between tabs, with wrapping.
    • Ctrl+PageUp/PageDown/Home/End — same, but without wrapping.
    • Ctrl+Shift+PageUp/PageDown/Home/End — tab reordering.
    • Alt+[1 – 9] — switching to one of the first 9 tabs.
  • The fact they are separate allows Epiphany to keep its tab bar in fullscreen in 40.

    Epiphany 40 in fullscreen mode

What it doesn’t provide

  • It doesn’t allow to set an arbitrary widget as a tab layout. The layout is fixed and managed exclusively through HdyTabPage properties.
  • It doesn’t allow to toggle reordering and detaching for different tabs separately. Tabs are always reorderable and detachable, there’s no way to disable that.
  • Similarly, there’s no way to make certain tabs expand or not expand. All tabs (except pinned) are expanded by default.
  • All tabs (except when pinned) have close buttons. They cannot be removed, although it’s possible to delay and/or reject a close request, for example if the app wants to show a confirmation dialog upon closing a tab.
  • Close buttons are visible on selected and/or hovered tabs. There’s no option to show them on all tabs.
  • Vertical tabs. HdyTabBar is strictly horizontal and its layout wouldn’t make sense vertical. However, it does have API to observe its pages through a GListModel (GtkSelectionModel in GTK 4), so it’s very easy to make a vertical list instead.

However, since Epiphany is also used on elementary OS, HdyTabBar does provide API to disable autohide and tab expansion, and to swap the close button and indicator. All those options in Epiphany still work as before.

Epiphany in “elementary mode”

Adaptiveness and future

However, HdyTabBar isn’t adaptive either. Epiphany still ships the same mobile UI in 40. However, it makes it a lot easier to implement alternative switchers. For example, a HdyFlap-based bottom sheet:

A work-in-progress mobile tab switcher

However, this is very basic. Mobile tab switchers are often very elaborate, for example using 3D stacks of cards, grids, carousels or lists with previews. Unfortunately, all of that is off limits in GTK 3. However, with GTK 4 it’s not, and we can do things such as this:

A work-in-progress carousel-based tab overview A work-in-progress grid-based tab overview

Having a proper overview would also allow to finally remove the tab popover from Epiphany, which is still there in 40, but only shows when the tab bar starts scrolling.

A GTK 4 port of HdyTabView and HdyTabBar is complete and fully working and just needs code review, though the overview is not a reusable widget yet.

Thanks to GNOME Design Team for designing it and answering my endless questions about how tiny and unimportant things should work, and Thibault Martin and Jordan Petridis for testing it.

Games 3.38

Games 3.38.0

I wanted to start this blog post with “It’s that time of year again”, but looks like Michael beat me to it. So, let’s take a look at some of the changes in GNOME Games 3.38:

retro-gtk 1.0

The library Games uses to implement Libretro frontend, retro-gtk, has been overhauled this cycle. I’ve already covered the major changes in previous blog post, but to recap:

  • Cores now run in a separate process. This provides a better isolation: a crashing core will display an error screen instead of crashing the whole app. This can also improve performance in case the window takes a long time to redraw, for example with fractional scaling.
  • Libretro cores that require OpenGL should now work correctly.
  • The core timing should now be more accurate.
  • Fast-forwarding should actually work, although we don’t make use of it in Games at the moment.

Finally, retro-gtk now has proper docs, published here to go along with the stable API.

Nintendo 64 support

The Legend of Zelda: Ocarina of Time running in Games 3.38, with controller pak switcher open

This was on the radar for a while, but wasn’t possible because all available Nintendo 64 cores use hardware acceleration. Now that retro-gtk supports OpenGL, we can ship ParaLLEl N64 core, and it just works. There’s even a menu to switch between Controller Pak and Rumble Pak.

Unfortunately, retro-gtk still doesn’t support Vulkan rendering, so we can’t enable the fast and accurate ParaLLEl RDP renderer yet.


Collections in Games 3.38

Neville Antony has been working on implementing collections as part of his GSoC project. So far we have favorites, recently played games and user-created collections.

You can read more about the project in Neville’s blog

Faster loading

In my previous blog post I already mentioned some improvements to the app startup time, but a noticeable progress was made since then. Newly added aggressive caching allows the app to show the collection almost instantly after the first run, and then it can scan and update the collection in background.

Search provider

Search Provider in Games 3.38

Having a complete cache of the collection also allowed to easily implement a fast search provider, so the collection can now be searched directly from GNOME Shell search.

Nintendo DS screen gap

Nintendo DS has two screens. Most games use one of the screens to display the game itself, and the other one for status or map. However, some games use both screens and rely on the fact they are arranged vertically with a gap between them.

If the two screens are shown immediately adjacent to each other, these games look confusing, so in 3.38 a screen gap will be automatically added when using vertical mode.

The size of the screen gap is optimized for a few known games, such as Contra 4, Sonic Colors or Yoshi’s Island DS, other games use a generic value of 80 pixels.

If you know another game that needs to use a specific gap size, please mention it in the comments or open an issue on GitLab.

Gamepad hot plugging in Flatpak

Previously, a major limitation of Flatpak version was that gamepads had to be plugged in before starting the app. Thanks to a change in libmanette 0.2.4, this has been fixed and gamepads can be connected or disconnected at any time now.

Technically, this isn’t a 3.38 addition, and in fact the 3.36.1 build on Flathub already supports it. Nevertheless, it was done since my last blog post and so here we are. :)

Miscellaneous changes

Games covers in Games 3.38

Game covers now look prettier thanks to Neville: they are now rounded and have a blurred version of the cover as background.

Preferences window in Games 3.38

The preferences window has been overhauled to be more similar to the libhandy preferences; unfortunately we can’t use HdyPreferencesWindow yet because of some missing functionality.

Controller testing in Games 3.38

When testing or re-mapping a controller, analog stick indicators now show the stick’s precise position instead of just highlighting one of the edges.

Search empty state in Games 3.38

Veerasamy Sevagen implemented an empty state screen for the collection search.

Swipe to go back in Games 3.38

Swipe gestures to go back are now supported throughout the app. The only place that lacks the gesture now is exiting from a running game, because the game may require pointer input, and having a swipe there may cause conflicts.

Getting Games

Download on Flathub

As always, the latest version of the app is available on Flathub.