Skip to content

Theming ​

Theming is how you make Avo look like your product — its colors, fonts, surfaces, logos, and chrome.

The fastest way to theme Avo is to let an AI agent do it for you. Everything Avo exposes for theming is plain, well-structured text — Ruby config, CSS variables, ERB — which is exactly what an LLM handles well. Point your coding agent at this guide, describe the look you want, and it can produce a complete, coherent theme far faster than hand-tuning values one at a time. Skip to the ready-made prompts →

Hand your agent these pages

Theming and customizing draws on a handful of Avo surfaces. Give your agent any of these (or read them yourself) — together they're the full toolbox:

Do it by hand ​

Everything the agent does, you can do yourself. Avo gives you a ladder of approaches, from a few lines of Ruby to full CSS control — pick the one that matches how far you want to go.

You want to…Reach forBuild step
Swap logos, pick a color scheme and accent/neutralAppearance confignone
Re-skin the whole UI by overriding colors and radiiCSS variablesnone
Use your own typefaceFont variablesnone
Restyle specific components (navbar, sidebar, tables)Component CSS variablesnone
Style your own custom tools, fields, or pagesTailwind & custom CSSTailwind
Rewrite a view's markup outrightEject a view for full controlnone

Most apps only ever touch the first two rows. Start there and go deeper only when you hit something the higher layer can't express — the further down you go, the more you own.

Recolor with the appearance config ​

The fastest way to brand Avo is config.appearance — set your logos, default color scheme, and pick a neutral and accent palette (built-in presets or your own brand colors), all from Ruby with no build step.

ruby
# config/initializers/avo.rb
Avo.configure do |config|
  config.appearance = {
    logo: "my_company/logo.png",
    neutral: :slate,
    accent: :blue
  }
end

This covers the palette that drives most of the interface. For everything it deliberately leaves out — navbar background, sidebar surfaces, table row affordances, focus rings, motion — drop to CSS variables below. See the Appearance guide for the full palette, logo, persistence, and locking options.

Re-skin with CSS variables ​

Avo's entire look is driven by CSS custom properties, so overriding a handful of variables re-skins the whole interface — no @apply, no build step. The quickest surface for this is avo-overrides.css: an empty file Avo ships and loads on every screen, after its own stylesheet, so your rules win the cascade.

Eject it into your app, then override whatever you like:

bash
rails g avo:eject --partial :avo_overrides_css
css
/* app/assets/stylesheets/avo-overrides.css */
:root {
  --color-accent: var(--color-fuchsia-500);
  --color-accent-content: var(--color-fuchsia-600);
  --radius-card: 1.5rem;
}

.dark {
  --color-accent: var(--color-fuchsia-400);
}

Set light-mode values on :root and dark-mode-specific values on .dark when you need them to differ.

Let an AI agent do it ​

Because it's plain, well-named CSS variables in a single file, this is the surface where an LLM shines — hand it this guide and a vibe, and it can write a complete, coherent theme in one shot. Copy a prompt below (pick a theme in the tabs) and paste it into your coding agent:

text
Use the Avo theming guide at https://docs.avohq.io/4.0/theming.html and reskin my Avo app to a coastal theme — soft sand neutrals, sea-glass and deep-ocean accents. Override CSS variables in app/assets/stylesheets/avo-overrides.css only, with matching :root and .dark values.
text
Use the Avo theming guide at https://docs.avohq.io/4.0/theming.html and reskin my Avo app to a rose theme — warm blush neutrals and a rich rose accent. Override CSS variables in app/assets/stylesheets/avo-overrides.css only, with matching :root and .dark values.
text
Use the Avo theming guide at https://docs.avohq.io/4.0/theming.html and reskin my Avo app to an 80's sunset theme — dusk purples and hot magenta-to- orange accents. Override CSS variables in app/assets/stylesheets/avo-overrides.css only, with matching :root and .dark values.

TIP

avo-overrides.css is served as-is — it is not run through the Tailwind build. For custom utility classes (@apply, arbitrary values) in your own UI, use the TailwindCSS integration instead.

Change the font ​

Avo ships Inter and reads it through a single variable, --font-sans, which every screen inherits. Monospace text — code snippets in alerts, file details in the media library — reads --font-mono. Point either at another family in avo-overrides.css and the whole interface follows, with no build step:

css
/* app/assets/stylesheets/avo-overrides.css */
:root {
  --font-sans: "IBM Plex Sans", system-ui, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, monospace;
}

That is the entire change for a font the visitor's device already has, such as a system font stack. For any other typeface, load it first — pick one of the two approaches below.

Avo sets text at weights 400, 500, 600, and 700. Load all four (or a variable font covering that range) — otherwise the browser synthesizes the missing weights.

