| name | avo-custom-fields |
| description | Author a brand-new reusable Avo field TYPE with `rails generate avo:field` when the ~38 built-in types don't fit — scaffolds Edit/Show/Index ViewComponents plus a `Field` config class, adds field-specific options, and customizes how the field renders. Use when the user wants to build a new field type for the admin — "add a new field type to Avo", "Avo doesn't have a field for X, build one", "I need a color-picker / slider / rating / custom-widget field", "render this attribute as a custom control on the form", "make a field that renders a custom widget", or "duplicate the text field and extend it". NOT for using an existing type (`field :x, as: :select`) or adding a normal field to a resource — that is the avo-fields skill. |
| allowed-tools | Read, Edit, Write, Glob, Grep, Bash, WebFetch |
| metadata | {"requires-gem":"none — Community"} |
Author a custom Avo field type
Avo (a Rails admin framework) ships ~38 built-in field types. When none of them fit, you can author your own reusable field type and then use it like any built-in: field :progress, as: :progress_bar. This skill is about creating that type with bin/rails generate avo:field, which scaffolds three ViewComponents — Edit (also used for New), Show, and Index — plus a Field configuration class. You edit those four files to define options and rendering.
Critical distinction — do not confuse these two things:
- Using a field —
field :status, as: :select, adding/removing/reordering a field, changing an existing field's type or options → that's editing def fields in a resource. Use the avo-fields skill, not this one. "Add a status field to the Project model" is avo-fields.
- Authoring a field type — inventing a new
as: value backed by your own components (a color picker, a slider, a star widget Avo doesn't have) → this skill. The tell is that the user wants behavior/rendering no built-in type provides, or explicitly says "new field type", "build a field", "custom widget field", or "extend the text field".
If the request is really "just show attribute X as a picker" and a built-in type already does it (:select, :radio, :stars, :code, :key_value, …), stop and hand off to avo-fields — authoring a whole type is overkill.
Custom fields are Community (free).
Docs (fetch on demand — don't rely on memory for exact helper/option names):
- Docs map / index: https://docs.avohq.io/4.0/docs-map.md
- Custom fields (primary guide — generator, options, component helpers, Stimulus, non-model fields): https://docs.avohq.io/4.0/custom-fields.md
- Field wrappers (
field_wrapper / index_field_wrapper, dash_if_blank, what the wrapper draws for you): https://docs.avohq.io/4.0/field-wrappers.md
- Eject an existing field's components (
--field-components, --view, --scope) — tweak a built-in field's rendering without authoring a new type: https://docs.avohq.io/4.0/eject-views.md
- Custom CSS/JS pipeline (custom fields get NO automatic asset loading): https://docs.avohq.io/4.0/asset-handling.md
- Ship a field type from a gem (
register_field, for plugin authors): https://docs.avohq.io/4.0/plugins.md
- Field visibility helpers (
hide_on, only_on, …): https://docs.avohq.io/4.0/field-options.md
When this applies
Use this skill when the user wants a field type that doesn't exist yet:
- "Add a new field type", "build a custom field", "Avo has no field for X".
- A specific custom widget: color picker, slider/range, dial, signature pad, rating other than
:stars, a masked/formatted input, a bespoke display cell.
- "Duplicate/clone the text (or any) field and extend it" — start from a template with
--field-template.
- Rendering a value as a custom control on the form, or a custom cell on Index/Show, with its own ERB/JS/CSS.
- A field whose value isn't a real column and needs custom getter/setter behavior on the model.
Not this skill:
- Adding or changing a normal field, picking an
as: type, setting field options → avo-fields.
- Only restyling a built-in field's existing markup, everywhere or in one place → eject its components with
avo:eject --field-components (see the eject-views doc) rather than authoring a new type.
- Loading the JS/CSS your new field needs → the pipeline setup lives in avo-custom-ui (asset-handling). This skill writes the field; that one wires the assets.
Workflow
-
Confirm it's really a new type. Re-read the "Critical distinction" above. If a built-in type covers it, hand off to avo-fields.
-
Generate the field. Pick a snake_case name (the as: value users will type):
bin/rails generate avo:field progress_bar
This creates:
app/avo/fields/progress_bar_field.rb — the Avo::Fields::ProgressBarField config class (registers the type, holds options).
app/components/avo/fields/progress_bar_field/{edit,show,index}_component.rb + matching .html.erb — the three ViewComponents.
To start from an existing built-in instead of blank text components, clone it (all components come out identical to the original, renamed):
bin/rails generate avo:field super_text --field-template text
The generator flag is --field-template (Avo's docs also write it --field_template; Thor accepts either). The avo:field generator has no --view or --scope option — those belong to avo:eject --field-components (used to override an existing field's components, not to author a new one).
-
Restart the Rails server. A new field type isn't picked up until you restart. Tell the user this explicitly — it's the #1 "my field doesn't work" cause.
-
Define field-specific options in app/avo/fields/<name>_field.rb — an attr_reader per option plus reading it in initialize (see Key pieces). Options users pass to field :x, as: :your_field, foo: 1 arrive in args.
-
Customize the three components. Edit the .html.erb templates to render your control on Edit and your display on Show/Index. Keep the field_wrapper / index_field_wrapper block so the field looks native (see Key pieces).
-
Wire assets if needed. Custom fields have no automatic asset loading. Any JS/CSS must go through your own Avo asset pipeline — cross-link . Avo does ship a few reusable Stimulus controllers (e.g. ) you can attach without new assets.
Key pieces
The Field config class
app/avo/fields/<name>_field.rb registers the type and declares its options. Expose each option with an attr_reader and read it (with a default) from args in initialize:
class Avo::Fields::ProgressBarField < Avo::Fields::BaseField
attr_reader :max, :step, :display_value, :value_suffix
def initialize(name, **args, &block)
super(name, **args, &block)
@max = args[:max] || 100
@step = args[:step] || 1
@display_value = args[:display_value] || false
@value_suffix = args[:value_suffix] || nil
end
end
BaseField also provides typed helpers that read + coerce an arg in one line: add_boolean_prop(args, :display_value), add_string_prop(args, :value_suffix), add_array_prop, add_object_prop. You still declare the matching attr_reader.
Then it's used like any built-in:
def fields
field :id, as: :id
field :progress, as: :progress_bar, step: 10, display_value: true, value_suffix: "%"
end
Visibility: call the standard helpers inside initialize to bake in a default (e.g. hide_on :forms), or let users override per usage with hide_on: / only_on: in the field call.
table_header_class: override this method to return a CSS class for the Index <th> (e.g. force a column width with "w-32"). Defaults to nil.
The three components
Generated components are plain text fields you replace. Each inherits from an Avo base component (Avo::Fields::EditComponent, ShowComponent, IndexComponent), which exposes what you render with. Available in the .html.erb templates:
| Helper / var | Where | What it is |
|---|
@field | all three | Your field instance. @field.value, @field.id, @field.placeholder, @field.name, plus every attr_reader you added (@field.max, …). |
@resource, @view, @index | all three | The resource, an Avo::ViewInquirer (@view.edit?, @view.show?, …), and the field's position. |
field_wrapper_args | all three | Splat into the wrapper: field_wrapper **field_wrapper_args. Carries @field, @resource, @view, and layout flags. |
@form | Edit only | Rails form builder — build inputs with @form.range_field @field.id, @form.text_field @field.id, etc. |
classes("extra") | Edit only | Input CSS classes with error state / size / HTML overrides already applied. |
disabled? | Edit, Show | true when readonly or disabled — prefer it over @field.readonly, it covers both. |
The field wrapper (why every component starts with it)
The first thing each component does is wrap your content in field_wrapper (Show/Edit) or index_field_wrapper (Index). The wrapper is what makes a custom field look native: it draws the label, required asterisk, help text, validation error, blank-— placeholder, and applies stacked / full_width / density layout. You render only the value; the wrapper renders everything around it. That's why you splat field_wrapper_args instead of hand-building the label. Pass extra options alongside it (field_wrapper **field_wrapper_args, dash_if_blank: false).
Typical customization — a <progress> bar on Show, a range slider on Edit:
<%# show_component.html.erb %>
<%= field_wrapper **field_wrapper_args do %>
<% if @field.display_value %>
<div class="text-center text-sm font-semibold w-full leading-none mb-1">
<%= @field.value %><%= @field.value_suffix if @field.value_suffix.present? %>
</div>
<% end %>
<progress max="<%= @field.max %>" value="<%= @field.value %>" class="block w-full"></progress>
<% end %>
<%# edit_component.html.erb %>
<%= field_wrapper **field_wrapper_args do %>
<%= @form.range_field @field.id,
class: "w-full", disabled: disabled?, min: 0,
max: @field.max, step: @field.step %>
<% end %>
Index uses index_field_wrapper the same way.
Pre-built Stimulus controllers
Avo bundles reusable controllers so you don't have to ship JS for common patterns — e.g. hidden-input, which collapses content behind a "Show content" trigger (as the Trix field does). Wire it in ERB: put data-controller="hidden-input" on a wrapper, add a link with data: { action: "click->hidden-input#showContent" }, and mark the collapsible div with data-hidden-input-target="content". See the custom-fields doc for the full markup.
Gotchas
Report
After building the field, tell the user:
- The generator command run and the four files created (
app/avo/fields/<name>_field.rb + the three component pairs under app/components/avo/fields/<name>_field/).
- The
as: value they now use in a resource (field :x, as: :<name>) and any options the field accepts (each attr_reader + its default).
- That they must restart the Rails server before the field appears.
- Any JS/CSS the field needs and that it must be wired through their own Avo asset pipeline (point to avo-custom-ui), plus any model getter/setter required for a non-model value.
- If you cloned with
--field-template, which type you started from.