One dial for your gray
Every neutral surface in Petal Components - page backgrounds, panels, borders, muted text - is written against the `gray` ramp. `gray` is a semantic name, not Tailwind's gray: your app decides what it points at, in one block, in your own `app.css`. Change the target and the entire library follows, light and dark.
@theme inline {
--color-gray-50: var(--color-slate-50);
--color-gray-100: var(--color-slate-100);
--color-gray-200: var(--color-slate-200);
--color-gray-300: var(--color-slate-300);
--color-gray-400: var(--color-slate-400);
--color-gray-500: var(--color-slate-500);
--color-gray-600: var(--color-slate-600);
--color-gray-700: var(--color-slate-700);
--color-gray-800: var(--color-slate-800);
--color-gray-900: var(--color-slate-900);
--color-gray-950: var(--color-slate-950);
}
Swap `slate` for `zinc`, `neutral`, `stone`, or any 11-step ramp of your own, and you have rethemed the app. Without the block you get the shipped default, which is zinc: a colourless neutral, the same call shadcn made. Cool-blue apps want `slate`, warm apps want `stone`. There is nothing to configure in Elixir and no theme object at runtime - it is CSS variables all the way down.
You can try every neutral live in the dev playground (clone petal_components and run `mix run dev.exs`) - the `gray` dial in the top bar restyles the whole page, and the theme lives in the URL.
The materials
Dark mode is built from two materials, and knowing which is which explains every colour you see:
Panels - pages, cards, dropdown menus, modals, tables - are opaque steps of your `gray`. Point gray at slate and panels are slate.
The ghost material - inputs, hover fills, hairline borders, the translucent layer that sits on top of panels - is your `gray` at low opacity. Since v4.6.2 it is carried on the ramp's most colourful step, so it picks up your neutral's temperature instead of reading as colourless next to it. A slate app gets faintly slate inputs; a stone app gets warm ones. On zinc or neutral there is no hue to carry and it looks exactly as it always did.
Neither material is something you set directly. Both derive from the one gray block above - that is the point.
Radius
One custom property drives corner rounding across the library - buttons, inputs, badges, modals, dropdowns, tooltips:
:root {
--pc-radius: 0.625rem; /* the shipped default */
}
/* sharp, industrial */
:root { --pc-radius: 0; }
/* soft, friendly */
:root { --pc-radius: 1rem; }
Components that need a larger or smaller rounding than their neighbours derive it from the same token, so the whole app stays proportionate when you change it.
Primary and the semantic ramps
Colour works the same way as gray: semantic ramp names that your app points at real ramps. `primary`, `secondary`, `success`, `danger`, `warning` and `info` each map to an 11-step ramp in the same `@theme inline` block:
@theme inline {
--color-primary-50: var(--color-blue-50);
/* ... one line per step ... */
--color-primary-950: var(--color-blue-950);
--color-success-500: var(--color-green-500);
--color-danger-500: var(--color-red-500);
/* etc */
}
Want the shadcn look? Point `primary` at a monochrome ramp - near-black solids in light mode, white in dark. The look lives in the token values, not in component special cases, which is why one block gets you there.
The one rule
If you write your own components alongside ours (or have an AI assistant write them), there is a single rule that keeps everything on the dial: surfaces use the `gray` ramp, and nothing references a literal palette name. The moment something says `zinc-800` or `slate-900` by name, it stops listening to your theme and will stick out the first time you change it. We learned this the hard way - a handful of our own dark surfaces hardcoded `zinc` and ignored remapped grays until v4.6.1.
Everything here also holds for Petal Pro: Pro's own components are written against the same ramps, so one block rethemes the whole app - library and Pro chrome together.