Ship better software, faster

We help startups and scale ups build, modernize, and scale their apps - combining senior engineering with Ruby, Python, and Node to deliver measurable outcomes.

Crafting user experiences
Building scalable products

The challenges we solve every week

Experience the advantage of an all-inclusive project solution, where excellence, speed, and responsiveness converge to ensure the highest quality outcome.

  • Legacy apps slowing you down

    Modernization & upgrades with zero downtime

  • Backlog bigger than your team

    Elastic feature pods that integrate seamlessly

  • Want to add AI features

    Build, integrate, and deploy data-powered APIs fast

  • App performance issues

    Profiling and optimization across Ruby, Python, and Node

What We Do

Every project needs the right balance between delivery speed, architectural acumen and lasting quality. Infinity Loop brings a flexible, senior only engineering model that adapts to what your product actually needs. Ruby, Python, Node: we pick the stack that fits. Development services crafted to deliver value.

burger illustration

Modernization &
Upgrades

Refactor, upgrade, and stabilize existing apps without downtime.

Example: Rails 5 → Rails 7 migration completed in 6 weeks with 40% faster deploys.

Read more
burger illustration

Performance & Observability

Detect bottlenecks, optimize load times, and build reliability into every release.

Example: Reduced p95 latency by 63% across multi-stack API workloads.

Read more
burger illustration

AI-Enabled MVPs & Features

Build intelligent product features with Python/FastAPI and integrate them into Rails or Node apps seamlessly.

Example: Integrated ML-powered recommendations into SaaS product within 3 sprints.

Read more
burger illustration

More features

Neque Dolor, fugiat non cum doloribus aperiam voluptates nostrum.

Read more

How We Work

Delivery That’s Predictable and Proven. No jargons. Just clarity, accountability, and measurable progress.

Scope Clearly

Define success, risks, and metrics before we start.

icon illustration

Requirements Validation

Ensure goals, risks, and success metrics are fully understood and agreed upon from the start.

icon illustration

Clear Roadmap

Define deliverables, boundaries, and priorities so everyone knows what’s in-scope and what’s not.

scope clearly illustration
scope clearly illustration
scope clearly illustration

We’re Engineers Who Care About Results.

Infinity Loop is a UK-based software development partner led by senior engineers who’ve scaled products, rebuilt legacy systems, and delivered under tight deadlines.

We work asynchronously, communicate transparently, and measure what matters, so your roadmap actually moves.

Get started

Case Studies

Recent Work & Measurable Wins

Thoughts on design, business and indie-hacking

artificial-intelligence

How to Use Generative AI to Accelerate Software Development Without Sacrificing Quality

