| 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"} |
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.