Animation somewm-only
Set a duration to enable animation; 0 disables it. SomeWM uses Clay transitions for layout, visibility and widget-property changes.
Use .animation for a persistent policy and widget:animate{...} for a single change. See Updating a policy to edit an existing policy.
Layout changes
Animate client layout changes, including resize, tag layout changes and carousel scrolling:
theme.animation_layout_duration = 0.15 -- seconds
somewm.animation.layout_duration sets the duration from code. Theme values animation_layout_duration and animation_duration take precedence. The deprecated somewm.layout_animation module sets it to 0.15 when required. A client’s first frame after enabling it or reloading does not animate. Appearance and disappearance use separate triggers below.
Tiled windows ease into their new geometry when the master width changes and when the windows swap places.
Clients
A client reads its durations and entrances from c.animation, a table with one key per trigger, and falls back to the theme.
| Trigger | When | Theme duration | Theme entrance |
|---|---|---|---|
appear | the client maps | animation_appear_duration | animation_appear |
disappear | the client closes | animation_disappear_duration | animation_disappear |
tag | the client enters or leaves the view with a tag switch | animation_tag_duration | animation_tag; default grow |
focus | the border colour changes with focus | animation_focus_duration | |
maximize | maximized or fullscreen toggles | animation_maximize_duration | |
floating | floating toggles | animation_floating_duration |
theme.animation_duration is the duration for every trigger that has none of its own, layout included.
The same window moves between maximised, tiled and floating states, with a different duration for each transition.
Choose an entrance effect:
| Effect | Behaviour |
|---|---|
grow | Increases the frame’s layout size from zero. |
scale, scale_top, scale_bottom, scale_left, scale_right | Scales the rendered image around its centre or an edge midpoint; layout size stays unchanged. See Scale. |
slide_left, slide_right, slide_up, slide_down | Slides from that screen edge. |
fade | Increases opacity from transparent. |
Exits reverse the effect.
A list runs several effects together in one transition, with one duration:
theme.animation_appear = { "fade", "slide_left" }
- One slide, one size effect (
growor one of the scale words) andfadecombine, in any order. - A second slide direction, or a second size effect, warns once and the first one named stays.
- A repeated word counts once.
collapseis another name forgrow.
{ "slide_left", "slide_right" } logs:
transition.enter: slide_left + slide_right conflict; slide_right is ignored, slide_left remains active
none names no entrance. Beside another effect it warns the same way. An explicit disappear = "none" closes the client without an animation instead of using the tag exit.
From a rule:
ruled.client.append_rule {
rule = { class = "mpv" },
properties = { animation = { appear = "fade", disappear = "fade", tag = "slide_left" } },
}
Each c.animation trigger accepts an effect name, a list of effects, or a duration. Effects use their trigger’s theme duration, falling back to theme.animation_duration. The duration must be above 0.
A client fades in and out, followed by a popup using the same entrance and exit style.
Or from the theme for every client:
theme.animation_appear = "grow"
theme.animation_appear_duration = 0.2
theme.animation_tag_duration = 0.15
theme.animation_focus_duration = 0.1
A number in c.animation is a duration for that trigger. To ease one client's focus border over 0.3 seconds, reassign its policy:
local policy = c.animation or {}
policy.focus = 0.3
c.animation = policy
Tag transitions default to grow; override the effect with c.animation.tag or theme.animation_tag. On exit, the frame, border and titlebars animate, but the application’s pixels disappear on the first frame.
Growing entrances and shrinking exits on clients, tag changes and a popup, shown with different durations.
Scale
Three popups enter and leave together, with 0.8 seconds for each transition. The labels stay fixed so you can compare the effects.
Show Lua
Save this as /tmp/animation-comparison.lua, then run it in SomeWM 2.1:
somewm-client eval 'dofile("/tmp/animation-comparison.lua")'
The demo opens on your focused screen, runs twice, and closes itself. It uses your theme's background, foreground and font family. Animations must be enabled.
local options = ... or {}
local awful = require("awful")
local beautiful = require("beautiful")
local gears = require("gears")
local wibox = require("wibox")
assert(require("somewm.animation").enabled, "Enable SomeWM animations first")
if _G.somewm_animation_comparison then _G.somewm_animation_comparison.stop() end
local target = awful.screen.focused()
if options.output then
target = assert(output.get_by_name(options.output), "Output not found").screen
end
local bg, fg = beautiful.bg_normal or "#282828", beautiful.fg_normal or "#ebdbb2"
local family = (beautiful.font or "sans 10"):gsub("%s+%d+%.?%d*$", "")
local function text(value, size, color, bold)
return wibox.widget {
markup = '<span foreground="' .. (color or fg) .. '">'
.. gears.string.xml_escape(value) .. '</span>',
font = family .. (bold and " Bold " or " ") .. size,
align = "center",
valign = "center",
widget = wibox.widget.textbox,
}
end
local effects = {
{ name = "Fade", enter = "fade", color = "#d79921" },
{ name = "Scale", enter = "scale", color = "#83a598" },
{ name = "Scale + fade", enter = { "scale", "fade" }, color = "#b8bb26" },
}
local state = { running = true, completed = false, timers = {}, cards = {}, screen = target }
_G.somewm_animation_comparison = state
function state.stop()
for _, timer in ipairs(state.timers) do timer:stop() end
for _, card in ipairs(state.cards) do card.animation = false; card.visible = false end
if state.board then state.board.visible = false end
state.running = false
end
local function later(seconds, callback)
local timer = gears.timer.start_new(seconds, function()
if not state.running then return false end
local ok, err = pcall(callback)
if not ok then state.error = tostring(err); state.stop() end
return false
end)
state.timers[#state.timers + 1] = timer
end
local columns = wibox.layout.flex.horizontal()
columns.spacing = 24
local slots = {}
for i, effect in ipairs(effects) do
slots[i] = wibox.widget { forced_height = 200, widget = wibox.widget.textbox }
columns:add(wibox.widget {
text(effect.name, 20, effect.color, true),
slots[i],
spacing = 12,
layout = wibox.layout.fixed.vertical,
})
end
local phase = text("Same duration. Same popup.", 16)
state.board = awful.popup {
screen = target, visible = true, ontop = true, input_passthrough = true,
bg = bg, fg = fg, minimum_width = 1004, maximum_width = 1004,
placement = awful.placement.centered,
widget = {
{
text("Three ways to appear", 27, fg, true),
text("Fade, scale, and both together · 0.8 seconds", 16),
columns,
phase,
spacing = 20,
layout = wibox.layout.fixed.vertical,
},
margins = 28,
widget = wibox.container.margin,
},
}
local function build_cards()
local board = state.board
if board.width ~= 1004 or board.height < 300 then return false end
local boxes = {}
for i, slot in ipairs(slots) do
local x = 28 + (i - 1) * 324 + 150
for y = 0, board.height - 1, 8 do
for _, hit in ipairs(board:find_widgets(x, y)) do
if hit.widget == slot then boxes[i] = hit; break end
end
if boxes[i] then break end
end
if not boxes[i] or boxes[i].height < 200 then return false end
end
state.geometry = board:geometry()
for i, effect in ipairs(effects) do
local box = boxes[i]
state.cards[i] = awful.popup {
screen = target, visible = false, ontop = true, input_passthrough = true,
placement = awful.placement.top_left,
offset = {
x = board.x - target.geometry.x + box.x + (box.width - 270) / 2,
y = board.y - target.geometry.y + box.y + (box.height - 170) / 2,
},
minimum_width = 270, maximum_width = 270,
minimum_height = 170, maximum_height = 170,
bg = bg, fg = fg, shape = gears.shape.rounded_rect,
border_width = 2, border_color = effect.color,
widget = {
{
text("Hello, SomeWM", 21, fg, true),
text("A real popup", 16),
spacing = 8,
layout = wibox.layout.fixed.vertical,
},
valign = "center", halign = "center",
widget = wibox.container.place,
},
animation = {
visible = { duration = 0.8, enter = effect.enter, exit = effect.enter },
},
}
end
return true
end
local function show(visible, label)
phase.text = label
for _, card in ipairs(state.cards) do card.visible = visible end
end
local function cycle(number)
state.phase = "enter"
show(true, "Entering together")
later(2.2, function()
state.phase = "exit"
show(false, "Leaving together")
end)
later(4, function()
if number < 2 then
cycle(number + 1)
else
state.phase = "final"
show(true, "Same duration. Same popup.")
later(2.2, function() state.completed = true; state.stop() end)
end
end)
end
local attempts = 0
local ready = gears.timer.start_new(0.05, function()
attempts = attempts + 1
local ok, built = pcall(build_cards)
if not ok or attempts > 100 then
state.error = ok and "Popup layout did not settle" or tostring(built)
state.stop()
return false
end
if not built then return true end
state.ready = true
state.phase = "ready"
later(1.2, function() cycle(1) end)
return false
end)
state.timers[#state.timers + 1] = ready
return "Animation comparison opened on screen " .. target.index
Download the Lua file. To stop early:
somewm-client eval 'if somewm_animation_comparison then somewm_animation_comparison.stop() end'
scale transforms the surface, border and titlebars together. Its directional variants keep the named edge’s midpoint fixed. Layout and configure sizes stay unchanged, so neighbours stay in place and clients need not redraw. Pointer events follow the scaled image, including mouse::enter.
ruled.client.append_rule {
rule = { class = "mpv" },
properties = { animation = { appear = "scale_top", disappear = "scale_top" } },
}
During a transition, somewm-client clay tree reports both layout and display geometry. In scale 0.715333 anchor top presented 29,32 146x161 box 0,32 204x225, presented is the scaled image and box is the layout slot.
A popup can scale and fade in together and scale out from its bottom edge:
local popup = awful.popup {
widget = wibox.widget.textbox("hello"),
placement = awful.placement.centered,
animation = { visible = { duration = 1, enter = { "scale", "fade" }, exit = "scale_bottom" } },
}
popup.visible = true
The dump reports both fade_alpha and scale; they complete together. grow and scale conflict, so only the first effect is used.
Wiboxes and popups
A wibox or awful.popup animates its visibility:
local popup = awful.popup {
widget = require("wibox").widget.textbox("hello"),
animation = { visible = { duration = 0.2, enter = "fade", exit = "fade" } },
}
enter and exit take the same words and lists as a client. Setting visible then eases the box in or out. A wibox leaving the screen keeps its box and border for the duration, and its shadow goes on the first frame.
A popup enters and exits using slide_left, slide_right, slide_up and slide_down.
Widgets
Set widget.animation to a table of properties and durations in seconds:
local box = wibox.container.background()
box.animation = { bg = 0.3 }
box.bg = "#ff0000" -- eases over 0.3 s once the box has been drawn
A widget's first frame never animates, so the change eases only once the widget has been drawn with animation set.
Properties that animate:
| Widget | Properties |
|---|---|
wibox.container.background | bg, border_color, border_width |
| any widget | forced_width, forced_height |
wibox.widget.progressbar | value |
wibox.widget.slider | value |
Any other property in the table warns once and that property snaps. On a wibox.container.background whose shape is not a rounded rectangle, bg, border_color and border_width snap and warn once. A widget's position inside its layout always snaps, so a spacing change moves it in one frame while its own size eases.
For a single change:
box:animate { bg = "#ff0000", duration = 0.3 }
It sets each named property and eases it once; the next plain assignment snaps again.
Three widgets change size and colour at different speeds. The heart then grows and shrinks twice to make a gentle pulse.
When animation is disabled, :animate{} still applies its values immediately.
Each output keeps 200 transition records, including configured records that are currently idle. When the budget is exhausted, the output warns once and the remaining nodes snap.
Updating a policy
Clients, wiboxes, popups and widgets copy .animation on assignment and access. Edit a copy and assign it back to apply changes:
local policy = box.animation or {}
policy.bg = 0.5
box.animation = policy
Assigning an edited policy notifies property::animation observers and schedules the update. Editing the original table or a returned copy alone leaves the stored policy unchanged.
Disabling animation
Assign .animation = false to disable an object and its descendants, including a client’s titlebar widgets. This overrides descendant policies. Assign nil to remove the override; theme defaults and descendant policies then apply, unless an ancestor still disables them.
Disable all transitions with:
require("somewm.animation").enabled = false
Active transitions finish on the next rendered frame, including retained exits. Setting enabled = true enables future changes; it does not restart completed transitions or alter stored policies. A running mousegrabber also suppresses transitions during a drag.
Clients in the Tree for the frame these transitions move.