Load a hosted font ​

Every font service hands you a stylesheet URL. Paste it as a @import at the very top of avo-overrides.css — CSS requires imports before any other rule — and set the variable below it:

css
/* app/assets/stylesheets/avo-overrides.css */
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap");

:root {
  --font-sans: "IBM Plex Sans", system-ui, sans-serif;
}

If you'd rather load it with <link> tags — the service's copy-paste snippet, with preconnect hints — eject the :head partial and drop them in. Fonts don't compete in the cascade, so it doesn't matter that :head renders after Avo's assets (:pre_head, which renders before them, works just as well). The variable override still lives in avo-overrides.css:

erb
<%# app/views/avo/partials/_head.html.erb %>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap">

Where to get that URL:

ServiceStylesheet URLNotes
Google Fontshttps://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swapThe largest library. Pick the four weights in the "Get font" panel.
Bunny Fontshttps://fonts.bunny.net/css?family=ibm-plex-sans:400,500,600,700Same library as Google Fonts, served without tracking — the GDPR-friendly swap.
Fontsharehttps://api.fontshare.com/v2/css?f[]=satoshi@400,500,700&display=swapIndian Type Foundry's free catalog — a different flavor from the Google set.
Adobe Fontshttps://use.typekit.net/<kit-id>.cssCommercial faces bundled with Creative Cloud. Add the site's domain to the kit.

WARNING

If your app sets a Content Security Policy, allow the font host in font-src — and in style-src when the font's stylesheet is fetched from there too.

Self-host the font files ​

Drop the files under public/fonts/ and declare them with @font-face in avo-overrides.css. Referencing them by absolute path keeps the same file working under Propshaft and Sprockets alike, with no digest lookup involved. For a Google Fonts family, google-webfonts-helper packages the .woff2 files and the matching @font-face block:

css
/* app/assets/stylesheets/avo-overrides.css */
@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/fonts/ibm-plex-sans-variable.woff2") format("woff2");
}

:root {
  --font-sans: "IBM Plex Sans", system-ui, sans-serif;
}

That declaration covers a variable font. For static files, write one @font-face per weight, each with a single font-weight value.

Fine-tune specific components ​

Individual components expose their own variables — the top navbar palette, sidebar background, table row hover/selected states, focus ring, and more. Override them in avo-overrides.css (above) or, if you'd rather keep theming next to the rest of your <head>, eject the :head partial:

bash
bin/rails generate avo:eject --partial :head
erb
<%# app/views/avo/partials/_head.html.erb - append in the file %>
<style>
  .top-navbar {
    --top-navbar-background: #1e3a5f;
    --top-navbar-content: #dbeafe;
  }

  .avo-sidebar {
    --sidebar-background: #f5f7fa;
    --sidebar-border: #d6dbe2;
  }
</style>

Avo renders the :head partial after its bundled stylesheets, so these take precedence too. The full list of component variables, with defaults and descriptions, lives in the CSS variables reference.

Set a background image ​

The main content panel takes a solid color from --color-main-content-background. For an image or gradient instead, there's no config option — set background-image on .main-content in avo-overrides.css. It layers over that color, so keep an opaque image or leave the color as the fallback for any area the image doesn't cover:

css
/* app/assets/stylesheets/avo-overrides.css */
.main-content {
  background-image: url("/my-background.png"); /* a file in public/, or a full URL */
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

/* Or a gradient — no asset needed: */
/* .main-content { background-image: linear-gradient(135deg, #dbeafe, #fce7f3); } */

Scope a different image per scheme with .dark .main-content { background-image: … }.

Style your custom UI ​

Theming variables cover Avo's own screens. When you build custom tools, custom fields, or resource tools, you're writing your own markup — style it with Tailwind utility classes or your own CSS:

  • Tailwind classes in custom UI — the TailwindCSS integration compiles utility classes used in your extension points, so classes that aren't in Avo's precompiled bundle still work.
  • Your own stylesheets and scripts — Asset handling covers loading custom CSS/JS through your app's pipeline; the JavaScript guide covers adding behavior with Stimulus.

Eject a view for full control ​

When even CSS can't express what you need — you want to change the markup itself, not just its styling — eject the view or partial into your app and rewrite it. This is the deepest level of control Avo offers.

bash
bin/rails generate avo:eject --component Avo::Views::ResourceIndexComponent

You own ejected views from here on

An ejected file is a frozen copy of Avo's markup at the moment you generated it. Avo no longer controls it, so improvements and fixes shipped in later versions won't reach your copy — you're accountable for reviewing Avo's changes on each upgrade and porting anything you want into your ejected file. Prefer the CSS-variable and Tailwind layers above whenever they can do the job; reach for ejecting only when they genuinely can't.