Skip to main content
Version: 2.1

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.

Layout resize and swap.

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.

TriggerWhenTheme durationTheme entrance
appearthe client mapsanimation_appear_durationanimation_appear
disappearthe client closesanimation_disappear_durationanimation_disappear
tagthe client enters or leaves the view with a tag switchanimation_tag_durationanimation_tag; default grow
focusthe border colour changes with focusanimation_focus_duration
maximizemaximized or fullscreen togglesanimation_maximize_duration
floatingfloating togglesanimation_floating_duration

theme.animation_duration is the duration for every trigger that has none of its own, layout included.

Maximise, restore, float and tile.

The same window moves between maximised, tiled and floating states, with a different duration for each transition.

Choose an entrance effect:

EffectBehaviour
growIncreases the frame’s layout size from zero.
scale, scale_top, scale_bottom, scale_left, scale_rightScales the rendered image around its centre or an edge midpoint; layout size stays unchanged. See Scale.
slide_left, slide_right, slide_up, slide_downSlides from that screen edge.
fadeIncreases 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 (grow or one of the scale words) and fade combine, 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. collapse is another name for grow.

{ "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.

Fade.

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.

Grow and shrink.

Growing entrances and shrinking exits on clients, tag changes and a popup, shown with different durations.

Scale​

Fade, scale, and both together.

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.

Directional slides.

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:

WidgetProperties
wibox.container.backgroundbg, border_color, border_width
any widgetforced_width, forced_height
wibox.widget.progressbarvalue
wibox.widget.slidervalue

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.

Widget timing and pulse.

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.