Skip to main content

Turbo Frames Guide

Turbo Frames enable partial page updates by wrapping sections of your page in <turbo-frame> elements. When a link or form within a frame is activated, only that frame's content is updated.

How It Works​

  1. User clicks a link or submits a form inside a <turbo-frame>
  2. Turbo.js sends a request with the Turbo-Frame header
  3. Your server checks for this header and returns a partial view
  4. Turbo.js extracts the matching frame from the response and updates the DOM

Basic Setup​

1. Create a Partial View​

<!-- Pages/Shared/_CartItems.cshtml -->
<turbo-frame id="cart-items">
@foreach (var item in Model.Items)
{
<div class="cart-item">
<span>@item.Name</span>
<span>@item.Price.ToString("C")</span>
</div>
}
</turbo-frame>

2. Include the Partial in Your Page​

<!-- Pages/Cart/Index.cshtml -->
@page
@model CartModel

<h1>Shopping Cart</h1>

<partial name="_CartItems" model="Model" />

<a href="/Cart?handler=Refresh" data-turbo-frame="cart-items">
Refresh Cart
</a>

3. Handle Frame Requests​

// Pages/Cart/Index.cshtml.cs
public class CartModel : PageModel
{
public List<CartItem> Items { get; set; }

public void OnGet()
{
Items = GetCartItems();
}

public IActionResult OnGetRefresh()
{
Items = GetCartItems();

// Check if this is a Turbo Frame request
if (Request.Headers.ContainsKey("Turbo-Frame"))
{
return Partial("_CartItems", this);
}

// Regular request - redirect to full page
return RedirectToPage();
}
}

Helper Extensions​

Use the helper extensions for cleaner code:

using Tombatron.Turbo;

public IActionResult OnGetRefresh()
{
Items = GetCartItems();

if (HttpContext.IsTurboFrameRequest())
{
return Partial("_CartItems", this);
}

return RedirectToPage();
}

Available Extensions​

// Check if any Turbo Frame request
HttpContext.IsTurboFrameRequest()

// Check for specific frame
HttpContext.IsTurboFrameRequest("cart-items")

// Check for frames with a prefix (dynamic IDs)
HttpContext.IsTurboFrameRequestWithPrefix("item_")

// Get the frame ID
string? frameId = HttpContext.GetTurboFrameId();

Dynamic Frame IDs​

For lists of items, use dynamic IDs with a consistent prefix:

<!-- Partial for each item -->
@foreach (var item in Model.Items)
{
<turbo-frame id="item_@item.Id">
<div class="item">
<span>@item.Name</span>
<form method="post" asp-page-handler="Remove" asp-route-id="@item.Id">
<button type="submit">Remove</button>
</form>
</div>
</turbo-frame>
}

Handle with prefix matching:

public IActionResult OnPostRemove(int id)
{
RemoveItem(id);

if (HttpContext.IsTurboFrameRequestWithPrefix("item_"))
{
// Return empty frame to remove the item
return Content($"<turbo-frame id=\"item_{id}\"></turbo-frame>", "text/html");
}

return RedirectToPage();
}

Lazy Loading​

Load content only when the frame scrolls into view:

<turbo-frame id="comments" src="/posts/@Model.Id/comments" loading="lazy">
<div class="loading-placeholder">
Loading comments...
</div>
</turbo-frame>

The frame will request /posts/{id}/comments when it becomes visible.

Breaking Out of Frames​

Use target="_top" to make links navigate the whole page:

<turbo-frame id="search-results">
@foreach (var result in Model.Results)
{
<!-- This link navigates the whole page, not just the frame -->
<a href="/products/@result.Id" data-turbo-frame="_top">
@result.Name
</a>
}
</turbo-frame>

Or set it on the frame itself:

<turbo-frame id="login" target="_top">
<!-- All links/forms in this frame will navigate the full page -->
</turbo-frame>

Targeting Other Frames​

A link can update a different frame:

<turbo-frame id="sidebar">
<a href="/products/1" data-turbo-frame="main-content">View Product</a>
</turbo-frame>

<turbo-frame id="main-content">
<!-- Product details appear here -->
</turbo-frame>

Disabling Frame Navigation​

Prevent the frame from intercepting navigation:

<turbo-frame id="preview" disabled>
<!-- Links and forms work normally -->
</turbo-frame>

Best Practices​

1. Always Return a Matching Frame​

The response must contain a <turbo-frame> with the same ID:

<!-- Request: Turbo-Frame: cart-items -->
<!-- Response must include: -->
<turbo-frame id="cart-items">
<!-- Updated content -->
</turbo-frame>

2. Handle Non-Turbo Requests​

Always provide a fallback for regular requests:

if (HttpContext.IsTurboFrameRequest())
{
return Partial("_MyPartial", Model);
}
return RedirectToPage();

3. Use Semantic IDs​

Choose IDs that describe the content:

<!-- Good -->
<turbo-frame id="user-profile">
<turbo-frame id="order-history">
<turbo-frame id="product-reviews">

<!-- Avoid -->
<turbo-frame id="frame1">
<turbo-frame id="content">

4. Keep Frames Focused​

Each frame should represent a single, cohesive piece of content. If you need to update multiple areas, consider using Turbo Streams instead.

Troubleshooting​

Frame Not Updating​

  1. Check that response contains a frame with matching ID
  2. Verify the Turbo-Frame header is present in the request
  3. Ensure partial view renders the complete frame element

Full Page Loads Instead of Frame Updates​

  1. Check that Turbo.js is loaded on the page
  2. Verify the link/form is inside the frame or uses data-turbo-frame
  3. Ensure target="_top" isn't set unintentionally

Caching Issues​

The Vary: Turbo-Frame header is added automatically to prevent caching issues. If using a CDN, ensure it respects this header.

See Also​