💫 v0.24.1 — Motion Primitives & Consistency Polish
A point release that pulls the Motion system forward from v0.25. It adds a native, zero-dependency MotionComponent for reveal animations and re-plumbs five existing components — Tooltip, Dropdown, Password Requirements, Autocomplete, and Dialog — to share the same motion tokens, so entrances feel consistent across the library. Rounding it out: a way to push single table rows over Turbo Streams without drifting from your table markup, a custom dropdown toggle slot, and a batch of fixes from real-world use. No breaking API changes.
What's New
Motion — midwest_motion
Native, zero-dependency reveal animations. MotionComponent wraps content and animates it into place with one or more of five primitives — fade, slide, scale, rotate, and blur — passed to animation: as a single value or an array. Each primitive drives an independent CSS property (opacity, the individual translate/scale/rotate transform properties, and filter), so compounds like [:slide, :blur] layer cleanly instead of fighting over transform.
Three triggers cover the common cases:
trigger: :reveal(default) — animates in the first time it scrolls into view, via a tinymidwest-motionStimulus controller (threshold:,once:).trigger: :load— plays once on first render through a pure-CSS@starting-styleentry. No controller, no JavaScript.trigger: :hover— a reversible reveal that plays while the element is hovered, focused, or shown as a popover.
duration: / easing: resolve to the shared --duration-* / --ease-* design tokens (or take raw CSS), and distance: maps a size keyword onto the spacing scale. Everything honors prefers-reduced-motion, and the :reveal / :hover hidden states are additionally gated on scripting: enabled so no-JS visitors never see a flash of hidden content.
<%= midwest_motion do %>Fades up into view<% end %><%= midwest_motion(animation: [:slide, :blur], direction: :up, delay: 100) do %> Slides up while sharpening from a blur, 100ms after entering the viewport<% end %>App-wide motion defaults — Midwest.configure
Set the "design feel" of motion once from an initializer. Keywords resolve to design tokens (or pass raw CSS), and they cascade onto the page through midwest_body_styles while any per-call component option still wins locally.
# config/initializers/midwest.rbMidwest.configure do |config| config.motion.duration = :slow # => var(--duration-slow) config.motion.distance = :xl # => var(--space-2xl) config.motion.easing = :emphasized # => var(--ease-emphasized)endWhat Changed
Motion-driven entrances across the library
Five existing components now render their entrances through the shared motion system, so they inherit the same Midwest.config.motion defaults and feel consistent. All fall back gracefully — mobile bottom-sheets and reduced-motion are unaffected.
TooltipComponent— entrance driven by the shared motion system (animation:/direction:/duration:/distance:, with config-aware defaults).
DropdownComponent— the desktop menu entrance is now rendered throughmidwest_motion(trigger: :hover); the mobile bottom-sheet is unchanged. Applies to everyDropdownMenuComponentconsumer — dropdown, breadcrumb dropdown, split button, and nested submenus. This release also adds a custom toggle slot:with_toggle(variant: :tab, icon: :calendar) { "Your events" }renders a realmidwest_buttonas the trigger and forwardsvariant:/icon:/size:/class:straight through, so you style the trigger without nesting a second focusable control inside it.
PasswordRequirementsComponent— the anchored (popover) panel's entrance is driven by the shared motion system (trigger: :hover); inline and static modes are unchanged.
Form::AutocompleteComponent— the desktop dropdown entrance is driven by the shared motion system via an opt-inmotion:flag on the sharedPopoverComponent; the mobile bottom-sheet is unchanged.
DialogComponent— the inner.dialog-contentis now rendered throughmidwest_motion(trigger: :load), so dialog content animates in on each open. The native<dialog>keeps its backdrop, top-layer, and focus behavior, and the panel variant keeps its right-edge slide.
PopoverComponent — opt-in motion
PopoverComponent gains a motion: flag (off by default). When enabled, the popover is rendered through midwest_motion (trigger: :hover) so its desktop entrance is driven by the shared motion tokens. Other popover consumers (date/time pickers, confirmation) keep the flag off.
TablePresenter — sync single rows without drift
Pushing a per-row update over Turbo Streams no longer means hand-writing (and slowly drifting from) a row partial. Two new entry points reuse the presenter's own column definitions so a single row matches midwest_table_from exactly:
midwest_table_row_from(presenter, row)— a view helper for controller-driven Turbo Stream responses, where a view context resolvesrow_hrefand any route helpers in cell blocks.presenter.row_component(row)— for model-side broadcasts with no view context (click-nav attributes are omitted and reapplied on the next full render).
<%# controller: replace just the updated row %>render turbo_stream: turbo_stream.replace( dom_id(@user), midwest_table_row_from(@table, @user))ButtonComponent — correct type inside forms
Buttons now emit type="button" by default and type="submit" / type="reset" when rendered as: :submit / as: :reset. Previously a plain button inside a <form> could accidentally submit it. Pass as: :submit to opt into submission.
Polish from real-world use
TabComponent— fixed a duplicated icon:button_bodyalready renders theicon:option, so the label no longer embeds it a second time.- Command Palette, Live Summary, card options — small CSS and markup refinements surfaced while building with the library.
What Was Removed
- Nothing was removed this release. No component options, slots, or helpers were dropped, and there are no breaking API changes.
Dev Process
- Previews — a new
MotionComponentPreview(playground plusanimations,directions,compound,on_load, andreplayexamples), and motionanimation:/direction:/duration:controls added to the Tooltip, Dropdown, Password Requirements, and Autocomplete playgrounds. - Configuration — a new
Midwest::Configuration/Midwest::Motionlayer backsMidwest.configure, with the sharedMidwest::Internal::Motionableconcern feeding motion options into every consuming component. Documented in the install guide and a newrails_features/12_motion_configpage. - MCP guidance — new
component_guidance.ymlentries: an icon anti-pattern (don't wrapmidwest_iconin a styling span), the button submit-type gotcha, and the dropdown custom-toggle / global-label patterns. - Tests — new suites for
MotionComponent, theMotionableconcern,Midwest::Configuration/Midwest::Motion,TableRowComponent/TablePresenterrow sync, the dropdown toggle slot, plus coverage for the dialog, tooltip, popover, and autocomplete motion paths. - Roadmap — Motion Primitives and its five motion-consistency improvements are marked complete, shipped a milestone early in this point release.