acts_as_list: ordered records in Rails, then and now

Tasks in a to-do list, chapters in a book, slides in a deck: many models need a user-defined order. acts_as_list has handled that since before Rails 2, and it is still maintained.
From Rails core to a gem
In Rails 1.x, acts_as_list was part of Active Record itself, alongside acts_as_tree and acts_as_nested_set. Rails 2.0 extracted these features into separate plugins to keep the core smaller. acts_as_list then lived as a plugin and, from the Rails 3 era onwards, as a gem. Like many plugins of the time, it went through a period around the Rails 3 release when users were unsure which fork worked with which version, before the main repository consolidated the fixes.
The project is now maintained in the brendon/acts_as_list repository. According to RubyGems.org, the latest release is 1.2.6, from October 2025, and it requires Active Record 6.1 or newer.
How it works
You add an integer position column and declare the list, optionally scoped to a parent:
class TodoItem < ApplicationRecord
belongs_to :todo_list
acts_as_list scope: :todo_list
end
class TodoList < ApplicationRecord
has_many :todo_items, -> { order(position: :asc) }
end
New records are added at the bottom of their list by default (configurable with add_new_at: :top). The gem provides methods to reorder items, each of which updates the positions of the affected neighbours:
move_higher,move_lower,move_to_top,move_to_bottominsert_at(3)to place an item at a given positionfirst?,last?,higher_item,lower_itemfor navigationremove_from_list, which closes the gap left by an item
Scopes can be a single association, several columns, or a custom SQL condition, so one table can hold many independent lists. Destroying an item shifts the items below it up by one.
Things to watch
Concurrency. Reordering updates many rows. If two requests move items in the same list at the same time, positions can end up duplicated or with gaps. The project's README discusses this and the trade-offs of adding a unique index on the scope and position columns, which is harder than it sounds because the gem temporarily shifts positions during updates. For lists that many users edit at once, consider wrapping reorders in a lock on the parent record, or use one of the alternatives below.
Callbacks and bulk operations. acts_as_list maintains positions through Active Record callbacks. Methods that skip callbacks, such as update_all, delete_all or insert_all, leave the list inconsistent; you then need to renumber it yourself.
Large lists. Moving an item from the bottom to the top of a list of thousands updates thousands of rows. For long, frequently reordered lists, a sparse ranking scheme performs better.
Alternatives
- positioning, from the same maintainer, is a newer gem designed with lessons learned from acts_as_list, including stronger handling of concurrent updates. It is still in the 0.x series (0.4.8 in March 2026), so read its README for current limitations before choosing it.
- ranked-model stores sparse integer ranks, so most moves update a single row, with occasional rebalancing. The latest release is 0.5.0, from October 2026.
- For short lists that change rarely, a plain integer column and a few lines of code are enough; renumber the whole list in one transaction when it changes.
Drag-and-drop reordering
The usual interface for ordered lists is drag and drop. With Hotwire, a small Stimulus controller around a sortable JavaScript library sends the new position of the moved item to a member route, for example PATCH /todo_items/:id/move with a position parameter, and the controller calls insert_at. Send the target position rather than the whole new order: it is smaller, and it lets the gem compute the other changes inside a single transaction.
Should you use it?
For most ordered lists in a Rails application, acts_as_list remains a sound, maintained choice with a long track record. Choose positioning or ranked-model if you expect heavy concurrent reordering or very long lists. As with any gem, check its CI against your Rails version and run your own tests around reordering before an upgrade; our guide to evaluating a gem explains what to look for.
Category guidesPagination
Comparing Rails pagination gems — Pagy, Kaminari and will_paginate — and when to use offset or keyset pagination for large tables and infinite scrolling.
Plugin historiesPaperclip
What Paperclip did, why thoughtbot deprecated it in 2018, the kt-paperclip fork, and how to migrate Paperclip attachments to Active Storage or Shrine.
Plugin historiesGeokit-rails
What geokit-rails and the Geokit gem do, their release status in 2026, and when to use Geocoder or PostGIS instead for geocoding and location queries in Rails.