Software teams everywhere are adopting generative AI to speed up development — but many struggle with the balance between **velocity** and **quality**. While AI can dramatically reduce coding time, it can also introduce hidden risks when used without the right guardrails. In this article, we’ll break down practical, real-world ways engineering teams can integrate AI into their daily workflow to unlock productivity *and* preserve (or even improve) code quality. ## Why AI Is Transforming Software Engineering Generative AI tools are reshaping the development lifecycle by helping teams: - Generate boilerplate code in seconds - Document complex systems faster - Detect issues earlier in the pipeline - Improve developer onboarding - Support architectural decision-making But the real unlock isn’t just coding faster, it’s enabling teams to **focus on higher yielding engineering work**, like architecture, testing, and long-term maintainability. ## Use AI to Automate Repetitive Coding Tasks Much of software development involves predictable, repetitive work: - CRUD operations - Form objects - API boilerplates - Service object scaffolding - Test stubs AI assistants can generate these instantly. ### Example Instead of hand-writing a standard Rails service object: ```ruby class ProcessPayment def initialize(order:) @order = order end def call # business logic here end private attr_reader :order end ``` You can have AI generate the structure and focus your time on the actual business logic. ## Improve Code Quality With AI Assisted Reviews AI is a powerful second set of eyes for: - spotting edge cases - identifying unsafe patterns - checking for missing tests - validating performance risks - pointing out security vulnerabilities This is especially useful in legacy systems where context is limited or documentation is outdated. > ***Tip:*** > Use AI to explain code back to you, if the explanation doesn’t match your intention, you found a potential issue. ## Supercharge Test Coverage With AI AI can help teams write more (and better) tests by: - generating initial test files - suggesting missing test cases - creating fixtures - analyzing edge paths - explaining tricky business logic in plainer language For Rails teams using RSpec or Minitest, AI can dramatically speed up test creation and provide deeper insight into what edge cases might be missing. ## Use AI for Architecture, Design & Planning AI isn’t just for code, it shines during early phase engineering work: - breaking down epics into stories - designing APIs - reviewing ERDs and schema changes - evaluating trade-offs between patterns - analyzing scalability concerns This makes refinement meetings smoother and reduces rework later. ## Document Faster Without Losing Depth Developers rarely enjoy writing documentation, but AI does an exceptional job producing: - README files - setup instructions - architecture overviews - migration guides - onboarding materials AI generated documentation ensures consistency and helps new engineers ramp up quickly. ## Apply Guardrails to Use AI Safely and Effectively To avoid quality or security pitfalls, teams should establish guidelines: - Require human-in-the-loop review - Avoid pasting sensitive data into public AI tools - Verify code with AI-generated or AI-guided tests - Store prompts and decisions for auditability - Define which tasks AI can and cannot handle AI accelerates development but engineers stay responsible. ## Final Thoughts: AI Is a Force Multiplier, Not a Replacement Used thoughtfully, generative AI becomes a powerful partner that: - accelerates delivery - reduces cognitive load - improves quality - unblocks teams faster - enhances developer happiness The future of engineering isn’t AI vs. developers **it’s AI-empowered developers outperforming everyone else.**

minute read
ruby-on-rails

How to Modernize a Legacy Ruby on Rails App with Zero Downtime

**Upgrading a legacy Ruby on Rails application** can feel like changing the engine of a moving train. Users expect stability, while engineers wrestle with outdated dependencies, brittle code, and zero downtime requirements. This guide walks through a proven approach to modernizing a Rails app safely from code auditing to deployment, without breaking production. ## Why Modernization Matters Over time, even well-built Rails applications start to slow down: - Outdated gems introduce security risks - Monolithic codebases become hard to extend - Test coverage drops as features grow - Deployments feel risky and time-consuming Modernization isn’t just about chasing the newest Rails version, it’s about **restoring developer confidence and ensuring long term scalability**. ## 1. Start with a Code Audit Before touching a single file, understand where your app stands today. ### Inventory Your Stack - Ruby version and Rails version - Database engine and adapters - Frontend framework (if any) - Background job systems (Sidekiq, Resque, etc.) - CI/CD tools and hosting environment Use tools like `bundle outdated` to identify obsolete dependencies and potential conflicts. ### Assess Dependencies Check each gem’s support window and compatibility with your target Rails version. Pay attention to: - Deprecated gems - Forked libraries with unmaintained code - Security vulnerabilities (`bundle audit` is your friend) ### Evaluate Architecture Look for: - Tight coupling between models and controllers - Overgrown ActiveRecord models - Missing service layers or presenters Document pain points, these notes become your modernization roadmap. ## 2. Strengthen Test Coverage Your tests are the **safety net** for any major refactor. ### Measure Coverage Run `simplecov` to see which files are untested. Prioritize: - Business-critical flows - Controllers that handle payments, authentication, or data mutations ### Add Missing Tests If tests are weak, start with **request specs and smoke tests** you don’t need perfect coverage, just protection around key paths. ### Automate Testing Set up CI with GitHub Actions, CircleCI, or GitLab CI. Even a minimal pipeline running `bundle exec rspec` and `rubocop` adds huge value. ## 3. Plan Incremental Refactors Modernization should be **iterative**, not a “big bang.” ### Techniques for Safe Refactoring - **Feature flags:** Gradually roll out changes to small user groups - **Service objects:** Extract logic from controllers to simplify testing - **Background migrations:** Avoid long running DB locks - **API versioning:** Introduce new endpoints without breaking existing clients Each small refactor should leave the app in a working state, the **Boy Scout Rule**: *always leave the code cleaner than you found it.* ## 4. Upgrade Rails (Step by Step) Upgrading between major Rails versions (e.g., 5 → 6 → 7) is safer than jumping directly. ### Recommended Path 1. **Lock gem versions** in `Gemfile.lock` 2. **Run tests**, fix deprecations, and clean warnings 3. **Upgrade Ruby**, then the smallest Rails version increment 4. **Test and deploy** between each step Use [railsdiff.org](https://railsdiff.org) to compare versions and see what’s changed. ## 5. Achieve Zero-Downtime Deployments The biggest modernization fear: downtime during release. ### Blue/Green or Rolling Deploys Run two environments (old and new). Deploy to the new one, verify it, then switch traffic instantly. ### Database Migration Safety - Avoid destructive changes (e.g., dropping columns) in live migrations - Use the `strong_migrations` gem to catch unsafe operations - For large tables, migrate in batches with background jobs ### Caching and Assets Precompile assets and warm caches before switching traffic. This prevents initial load spikes. ## 6. Monitor, Measure, and Iterate After upgrading, monitor performance metrics and logs closely. - Track request times, error rates, and memory usage - Use tools like Skylight, New Relic, or Datadog for visibility - Schedule regular dependency updates (monthly or quarterly) Modernization isn’t a one time project, it’s an **ongoing process of keeping technical health aligned with business goals**. ## Conclusion Modernizing a legacy Ruby on Rails app doesn’t require downtime or chaos. With careful auditing, strong tests, and incremental refactors, you can evolve your codebase confidently, while users keep enjoying a seamless experience. **Next step:** [Download the Rails Modernization Checklist →](#) ### FAQs **1. How long does a Rails upgrade take?** It depends on your codebase size and test coverage. A well tested app can upgrade in days; untested monoliths may take months. **2. How do you test database migrations safely?** Run them on staging with production data clones and use `strong_migrations` to detect locking operations. **3. Can I modernize Rails 4 directly to 7?** It’s risky. Upgrade incrementally (4 → 5 → 6 → 7) to avoid breaking dependencies.

