| name | avo-custom-controls |
| description | Take over the button bars (controls) on an Avo resource's Show, Edit/New, and Index views, and at the end of each table row / grid item — by assigning `self.show_controls` / `edit_controls` / `index_controls` / `row_controls` blocks on the resource class. Relabel, reorder, icon-only, or remove the default buttons (back, edit, save, create, delete, detach, attach), add your own `link_to` links and `action` buttons, pull an action out of the Actions dropdown into its own button, or group extras into a `list` dropdown. Use when the user wants to add a "View on site" or "Download PDF" button to the record page, put a custom link or action at the end of each row, rename or remove the Delete / Save / Edit button on a resource, add an action button next to Edit on the show page, wire a button to a Turbo Frame or a Stimulus controller, add a mailto: or "open in Stripe" link to a record, or move too many buttons into a dropdown. Paid add-on. |
| allowed-tools | Read, Edit, Write, Glob, Grep, Bash, WebFetch |
| metadata | {"requires-gem":"avo-custom_controls — paid add-on (https://avohq.io/addons/custom-controls)"} |
Avo Custom Controls
Avo renders a default set of buttons — controls — in the header of the Show, Edit/New, and Index views, and at the end of each Index table row (and grid item). Custom controls let you take over any one of those areas: reorder or relabel the defaults, drop them, add your own links and action buttons, or fold extras into a dropdown.
- Where: four
class_attribute blocks on the resource — self.show_controls, self.edit_controls, self.index_controls, self.row_controls. Each holds a lambda that declares the controls, in order.
- License: paid add-on (
avo-custom_controls). State this up front — if the user isn't on a plan that includes it, the blocks are silently ignored and the defaults render. Add-on: https://avohq.io/addons/custom-controls
- Boundary: this skill is about the button placement. The operation a button runs is an Action — build that with the avo-actions skill, then reference the class here.
Docs — fetch on demand with WebFetch; prefer the raw .md (clean, no HTML):
Read the guide before a non-trivial layout, and the reference whenever you need a control's exact default or option list.
When this applies
Reach for custom controls when the request is about the buttons on a resource page — adding, renaming, removing, reordering, or grouping them:
| Request | Area | Approach |
|---|
| "Add a 'View on site' / 'Download PDF' button to the record page" | show_controls | link_to (or action) + default_controls |
| "Put a custom link / action at the end of each row" | row_controls | link_to / action, guard grid with params[:view_type] |
| "Rename / remove the Delete (or Save / Edit) button here" | show_/edit_controls | re-declare the block with only the buttons you want |
| "Add an action button next to Edit on the show page" | show_controls | action Avo::Actions::X + actions_list exclude: [X] |
| "Add an 'Attach' / 'Create' button with custom copy on index" | index_controls | attach_button / create_button label: "…" |
| "Too many buttons — put them in a dropdown" | any | list do … end wrapping link_to / action |
| "Only add a link before/after the existing buttons" | any | your control(s) + default_controls |
| "Make this button load a Turbo Frame / POST / run some JS" | any | link_to … data: { turbo_frame: } / data: { turbo_method: } / data: { controller: } |
If the user just wants the operation itself ("bulk-approve", "export to CSV") and doesn't care where the button lives, that's avo-actions — the action shows up in the Actions dropdown for free, no add-on needed. Come here only when they want to control placement.
Workflow
1. Pick the area and open the resource
Find the resource in app/avo/resources/ (Glob app/avo/resources/**/*.rb). Match the request to one of the four areas and its default lineup:
| Area | Renders on | Default controls (in order) |
|---|
show_controls | Show header | back_button, delete_button, detach_button, actions_list, edit_button |
edit_controls | Edit and New header | back_button (labeled "Cancel"), delete_button, actions_list, save_button |
index_controls | Index header | attach_button, actions_list, create_button |
row_controls | End of each Index row + grid item | order_controls, show_button, edit_button, detach_button, delete_button |
2. Assign the block, declaring controls in the order you want
class Avo::Resources::Fish < Avo::BaseResource
self.show_controls = -> do
link_to "View on site", "https://fish.com", target: :_blank
default_controls
end
end
The block runs in a context that exposes record, resource, view, and params (plus current_user and your app's route helpers via main_app), so you can build labels/paths from the record and branch on the view. Because edit_controls covers both Edit and New, branch on view (view != :new, view.edit?) when a control should appear on only one.
3. Keep the defaults, or replace them wholesale
- Add around the defaults: call
default_controls where you want the built-in lineup to slot in. Everything before it renders first, everything after renders last. This is the light-touch path — you don't re-declare buttons you're not changing.
- Replace entirely: omit
default_controls and declare exactly the controls you want. Anything you leave out disappears — this is how you "remove the Delete button": just don't declare it.
4. Add your own links, actions, and dropdowns
link_to "Label", path, … — any path: an external URL, a Rails path helper on record, a mailto:, #.
action Avo::Actions::SomeAction, … — pulls an action out into its own button. arguments: {format: :csv} passes data to it. Pair with actions_list exclude: [Avo::Actions::SomeAction] so it isn't also listed in the dropdown.
list label: "More" do … end — a dropdown holding link_to / action / divider when the bar gets crowded.
self.show_controls = -> do
link_to "View on site", post_path(record), icon: "heroicons/outline/globe-alt"
link_to "Email author", "mailto:#{record.author.email}", icon: "heroicons/outline/envelope"
link_to "Open in Stripe", "https://dashboard.stripe.com/customers/#{record.stripe_id}",
icon: "heroicons/outline/credit-card", style: :icon
action Avo::Actions::ExportSelection, arguments: {format: :csv}
actions_list exclude: [Avo::Actions::ExportSelection], label: "More"
default_controls
end
5. Drive Turbo and Stimulus from a control
link_to forwards data:, class:, and target: straight to the rendered <a>, so a control can trigger a frame, a non-GET request, or a Stimulus controller without any custom view:
self.show_controls = -> do
link_to "Preview", preview_fish_path(record), icon: "heroicons/outline/eye",
data: {turbo_frame: "fish_preview"}
link_to "Archive", archive_fish_path(record), icon: "heroicons/outline/archive-box",
data: {turbo_method: :post, turbo_confirm: "Archive this fish?"}
link_to "Copy API key", "#", icon: "heroicons/outline/clipboard",
data: {controller: "clipboard", action: "clipboard#copy", clipboard_text_value: record.api_key}
default_controls
end
The Stimulus controller itself still has to be registered through the asset pipeline — see avo-custom-ui.
6. Conditional visibility
A directly-declared action ignores the action's visible block — gate it with a plain Ruby if on the block's record / view / params instead:
self.show_controls = -> do
back_button
if record.released?
action Avo::Actions::ReleaseFish, style: :primary, color: :fuchsia
end
edit_button
end
(Actions placed inside a list are the exception — those do respect their visible block.)
Available controls
All controls are declared inside the four area blocks. Shared options (below) apply to most; control-specific options are noted.
Button controls (built-ins you can reorder / relabel / drop):
| Control | Does | Notes |
|---|
back_button | Hierarchical "go back" link (not history.back()) | Default label "Go back" / "Cancel" on edit |
edit_button | Link to the record's Edit view | Default label "Edit" |
show_button | Link to the record's Show view | Row-control default |
save_button | Submits the Edit/New form | Default label "Save" (or resource save translation) |
create_button | Link to the resource's New view | Index default |
attach_button | Opens the attach modal | Only shows when the resource is an association on another record |
delete_button | Destroy form; respects authorization policy | Takes confirmation_message |
detach_button | Detach form; respects policy | Association-only; takes confirmation_message |
order_controls | Reordering handles (see record-reordering feature) | Row-control default |
actions_list | Dropdown of the resource's actions | include: / exclude: to filter (single class or array); default style :outline |
Your controls:
| Control | Signature | Notes |
|---|
action | action Avo::Actions::Klass, **opts | Renders an action as a button. First arg is an Action class. arguments: passes data to it. Ignores the action's visible — use an if. |
link_to | link_to "Label", "path", **opts | Label is positional, not a label: option. Extra opts: target: (:_blank/:_top/:_self), data:, class:, size: — all forwarded to the <a>. |
list | list label: "…" do … end | Dropdown; also takes title:, style:, color:, icon:. Inside: only link_to, action, divider. Default style :outline, color :primary. Actions inside respect visible. |
divider | divider | Separator line — inside a list only. |
default_controls | default_controls | Re-inserts the current area's default lineup. |
Shared options (each control's reference entry lists which it accepts):
| Option | Type | Notes |
|---|
label: | String | Button text. "" → icon-only button. |
title: | String | Tooltip. (With style: :icon, link_to takes the tooltip from label, not title.) |
style: | Symbol | :primary, :outline, :text, :icon. Default :text (:outline for actions_list/list). |
color: | Symbol | Any Tailwind palette color (:blue, :fuchsia, :slate…). Default :gray. |
icon: | String | Heroicon with style prefix: "heroicons/outline/globe" or "heroicons/solid/eye". |
size: | Symbol | :sm, :md, :lg. Default :md; row controls auto-apply :sm. |
confirmation_message: | String | delete_button / detach_button only — the confirm-dialog text. |
arguments: | Hash | action only — data passed into the action. |
Gotchas
action ignores the action's visible block. A control declared with action always renders — wrap it in a plain if (on record/view/params) to hide it. Only actions inside a list respect visible. (See avo-actions for writing the visible block itself.)
- Button controls are banned inside
list. back_button, edit_button, save_button, etc. inside a list block raise in development ("not allowed on a custom list"). A list may only hold link_to, action, and divider.
- Nested items ignore
style:, color:, and size:. The dropdown styles its own items, so those options do nothing on a link_to or action inside a list — no error, just no effect. icon:, label:, and a link's target:/data:/class: still apply. Style the dropdown itself on the list.
row_controls also render on grid items. The same block drives table rows and grid cards. To show a control on only one, branch on params[:view_type] (e.g. unless params[:view_type] == "grid").
- Replacing a block removes what you don't declare. Assigning
show_controls and listing three buttons drops the other two. To keep the defaults and merely add around them, call default_controls instead of re-listing everything.
- Icons here are Heroicons, not the Tabler icons used for the menu/resources — and the style segment is required:
"heroicons/outline/academic-cap".
delete_button / detach_button honor your policy. They already respect Pundit destroy? / detach rules — you don't gate them by hand. If a delete button "won't show," check the policy (see ).
Report
When done, tell the user:
- Which resource and which area(s) you changed (
show_controls / edit_controls / index_controls / row_controls), and the final control order.
- What you added or removed — new
link_to/action buttons, relabeled/dropped defaults, any list dropdown — and whether you kept the rest via default_controls.
- Any conditional logic (
if record.…, view, params[:view_type]) and why.
- Follow-ups they still own: it's a paid add-on (confirm the license), any referenced Action classes that must exist (build with avo-actions), and policy rules behind delete/detach visibility (avo-authorization).