RRUI-006 · Pagination · Alignment · Mobile

Pagination alignment overflows the container on mobile

On a narrow viewport, rui_pagination with align: :left | :center | :right | :between clips out of its parent. Center and right look especially wrong: controls spill off the opposite edge instead of sitting inside the card. Visible on the RapidRails docs Alignment section in mobile mode.

Open gem 0.58.1 2026-08-13 repro: rapidrails.cc Pagination → Alignment

Docs say page numbers hide on mobile. USAGE: “Page numbers are hidden … Shows ‘Page X of Y’ instead.” In practice the numbered series (current page + ellipsis, and often the other links) stays in flow and the mobile label is added, so the inner nav row is wider than the card. justify-* then aligns that overflowing row as a unit.

Summary

Component RapidRailsUI::Pagination::Component
Surface Docs Alignment examples; any host card narrower than the numbered row (~phone width)
Trigger variant: :numbered (default) + any align: + viewport / parent < sm
Symptom :left / :between clip on the right; :center clips both sides; :right clips on the left

Repro

  1. Open RapidRails Pagination docs → Alignment.
  2. Switch the browser to a phone width (DevTools mobile, ~375px).
  3. Compare the four examples: align: :left, :center, :right, :between.
<%= rui_pagination(current_page: 3, total_pages: 8, total_count: 80, align: :center) %>
  1. Actual: Numbered buttons + “Page 3 of 8” sit in one nowrap row wider than the card. Center/right overflow the opposite edge. Next/prev or page 8 get clipped.
  2. Expected: Controls stay inside the parent. On mobile: prev / “Page X of Y” / next (no numbered series). Alignment applies within the card, not past it.

Evidence (0.58.1)

1. Mobile hide is only on inactive page links

<% page_series.each do |item| %>
  <% if item == :gap %>
    <span class="…ellipsis…">&hellip;</span>          <%# never hidden %>
  <% elsif item == current_page %>
    <span class="…active button…"><%= item %></span>   <%# never hidden %>
  <% else %>
    <%= link_to item, …, class: "#{button_classes} #{responsive_hide_mobile_classes}" %>
  <% end %>
<% end %>

<span class="<%= responsive_show_mobile_classes %> …">
  Page <%= current_page %> of <%= total_pages %>
</span>

RESPONSIVE_HIDE_MOBILE = "hidden sm:inline-flex" is concatenated onto buttons that already include inline-flex from BUTTON_BASE_CLASSES — no tw_merge. Current page + ellipsis stay visible; the mobile label is added on top.

2. Inner nav cannot shrink or wrap

NAV_CLASSES = "flex items-center gap-1"
WRAPPER_BASE_CLASSES = "flex items-center gap-4 flex-wrap"
ALIGNMENT_CLASSES = {
  left: "justify-start",
  center: "justify-center",
  right: "justify-end",
  between: "justify-between"
}

The numbered row is a flex child with default min-width: auto and no flex-wrap / max-w-full / min-w-0. It will not shrink below its content width.

3. Why each align looks different

The wrapper is a full-width flex container. Alignment moves the overflowing child as a block:

Fix in the gem

  1. Honour the documented mobile collapse: hide the entire page_series below sm (current page + gaps included). Keep prev / “Page X of Y” / next only.
  2. tw_merge display utilities so hidden wins over inline-flex on the same node.
  3. Constrain the inner nav: min-w-0 max-w-full flex-wrap (or overflow-x-auto if a single row must stay).
  4. Keep wrapper w-full min-w-0 so justify-* runs inside the parent, not past it.
  5. Regression: render numbered pagination at a <640px width for each align:; assert no descendant overflows the nav box, and that inactive page links + ellipsis are not visible.
Minimal gate for the series (conceptually):
<div class="hidden sm:contents">  <%# or per-item hidden sm:inline-flex, merged %>
    <% page_series.each … %>
  </div>
  <span class="inline-flex sm:hidden">Page X of Y</span>

Done when