Motion Config
Overview
Midwest's motion primitives — the midwest_motion component and everything
built on it, such as tooltips — animate against a small set of motion design
tokens: duration, easing, delay, slide distance, scale, rotate, and blur.
Each of these has a system default expressed as a CSS custom property
(--motion-default-*) shipped in the stylesheet. You can reshape the motion
"feel" across an entire application from a single initializer — no CSS override
files, no per-component wiring — while any per-call option on a component still
wins locally.
The flow is:
- You set defaults in an initializer with
Midwest.configure. midwest_body_styles(already on your<body>after install) renders them as--motion-default-*custom properties.- Because those declarations sit on
<body>, they cascade to every element on the page, overriding the:rootvalues the stylesheet ships.
Configuring from an initializer
# config/initializers/midwest.rbMidwest.configure do |config| # Keywords resolve to the design tokens... config.motion.duration = :slow # => var(--duration-slow) config.motion.distance = :xl # => var(--space-2xl) config.motion.easing = :emphasized # => var(--ease-emphasized) # ...or pass a raw CSS value for anything off the scale. config.motion.duration = "800ms" config.motion.scale = 0.9 config.motion.rotate = "-10deg" config.motion.blur = "1rem"endOnly the properties you set are emitted; everything else keeps its shipped
default. Setting nothing is a no-op — midwest_body_styles renders an empty
string.
Bridgetown: the exact same
Midwest.configureblock works. Place it inconfig/initializers.rb.
Available properties
config.motion exposes one attribute per motion token. duration, easing,
delay, and distance accept the same keywords as the midwest_motion
component options (resolved through Midwest::Motion), or a raw CSS value.
scale, rotate, and blur are raw CSS values.
| Property | Keywords | System default | Custom property |
|---|---|---|---|
duration |
:fast :base :slow :slower |
var(--duration-base) |
--motion-default-duration |
easing |
:standard :in :out :emphasized |
var(--ease-standard) |
--motion-default-easing |
delay |
a number (ms) or raw CSS | 0ms |
--motion-default-delay |
distance |
:sm :default :lg :xl |
var(--space-lg) |
--motion-default-distance |
scale |
raw (e.g. 0.9) |
0.92 |
--motion-default-scale |
rotate |
raw (e.g. "-10deg") |
-6deg |
--motion-default-rotate |
blur |
raw (e.g. "1rem") |
0.5rem |
--motion-default-blur |
Keyword durations, easings, and distances map onto the underlying design tokens, so an override composes with the token layer rather than replacing it.
How it reaches the page
The install generator adds midwest_body_styles to your layout's <body> tag:
<body style="<%= midwest_body_styles %>">With the configuration above, that renders:
<body style="--motion-default-duration: var(--duration-slow); --motion-default-distance: var(--space-lg); --motion-default-easing: var(--ease-emphasized);">Every midwest_motion element resolves its transition from these properties, so
one initializer restyles motion everywhere — including tooltips, which are built
on midwest_motion.
Scoping to a subtree
Because these are ordinary inherited custom properties, you are not limited to
<body>. Redefine any of them on a wrapper element to scope a different motion
feel to one region of the page:
<section style="--motion-default-duration: var(--duration-slower);"> <%# midwest_motion / tooltips in here animate slower %></section>Structural defaults: animation and direction
Two motion defaults can't ride the CSS cascade, because they select CSS
classes rather than custom properties: animation (which primitives play —
:fade, :slide, :scale, :rotate, :blur, or an array) and direction
(:up :down :left :right). You can still set them in the initializer;
components read them in Ruby as the default when the option is left unset.
Midwest.configure do |config| config.motion.animation = %i[slide blur] # the default tooltip entrance config.motion.direction = :up # optional; see belowendThe tooltip, the dropdown menu, the anchored password requirements
panel, the autocomplete dropdown, and the dialog (its inner content) are
the current consumers. (The autocomplete opts the shared PopoverComponent into
motion with motion: true; the date/time pickers and confirmation leave it off.
The dialog animates its first nested element via trigger: :load while the
native <dialog> keeps the backdrop and focus.) When you don't pass animation:
or direction:, each falls back to these config values:
animation— per-callanimation:→config.motion.animation→ the component's built-in default (slide + blurfor the tooltip,slidefor the dropdown menu).direction— per-calldirection:→config.motion.direction→ a value derived from the component's placement, so the surface animates toward its anchor. The tooltip derives it fromplacement:(placed above, it slides down into place); the dropdown menu derives it fromalign:(aligned below the toggle, it slides up into place). Leaveconfig.motion.directionunset to keep this placement-aware behavior.
<%# Tooltip: uses config.motion.animation, direction derived from placement (:down) %><%= midwest_tooltip(tip: "Saved", placement: :top) %><%# Dropdown: same options; desktop entrance follows the motion system %><%= midwest_dropdown(title: "Menu", align: "bottom-start") %><%# Per-call options still win %><%= midwest_tooltip(tip: "Saved", placement: :top, animation: :fade, direction: :up) %>The dropdown menu applies motion to its desktop entrance only; on narrow screens it keeps its bespoke full-height bottom-sheet slide-up.
The demo configures these in config/initializers/midwest.rb — tweak it and
reload to see every tooltip and dropdown change at once.
Precedence
From lowest to highest, the value that wins for a given motion element is:
- The stylesheet
:rootdefault (--motion-default-*). - Your app-wide
Midwest.configurevalue (rendered onto<body>). - A closer ancestor override (a wrapper element setting the property).
- A per-call component option, which is emitted inline on the element itself and always wins:
<%# Inherits the app-wide defaults %><%= midwest_motion(animation: :slide) { "Default feel" } %><%# Overrides duration and distance just for this element %><%= midwest_motion(animation: :slide, duration: :fast, distance: :sm) { "Snappier" } %><%# Tooltips forward the same motion options %><%= midwest_tooltip(tip: "Saved", duration: :slow, distance: :lg) %>This means you set the house style once in the initializer and only reach for a per-call option when a specific element needs to differ.
Reading the configuration
The configuration is a plain, memoized object, handy in a console or a spec:
Midwest.config.motion.duration # => :slowMidwest.config.motion.to_custom_properties# => { "--motion-default-duration" => "var(--duration-slow)",# "--motion-default-distance" => "var(--space-lg)",# "--motion-default-easing" => "var(--ease-emphasized)" }# Note: animation/direction are structural — they never appear here.Midwest.reset_config! # drops overrides (test helper)