Oolvay’s styling system is split across several focused CSS files, all imported from app/styles/globals.css. Each file has a single responsibility so that changes stay local. Editing the scrollbar style does not require touching the theme, and swapping the theme does not break the prose overrides.
This page explains what each file does, why it is structured the way it is, and how to make the most common customizations, including swapping the entire visual theme with tweakcn and changing the font stack.
| File | What it owns |
|---|---|
globals.css | Theme tokens, Tailwind bridge, base resets, all other imports |
theme-transitions.css | Smooth animated transitions when switching between light and dark |
fumadocs-theme.css | Remaps Fumadocs color tokens to your shadcn tokens |
metal.css | Gold and silver gradient utility classes |
prose.css | Typography overrides for Tailwind Typography and Fumadocs |
scrollbars.css | A pretty-scrollbar utility for styled WebKit scrollbars |
switch.css | shadcn Switch component state overrides |
globals.cssThis is the entry point for all styles. It does four things.
@import "tailwindcss"
0 / 2,000 characters
This file defines design tokens as CSS custom properties on :root (light mode) and .dark (dark mode). Every color in the app (backgrounds, text, borders, sidebar, charts) is a token here. Colors use the oklch() color space, which gives more perceptually uniform results than hsl() when generating shades and gradients.
This is what makes classes like bg-background, text-primary, and border-border work. Without this block, Tailwind would not know what your custom properties mean.
@theme inline {
--color-background: var(--background);
--color-primary: var(--primary);
/* and so on for every token */
}Every element gets border-border and outline-ring/50 by default. The body gets bg-background text-foreground.
The imports in globals.css use relative paths like ./fumadocs-theme.css
rather than the @/ alias. CSS @import is processed by PostCSS before
Next.js resolves that alias, so @/ is not available in CSS files. Relative
paths are the correct approach here.
Several theme repositories and editors exist for shadcn/ui. tweakcn is one of them. It lets you adjust colors, radius, shadows, and fonts interactively and then exports the result as a CSS block ready to drop into globals.css. This is the fastest way to change Oolvay’s visual identity without hand-editing OKLCH values.
This is also why local style additions like metal.css, scrollbars.css, and switch.css live in separate files rather than inline in globals.css. Keeping them separate means the tweakcn paste never touches them.
Generate your theme
Go to tweakcn.com, use the editor to build out your theme, then click Code.
Paste into globals.css
Copy everything tweakcn gives you and paste it into app/styles/globals.css, replacing all existing content below the @import lines at the top. The imports must be preserved exactly as they are. Only tailwindcss is duplicated, remove one.
Update theme-transitions.css
This is the step most people miss. theme-transitions.css registers every color token as a @property with an initial-value. That initial value must match your new light-mode :root values. If it does not, the browser interpolates from the wrong starting color on the very first page load, producing a brief color flash.
Open app/styles/theme-transitions.css and update the initial-value of each @property to match the corresponding value from your new :root block. The property names map one-to-one.
@property --background {
syntax: "<color>";
inherits: true;
initial-value: oklch(/* your new --background light value */);
}You do not need to touch the transition: block at the bottom of the file.
If you skip this step, transitions will still work after the first toggle. But
on the very first page load the browser has no prior value to interpolate from
and falls back to the stale initial-value, producing a brief incorrect color
flash before the correct theme paints.
fumadocs-theme.css needs no changes
fumadocs-theme.css maps Fumadocs’ own --color-fd-* tokens to your shadcn tokens using var() references. Because it points to your tokens by reference rather than by hardcoded values, Fumadocs components automatically pick up whatever you put in globals.css. You do not need to edit this file after a tweakcn swap.
Oolvay ships with three font roles. --font-sans is Manrope, --font-serif is Playfair Display, and --font-mono is Fira Code. Changing any of these requires two coordinated edits, one in globals.css and one in app/layout.tsx. Both must be updated together. If the CSS token names a font that is not loaded in the layout, the browser falls back to the system font silently with no error.
Find your font's export name
Find the font on fonts.google.com and note its exact export name for next/font/google.
The export name is the family name with spaces replaced by underscores. For example:
DM Sans → DM_SansFira Code → Fira_CodePlayfair Display → Playfair_DisplayUpdate app/layout.tsx
Import your font from next/font/google and define it with the variable name that matches the token in globals.css.
The variable value is what connects the Next.js font loader to the CSS token. Keep it exactly as-is and only change the import name and font options.
For example, to swap Manrope for DM Sans:
// Before
import { Manrope, Fira_Code } from "next/font/google"
const sans = Manrope({
subsets: ["latin"],
variable: "--font-sans",
display: "swap",
weight: ["300", "400", "500", "600", "700", "800"],
})
// After
import { DM_Sans, Fira_Code } from "next/font/google"
const sans =
Update globals.css
Update --font-sans (or --font-serif / --font-mono) in both the :root and .dark blocks so the new font appears first in the stack.
:root {
--font-sans: DM Sans, ui-sans-serif, sans-serif, system-ui;
}
.dark {
--font-sans: DM Sans, ui-sans-serif, sans-serif, system-ui;
}Playfair Display () is defined in but is not imported in by default. It exists as a token for components that opt into the serif face explicitly. To use it globally, add a import to following the same pattern as the sans and mono fonts, and add its variable to the on .
This file is responsible for the smooth color animation when a user switches between light and dark mode.
CSS cannot animate custom properties by default. The browser has no way to know that --background holds a color value and not an arbitrary string. The @property at-rule solves this by explicitly registering a property with a declared syntax, which tells the browser it is safe to interpolate it as a color.
@property --background {
syntax: "<color>";
inherits: true;
initial-value: oklch(0.975 0.018 92);
}Once every token is registered this way, a single transition: rule on :root animates all of them simultaneously at 0.6s ease.
The .no-transitions class prevents this animation from running on the initial page load. You will notice it applied to <html> in layout.tsx. Without it, the browser would animate from the initial-value to the correct theme on every hard reload, which reads as a flash rather than a transition. The ThemeFlashGuard component removes the class after the first committed paint using requestAnimationFrame, so transitions are active from the very first user interaction onward.
Fumadocs uses its own set of CSS custom properties, all prefixed --color-fd-*, to style the docs layout, sidebar, search modal, and components. By default these are independent of your shadcn tokens.
fumadocs-theme.css bridges the two systems by pointing every --color-fd-* token at the corresponding shadcn token via var().
.fd-wrapper {
--color-fd-background: var(--background);
--color-fd-primary: var(--primary);
--color-fd-muted: var(--muted);
/* and so on */
}Because these are var() references rather than hardcoded values, the docs automatically inherit whatever theme you have set in globals.css. The search modal renders in a portal outside .fd-wrapper, so the same mappings are also applied to :root and .dark directly to cover that case.
The file also includes a set of targeted overrides for Fumadocs-specific behavior, including sidebar active link color, code block backgrounds via Shiki CSS variables, inline code visibility in dark mode, and card hover states. These do not need to be changed as part of a theme swap.
This file provides four utility classes for gold and silver gradient treatments. They are intentional branded accents, not general-purpose utilities.
| Class | Use |
|---|---|
bg-silver | Silver gradient background with matching border and shadow |
bg-gold | Gold gradient background, dark-mode aware |
text-silver | Silver gradient applied as a text fill via bg-clip-text |
text-gold | Gold gradient applied as a text fill via bg-clip-text |
Apply them like any other Tailwind class.
<span className="text-gold">Premium</span>Because they use @layer components, they compose with Tailwind utilities and can be overridden by utility classes when needed.
Tailwind Typography and Fumadocs both apply their own prose styles to long-form content. This file makes small corrections so those styles behave consistently with Oolvay’s design tokens.
List markers are set to --color-primary so bullet points and ordered list numbers pick up the brand color instead of the default gray.
Horizontal rules use --border to match the rest of the UI rather than Tailwind Typography’s hardcoded color.
Inline code pseudo-elements. Tailwind Typography adds pseudo-elements before and after every <code> element to visually wrap it in backtick characters. Oolvay styles inline code with a background highlight instead, so these pseudo-elements are cleared to prevent them from appearing on top of the highlight.
.prose code::before,
.prose code::after {
content: "";
}Both .prose (Tailwind Typography) and .fd-prose (Fumadocs) selectors are targeted throughout, so corrections apply in both the marketing pages and the docs.
This file defines a single Tailwind utility, pretty-scrollbar, that you can apply to any scrollable container.
@utility pretty-scrollbar {
overflow-y: auto;
/* slim, border-colored, transparent-tracked WebKit scrollbar */
}It is defined with @utility rather than @layer components so that Tailwind v4 treats it as a first-class utility and generates it on demand.
<div className="pretty-scrollbar max-h-96">{/* scrollable content */}</div>pretty-scrollbar only affects WebKit-based browsers (Chrome, Safari, Edge).
Firefox uses a different scrollbar API (scrollbar-width and
scrollbar-color) which is not included here. For most SaaS products the
WebKit coverage is sufficient, but if you need Firefox parity, add those
properties to the same @utility block.
The shadcn Switch component ships without its checked and unchecked states wired to your theme tokens, and its thumb translation can be slightly off depending on the version. This file corrects both.
[data-slot="switch"][data-state="checked"] {
background-color: var(--primary);
}
[data-slot="switch"][data-state="unchecked"] {
background-color: var(--input);
}These target shadcn’s own data-slot and data-state attributes rather than class names, so they remain stable across shadcn updates as long as those attributes do not change.
You do not need to touch this file unless you want the switch to use a color other than --primary when checked. If so, replace var(--primary) with any other token from globals.css.
--font-serifglobals.csslayout.tsxPlayfair_Displaylayout.tsxclassNamehtml