4 minute read
ruby-on-rails

Creating Tabbed Interfaces with Hotwire Turbo in Rails

Tabbed interfaces are a common UI pattern for organizing content, but traditional implementations often require full page reloads or heavy JavaScript dependencies. With Hotwire Turbo and Stimulus.js, which come built-in with Rails 7, we can create a seamless tabbed experience with minimal JavaScript and no page refreshes. In this post, I'll show you how to build beautiful tabbed interfaces with Turbo Frames that maintain their state, support pagination within tabs, and work smoothly with Flowbite's styling. ## The Challenge with Traditional Tabs Traditional tabbed interfaces in Rails typically have one of these issues: - Full page reloads: When clicking a tab, the page refreshes, causing a jarring experience - Heavy JavaScript: Complex JavaScript libraries that can be difficult to maintain - **State management challenges:** Maintaining tab state across page reloads or nested resources Hotwire Turbo solves these issues by allowing us to update just the parts of the page that change, without relying on complex JavaScript. ## The Solution: Turbo Frames + Stimulus We'll build a tabbed interface for an Estate management application that: - Uses Turbo Frames to swap content without page refreshes - Uses Stimulus.js for minimal JavaScript to handle tab styling - Maintains consistent styling with Flowbite's utility classes - Supports pagination within each tab ## Setting Up Our Controller ```ruby # app/controllers/estates_controller.rb class EstatesController < ApplicationController include Pagy::Backend def show @estate = Estate.find(params[:id]) # Handle different tab requests @active_tab = params[:tab] || "dashboard" case @active_tab when "transactions" @pagy, @transactions = pagy(@estate.all_transactions, items: 10) when "blocks" @pagy, @blocks = pagy(@estate.blocks, items: 10) when "properties" @pagy, @properties = pagy(@estate.properties, items: 10) when "accounts" @pagy, @accounting_periods = pagy(@estate.accounting_periods, items: 10) when "expenses" @pagy, @expense_categories = pagy(@estate.expense_categories, items: 10) end respond_to do |format| format.html format.turbo_stream end end end ``` The key elements here: - We determine the active tab from the URL parameter or default to "dashboard" - We prepare paginated data only for the active tab to minimize database queries - We respond to both HTML and Turbo Stream requests ## Creating the Stimulus Controller ```javascript // app/javascript/controllers/tabs_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["tab"] connect() { this.activateCurrentTab() } activateTab(event) { const clickedTab = event.currentTarget this.updateTabStyles(clickedTab.dataset.tab) } activateCurrentTab() { const urlParams = new URLSearchParams(window.location.search) const currentTab = urlParams.get('tab') || 'dashboard' this.updateTabStyles(currentTab) } updateTabStyles(activeTabName) { this.tabTargets.forEach(tab => { const isActive = tab.dataset.tab === activeTabName // Remove all styling classes tab.classList.remove( 'text-blue-600', 'hover:text-blue-600', 'dark:text-blue-500', 'dark:hover:text-blue-500', 'border-blue-600', 'dark:border-blue-500', 'dark:border-transparent', 'text-gray-500', 'hover:text-gray-600', 'dark:text-gray-400', 'border-gray-100', 'hover:border-gray-300', 'dark:border-gray-700', 'dark:hover:text-gray-300' ) // Add appropriate classes based on active state if (isActive) { tab.classList.add( 'text-blue-600', 'hover:text-blue-600', 'dark:text-blue-500', 'dark:hover:text-blue-500', 'border-blue-600', 'dark:border-blue-500' ) } else { tab.classList.add( 'dark:border-transparent', 'text-gray-500', 'hover:text-gray-600', 'dark:text-gray-400', 'border-gray-100', 'hover:border-gray-300', 'dark:border-gray-700', 'dark:hover:text-gray-300' ) } }) } } ``` This controller: - Identifies tabs using the tab target - Updates tab styling when clicked - Reads the current tab from the URL on page load - Applies Flowbite's styling classes to active/inactive tabs ## The Main View Template Now let's create our main view with the tabs: ```ruby <%# app/views/estates/show.html.erb %> <div class="px-4 pt-4"> <%# Breadcrumb navigation %> <nav class="mb-4 flex" aria-label="Breadcrumb"> <ol class="inline-flex items-center space-x-1 md:space-x-2 rtl:space-x-reverse"> <li class="inline-flex items-center"> <a href="/" class="inline-flex items-center text-sm font-medium text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white"> <svg class="me-2.5 h-4 w-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"> <path fill-rule="evenodd" d="M11.3 3.3a1 1 0 0 1 1.4 0l6 6 2 2a1 1 0 0 1-1.4 1.4l-.3-.3V19a2 2 0 0 1-2 2h-3a1 1 0 0 1-1-1v-3h-2v3c0 .6-.4 1-1 1H7a2 2 0 0 1-2-2v-6.6l-.3.3a1 1 0 0 1-1.4-1.4l2-2 6-6Z" clip-rule="evenodd"/> </svg> Home </a> </li> <li> <div class="flex items-center"> <svg class="mx-1 h-4 w-4 text-gray-400 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m9 5 7 7-7 7"/> </svg> <a href="#" class="ms-1 text-sm font-medium text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white md:ms-2">Estates</a> </div> </li> <li aria-current="page"> <div class="flex items-center"> <svg class="mx-1 h-4 w-4 text-gray-400 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m9 5 7 7-7 7"/> </svg> <span class="ms-1 text-sm font-medium text-gray-500 dark:text-gray-400 md:ms-2"><%= @estate.name %></span> </div> </li> </ol> </nav> <h1 class="mb-4 text-2xl font-semibold text-gray-900 dark:text-white"><%= @estate.name %></h1> </div> <%# Tab Navigation %> <div class="mb-4 border-b border-gray-200 dark:border-gray-700" data-controller="tabs"> <ul class="flex flex-wrap -mb-px text-sm font-medium text-center"> <li class="me-2"> <%= link_to "Dashboard", estate_path(@estate, tab: "dashboard"), class: "inline-block p-4 border-b-2 rounded-t-lg", data: { turbo_frame: "estate_tab_content", tab: "dashboard", tabs_target: "tab", action: "click->tabs#activateTab" } %> </li> <li class="me-2"> <%= link_to "Accounts", estate_path(@estate, tab: "accounts"), class: "inline-block p-4 border-b-2 rounded-t-lg", data: { turbo_frame: "estate_tab_content", tab: "accounts", tabs_target: "tab", action: "click->tabs#activateTab" } %> </li> <li class="me-2"> <%= link_to "Transactions", estate_path(@estate, tab: "transactions"), class: "inline-block p-4 border-b-2 rounded-t-lg", data: { turbo_frame: "estate_tab_content", tab: "transactions", tabs_target: "tab", action: "click->tabs#activateTab" } %> </li> <li class="me-2"> <%= link_to "Blocks", estate_path(@estate, tab: "blocks"), class: "inline-block p-4 border-b-2 rounded-t-lg", data: { turbo_frame: "estate_tab_content", tab: "blocks", tabs_target: "tab", action: "click->tabs#activateTab" } %> </li> <li class="me-2"> <%= link_to "Properties", estate_path(@estate, tab: "properties"), class: "inline-block p-4 border-b-2 rounded-t-lg", data: { turbo_frame: "estate_tab_content", tab: "properties", tabs_target: "tab", action: "click->tabs#activateTab" } %> </li> <li class="me-2"> <%= link_to "Expense Categories", estate_path(@estate, tab: "expenses"), class: "inline-block p-4 border-b-2 rounded-t-lg", data: { turbo_frame: "estate_tab_content", tab: "expenses", tabs_target: "tab", action: "click->tabs#activateTab" } %> </li> </ul> </div> <%# Tab Content %> <%= turbo_frame_tag "estate_tab_content" do %> <div class="px-4 pt-2 rounded-lg bg-gray-50 dark:bg-gray-800"> <% case @active_tab %> <% when "dashboard" %> <%= render "main_tab" %> <% when "accounts" %> <%= turbo_frame_tag "accounts_table" do %> <%= render "accounting_tab", accounting_periods: @accounting_periods %> <% end %> <% when "transactions" %> <%= turbo_frame_tag "transactions_table" do %> <%= render "transactions_tab", transactions: @transactions %> <% end %> <% when "blocks" %> <%= turbo_frame_tag "blocks_table" do %> <%= render "blocks_tab", blocks: @blocks %> <% end %> <% when "properties" %> <%= turbo_frame_tag "properties_table" do %> <%= render "properties_tab", properties: @properties %> <% end %> <% when "expenses" %> <%= turbo_frame_tag "expenses_table" do %> <%= render "expenses_tab", expense_categories: @expense_categories %> <% end %> <% end %> </div> <% end %> ``` Notice the structure: - A parent div with the Stimulus controller - Tab links with data attributes for Stimulus - A Turbo Frame that will be updated when tabs are clicked - Nested Turbo Frames for each tab's content (for pagination) ## Creating Tab-Specific Templates Let's look at an example tab template with pagination: ```ruby <%# app/views/estates/_transactions_tab.html.erb %> <div class="mb-4 overflow-x-auto md:mb-6 relative min-h-[590px]"> <h2 class="mb-4 text-xl font-bold leading-none text-gray-900 dark:text-white">Transactions</h2> <table class="w-full text-left text-sm text-gray-500 dark:text-gray-400"> <thead class="bg-gray-50 text-xs uppercase text-gray-500 dark:bg-gray-800 dark:text-gray-400"> <tr> <th scope="col" class="p-4"> <div class="flex items-center"> <input id="checkbox-all" type="checkbox" class="h-4 w-4 rounded-sm border-gray-300 bg-gray-100 text-primary-600 focus:ring-2 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-primary-600"/> <label for="checkbox-all" class="sr-only">checkbox</label> </div> </th> <th scope="col" class="px-4 py-3 font-semibold">Reference</th> <th scope="col" class="px-4 py-3 font-semibold">Entity</th> <th scope="col" class="px-4 py-3 font-semibold">Description</th> <th scope="col" class="px-4 py-3 font-semibold">Amount</th> <th scope="col" class="px-4 py-3 font-semibold">Date</th> <th scope="col" class="px-4 py-3 font-semibold">Status</th> <th scope="col" class="px-4 py-3 font-semibold">Actions</th> </tr> </thead> <tbody> <%= render @transactions %> <%# Placeholder rows for consistent height %> <% if @transactions.length < 10 %> <% (10 - @transactions.length).times do %> <tr class="border-b hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-gray-800 border-gray-200 h-[53px]"> <td class="w-4 px-4 py-3"></td> <td class="whitespace-nowrap px-4 py-3"></td> <td class="whitespace-nowrap px-4 py-3"></td> <td class="whitespace-nowrap px-4 py-3"></td> <td class="whitespace-nowrap px-4 py-3"></td> <td class="whitespace-nowrap px-4 py-3"></td> <td class="whitespace-nowrap px-4 py-3"></td> <td class="px-4 py-3"></td> </tr> <% end %> <% end %> </tbody> </table> </div> <%# Pagination %> <div class="pagination-container px-4"> <div class="pagination-info"> Showing <%= @pagy.from %>-<%= @pagy.to %> of <%= @pagy.count %> </div> <%== pagy_nav(@pagy, link_extra: 'data-turbo-frame="transactions_table"') %> </div> <%# Actions %> <div class="mt-4 mb-4"> <%= link_to "Add Transaction", new_transaction_path(estate_id: @estate.id), class: "text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800" %> </div> ``` This template: - Has a minimum height to prevent layout shifting - Includes placeholder rows for a consistent appearance - Has pagination that targets the specific tab's Turbo Frame - Includes action buttons relevant to the tab ## Transaction Partial For completeness, here's an example transaction partial: ```ruby <%# app/views/transactions/_transaction.html.erb %> <tr class="border-b hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-gray-800 border-gray-200"> <td class="w-4 px-4 py-3"> <div class="flex items-center"> <input id="checkbox-table-<%= transaction.id %>" type="checkbox" class="h-4 w-4 rounded-sm border-gray-300 bg-gray-100 text-primary-600 focus:ring-2 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-primary-600"/> <label for="checkbox-table-<%= transaction.id %>" class="sr-only">checkbox</label> </div> </td> <th scope="row" class="whitespace-nowrap px-4 py-3 font-medium text-gray-900 dark:text-white"> <%= link_to transaction.reference_number, transaction_path(transaction), class: "hover:underline", data: { turbo_frame: "_top" } %> </th> <td class="whitespace-nowrap px-4 py-3 font-medium text-gray-900 dark:text-white"><%= transaction.transactionable.name %></td> <td class="whitespace-nowrap px-4 py-3 font-medium"><%= transaction.description %></td> <td class="whitespace-nowrap px-4 py-3 font-medium text-gray-900 dark:text-white"><%= format_money(transaction.amount) %></td> <td class="whitespace-nowrap px-4 py-3 font-medium text-gray-900 dark:text-white"><%= l(transaction.transaction_date) %></td> <td class="whitespace-nowrap px-4 py-3"><%= status_badge(transaction) %></td> <td class="px-4 py-3"> <button id="transaction-<%= transaction.id %>-dropdown-button" type="button" data-dropdown-toggle="transaction-<%= transaction.id %>-dropdown" class="inline-flex items-center rounded-lg p-1 text-center text-sm font-medium text-gray-500 hover:bg-gray-200 hover:text-gray-900 focus:outline-none dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"> <svg class="h-5 w-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <path stroke="currentColor" stroke-linecap="round" stroke-width="4" d="M6 12h0m6 0h0m6 0h0"/> </svg> </button> <div id="transaction-<%= transaction.id %>-dropdown" class="z-10 hidden w-40 divide-y divide-gray-100 rounded-lg bg-white shadow-sm dark:divide-gray-600 dark:bg-gray-700"> <ul class="p-2 text-sm font-medium text-gray-500 dark:text-gray-400" aria-labelledby="transaction-<%= transaction.id %>-dropdown-button"> <li> <%= link_to transaction_path(transaction), data: { turbo_frame: "_top" }, class: "inline-flex w-full items-center rounded-md px-3 py-2 hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-600 dark:hover:text-white" do %> <svg class="me-1.5 h-4 w-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"> <path fill-rule="evenodd" d="M5 7.8C6.7 6.3 9.2 5 12 5s5.3 1.3 7 2.8a12.7 12.7 0 0 1 2.7 3.2c.2.2.3.6.3 1s-.1.8-.3 1a2 2 0 0 1-.6 1 12.7 12.7 0 0 1-9.1 5c-2.8 0-5.3-1.3-7-2.8A12.7 12.7 0 0 1 2.3 13c-.2-.2-.3-.6-.3-1s.1-.8.3-1c.1-.4.3-.7.6-1 .5-.7 1.2-1.5 2.1-2.2Zm7 7.2a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" clip-rule="evenodd"/> </svg> Details <% end %> </li> <li> <button type="button" class="inline-flex w-full items-center rounded-md px-3 py-2 text-sm font-medium text-red-600 hover:bg-gray-100 dark:text-red-500 dark:hover:bg-gray-600"> <svg class="me-1.5 h-4 w-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"> <path fill-rule="evenodd" d="M8.6 2.6A2 2 0 0 1 10 2h4a2 2 0 0 1 2 2v2h3a1 1 0 1 1 0 2v12a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V8a1 1 0 0 1 0-2h3V4c0-.5.2-1 .6-1.4ZM10 6h4V4h-4v2Zm1 4a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Zm4 0a1 1 0 1 0-2 0v8a1 1 0 1 0 2 0v-8Z" clip-rule="evenodd"/> </svg> Delete </button> </li> </ul> </div> </td> </tr> ``` Note the `data-turbo-frame="_top"` attribute on the links, which tells Turbo to navigate to a new page rather than trying to load content within the current frame. ## Adding Turbo Stream Support Finally, we need a Turbo Stream template to handle tab switching: ```ruby <%# app/views/estates/show.turbo_stream.erb %> <%= turbo_stream.update "estate_tab_content" do %> <div class="px-4 pt-2 rounded-lg bg-gray-50 dark:bg-gray-800"> <% case @active_tab %> <% when "dashboard" %> <%= render "main_tab" %> <% when "accounts" %> <%= turbo_frame_tag "accounts_table" do %> <%= render "accounting_tab", accounting_periods: @accounting_periods %> <% end %> <% when "transactions" %> <%= turbo_frame_tag "transactions_table" do %> <%= render "transactions_tab", transactions: @transactions %> <% end %> <% when "blocks" %> <%= turbo_frame_tag "blocks_table" do %> <%= render "blocks_tab", blocks: @blocks %> <% end %> <% when "properties" %> <%= turbo_frame_tag "properties_table" do %> <%= render "properties_tab", properties: @properties %> <% end %> <% when "expenses" %> <%= turbo_frame_tag "expenses_table" do %> <%= render "expenses_tab", expense_categories: @expense_categories %> <% end %> <% end %> </div> <% end %> ``` This template tells Turbo Stream how to update the page when a tab is clicked. ## Conclusion With this implementation, we've created a seamless tabbed interface that: - Provides instant tab switching without page refreshes - Maintains proper tab styling through Stimulus.js - Supports pagination within tabs for large data sets - Preserves consistent layout with placeholder rows - Uses Flowbite's styling for a professional look The beauty of this approach is how little JavaScript we needed to write. The combination of Turbo Frames and Stimulus.js handles all the heavy lifting, resulting in a faster, more maintainable codebase. This pattern can be extended to any Rails application that needs tabbed interfaces, and the same principles apply to other UI components like accordions, modals, and more. ## Key Takeaways - Turbo Frames allow us to update specific parts of the page without full refreshes - Stimulus.js provides targeted JavaScript functionality where needed - Nested Turbo Frames enable pagination within tabs - Unique frame IDs prevent conflicts between different views - The data-turbo-frame="_top" attribute allows links to navigate outside of frames By leveraging these techniques, you can create dynamic, responsive interfaces that feel like a single-page application but retain the simplicity and maintainability of traditional Rails views.

5 minute read

Subscribe to our newsletter

Join 10,000+ entrepeneurs and get creative site breakdowns, design musings and tips directly into your inbox.