| name | avo-branding-appearance |
| description | Brand and theme an Avo admin panel — logos, favicon, color scheme, neutral/accent palettes, chart colors, per-user theme persistence, deep CSS re-skinning, and menu/action icons — starting from the no-build `config.appearance` path in `config/initializers/avo.rb`. Use when the user wants to "brand the admin with our logo and colors", "make the admin match our brand", "add our company logo", "add a favicon", "make the admin default to / support dark mode", "let users switch themes" or "lock the theme", "remember each user's theme", "change the accent/primary color" or "make the buttons blue", "change the sidebar/navbar background", "give the admin a coastal/rose/sunset theme", "our admin looks too generic", "change the dashboard chart colors", or "use a custom icon for this menu item" — whether or not they name Avo. |
| allowed-tools | Read, Edit, Write, Glob, Grep, Bash, WebFetch |
| metadata | {"requires-gem":"none — Community"} |
These instructions ship inside the avo gem this app has locked, so they describe the version you are actually running. Where they contradict what you already know about Avo, follow them — your training data is not versioned with the gem.
Avo Branding & Appearance
Make the Avo admin look like your product — logos, favicon, color scheme, brand colors, chart colors, and the deeper CSS chrome (navbar, sidebar, tables). Avo gives you a ladder: start with a few lines of Ruby in config/initializers/avo.rb (config.appearance = { … }, no build step), and only drop to CSS or ejected views when the Ruby layer can't express what you want. Almost every branding request is satisfied by config.appearance alone — reach for CSS last.
Files you'll touch, from shallowest to deepest:
config/initializers/avo.rb → config.appearance = { … } — logos, favicon, scheme, palettes, picker/lock, persistence, chart colors. The default answer.
app/assets/stylesheets/avo-overrides.css — no-build CSS-variable re-skin (eject with rails g avo:eject --partial :avo_overrides_css). Served as-is.
app/views/avo/partials/_head.html.erb — inline <style> for component variables (eject with rails g avo:eject --partial :head).
app/assets/svgs/ — your own SVG icons for menu items / actions.
- A JSONB column +
load_settings/save_settings procs — only when persisting each user's theme to the database.
Docs
Authoritative docs — fetch on demand, verify option names against them (and the app's installed Avo source) before writing; don't inline whole pages:
When this applies
Explicit (Avo named): "set config.appearance", "change Avo's logo / logomark / favicon", "set the Avo accent/neutral palette", "lock the Avo theme", "restrict the appearance picker", "persist Avo appearance to the database", "override Avo CSS variables", "eject the :head partial", "set an icon on this Avo menu item".
Implicit (product-shaped, no mention of Avo): "brand the admin with our logo and colors", "make the admin match our brand", "add our company logo / a favicon", "the admin should default to dark mode" / "support dark mode", "let users switch themes" / "lock it so they can't", "remember each user's theme across devices", "change the accent/primary color" / "make the buttons blue", "change the sidebar/navbar background color", "give the admin a coastal / rose / 80s-sunset theme", "our admin looks too generic", "change the dashboard chart colors", "use a custom icon for this menu item".
Workflow
Read config/initializers/avo.rb first. If a config.appearance = { … } hash already exists, merge new keys into it rather than adding a second block. Work top-down through this ladder and stop at the shallowest layer that does the job.
1. Brand assets — logo, logomark, favicon, placeholder
Asset paths resolve through the Rails asset pipeline (e.g. app/assets/images/my_company/logo.png → "my_company/logo.png"). Each has an optional *_dark variant used in dark mode; omit the dark variant and the light file is used in both schemes.
Avo.configure do |config|
config.appearance = {
logo: "my_company/logo.png",
logo_dark: "my_company/logo-dark.png",
logomark: "my_company/logomark.png",
logomark_dark: "my_company/logomark-dark.png",
favicon: "my_company/favicon.ico",
favicon_dark: "my_company/favicon-dark.ico",
placeholder: "my_company/placeholder.svg"
}
end
The top navbar is dark in BOTH light and dark mode — logo always sits on a dark surface, so pick a file that reads on dark. logo_dark only swaps when the whole UI is in dark mode, not to fix contrast on the navbar.
2. Color scheme — light / dark / auto
config.appearance = {
scheme: :auto
}
Unless locked (step 4), users can flip the scheme from the navbar switcher. "Default to dark mode" → scheme: :dark. "Force dark, no toggle" → scheme: :dark + lock: [:scheme].
3. Brand colors — neutral & accent palettes
Two palettes drive the whole UI. Neutral = surfaces, borders, chrome. Accent = interactive emphasis: primary buttons, links, focus rings, selected rows. "Make the buttons blue" / "change the primary color" → set accent.
Built-in presets (fastest):
config.appearance = {
neutral: :slate,
accent: :blue
}
neutral: and accent: must be Symbols — a String or Hash raises ArgumentError.
Custom brand colors — define your own palette, then select it with the :brand preset:
config.appearance = {
neutral: :brand,
neutral_colors: {
25 => "oklch(98.5% 0.005 60)", 50 => "oklch(97% 0.008 60)", 100 => "oklch(93% 0.012 60)",
200 => "oklch(86% 0.015 60)", 300 => "oklch(76% 0.015 60)", 400 => "oklch(63% 0.014 60)",
500 => "oklch(53% 0.013 60)", 600 => "oklch(48% 0.012 60)", 700 => "oklch(43% 0.011 60)",
800 => "oklch(39% 0.010 60)", 900 => "oklch(28% 0.008 60)", 950 => "oklch(20% 0.005 60)"
},
accent: :brand,
accent_colors: {
color: "oklch(55% 0.2 280)",
content: "oklch(45% 0.2 280)",
foreground: "oklch(99% 0 0)"
}
}
Palette values accept any CSS color string (oklch(), #hex, rgb(), hsl(), var()). neutral_colors and accent_colors are independent — set either, both, or neither. Defining the colors only creates the palette; you still need neutral: :brand / accent: :brand (or "brand" listed in neutrals:/accents:) to actually select it. If the user gives you only one brand color, set accent_colors and leave the neutral as a preset like :slate.
4. Picker exposure — restrict, lock, and switcher layout
By default the navbar picker lets users change scheme, neutral, and accent. Trim the options, lock some down, or change the layout:
config.appearance = {
scheme: :light, neutral: :slate, accent: :blue,
neutrals: %w[brand slate stone olive],
accents: %w[brand blue indigo violet],
lock: [:scheme],
picker_layout: :inline
}
A value not in lock: is a default the user can still override. Lock all three to fully pin the theme.
5. Persist each user's picks
By default picks live in a cookie (per browser, zero setup). For cross-device persistence, switch to the database and supply both blocks; you need a JSON/JSONB column on the model backing current_user.
class AddAvoPreferencesToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :avo_preferences, :jsonb, default: {}
end
end
config.appearance = {
persistence: :database,
load_settings: -> {
current_user&.avo_preferences&.dig("appearance")&.symbolize_keys || {}
},
save_settings: -> {
next unless current_user
current_user.update!(
avo_preferences: current_user.avo_preferences.to_h.deep_merge(
"appearance" => settings.stringify_keys
)
)
}
}
Both blocks run in Avo's execution context with current_user (plus params, request, view_context, main_app). load_settings returns a Hash with any subset of :color_scheme, :neutral, :accent; missing keys fall back to defaults. save_settings receives a settings local that is a partial Hash — only the keys the user just changed — so always deep_merge into existing preferences, never overwrite the whole blob.
6. Chart colors
Recolor dashboard charts. Values are forwarded straight to Chart.js, so they must be hex (not oklch/rgb):
config.appearance = {
chart_colors: ["#0B8AE2", "#34C683", "#FFBE4F", "#FF7676", "#2AB1EE"]
}
7. Deep re-skin — CSS variables (only when config.appearance can't reach it)
Navbar background, sidebar surfaces, table row hover/selected, focus ring, and motion speeds aren't routed through Ruby — they're CSS custom properties. Avo's whole look is variable-driven, so overriding a handful re-skins everything with no build step. Put light-mode values on :root, dark-mode overrides on .dark.
Preferred surface — eject avo-overrides.css (loaded after Avo's stylesheet, so it wins the cascade):
rails g avo:eject --partial :avo_overrides_css
:root {
--color-accent: var(--color-fuchsia-500);
--color-navbar-background: #1e3a5f;
--color-sidebar-background: #f5f7fa;
--color-table-row-hover: #eef4fb;
--radius-card: 1.5rem;
}
.dark {
--color-accent: var(--color-fuchsia-400);
--color-navbar-background: #0b1a2b;
--color-sidebar-background: #11161c;
}
The navbar and sidebar expose scoped palette contracts on the .top-navbar and .avo-sidebar selectors (e.g. --top-navbar-content, --sidebar-link-active-background) so you can recolor them without leaking into popovers or the main content. If you'd rather keep the <style> next to the rest of the head, eject :head instead and append a <style> block there:
bin/rails generate avo:eject --partial :head
The full variable list (navbar, sidebar, table, focus ring, motion) with defaults lives in the CSS variables section of appearance-api.md — fetch it before writing component-level overrides. For named-theme requests ("coastal", "rose", "80s sunset"), work in avo-overrides.css with matching :root and .dark values.
8. Icons (menu items, actions)
Anywhere Avo takes an icon:, pass a path string. Prefer Tabler in v4 (tabler/outline/<name> or tabler/filled/<name>); Heroicons (heroicons/outline/<name>, also solid/mini/micro) are legacy-supported. Your own SVGs go in app/assets/svgs/ and are referenced by filename.
link "Reports", path: "/reports", icon: "tabler/outline/chart-bar"
dashboard :sales, icon: "my-icons/rocket.svg"
For populating menu/resource icons at scale (migrations, whole-sidebar passes), that's the avo-navigation-search skill — defer to it rather than hand-picking here. Avo's own avo/* icons are a private API — don't rely on them.
Key options (config.appearance)
| Option | Does | Type / values |
|---|
logo / logo_dark | Main navbar logo (+ dark variant) | String path; default "avo/logo.png" |
logomark / logomark_dark | Compact mark for collapsed navbar | String path |
favicon / favicon_dark | Browser favicon (+ dark variant) | String path |
placeholder | Fallback image for record with no cover | String path |
scheme | Default color mode | :auto (default) :light :dark |
neutral / accent | Palette presets | Symbol only (or :brand) |
neutral_colors | Custom 12-shade neutral | Hash of all 12 shades (25…950) |
accent_colors | Custom accent | Hash of :color, :content, :foreground |
neutrals / accents | Restrict picker options | Array of Strings (no colon) |
lock | Force values, hide switchers | Array subset of [:scheme, :neutral, :accent] |
picker_layout | Navbar switcher layout | :inline (default) :dropdown |
persistence | Where picks are stored | :cookie (default) :database |
load_settings / save_settings | DB persistence blocks | Proc; needs a JSON/JSONB column |
|
CSS-only knobs (navbar/sidebar/table/focus/motion variables) are not in this hash — see step 7 and appearance-api.md.
Gotchas
neutral: / accent: must be Symbols. A String or Hash raises ArgumentError. Custom colors go through neutral_colors: / accent_colors:, and you must also set neutral: :brand / accent: :brand (or list "brand" in neutrals:/accents:) to select the custom palette.
neutral_colors needs all 12 shades; accent_colors needs all 3 tokens. A missing or nil value raises ArgumentError.
- The navbar is dark in both modes. The
logo must read on a dark surface. logo_dark is for whole-UI dark mode, not navbar contrast.
chart_colors must be hex. They're passed straight to Chart.js — oklch()/rgb() won't work there (even though the palettes accept them).
avo-overrides.css is served as-is, NOT through the Tailwind build. Only put plain CSS + variable overrides there — no @apply, no arbitrary values. Tailwind directives for your own custom UI belong in app/assets/stylesheets/avo/ (which IS built). See the avo-custom-ui skill.
- DB persistence needs a JSONB column and BOTH blocks, and
save_settings gets a partial settings Hash (only the changed keys) — deep_merge, never overwrite the whole preferences blob.
config.branding was renamed to config.appearance in Avo 4. On a v3 app you may find config.branding = { … } — migrate it into config.appearance.
- Avo's
avo/* icons are a private API. Use tabler/* (preferred), heroicons/* (legacy), or your own SVGs in app/assets/svgs/.
- Verify before writing. Option names drift between versions — check the docs URLs above or the installed Avo source rather than trusting memory.
Cross-links
- avo-admin-config — other global
Avo.configure knobs beyond appearance.
- avo-navigation-search — populating menu/resource icons and menu structure at scale.
- avo-custom-ui — deeper CSS/Tailwind for your own tools/fields, and ejecting views for full markup control.
Report
When done, tell the user:
- Which file(s) you edited (full paths) and, for CSS/eject work, which generator command(s) to run (
rails g avo:eject --partial :avo_overrides_css / :head, or the migration).
- Which
config.appearance keys you set and why (assets, scheme, palettes, picker/lock, persistence, chart colors).
- Which layer of the ladder you used and why you didn't go deeper (Ruby vs
avo-overrides.css vs ejected :head).
- Anything the user must still do: add the asset files under
app/assets/, run the migration for DB persistence, or restart the server so initializer changes take effect.
- For custom palettes, remind them the same scale applies in both light and dark mode, and confirm the
:brand preset is selected.