Customizing
Every PhoenixPaper component takes a class attribute for your own Tailwind utilities. It adds to the component's built-in classes, it doesn't replace them. This page covers when that's enough, when you need Tailwind's ! modifier, and when an attr or paperize={false} is the better tool. For colors, see the Theming guide.
class adds, it doesn't replace
Your class is appended to the component's own classes, with no merging step. When yours and a built-in one set the same CSS property (two background colors, two flex directions), both end up on the element, and Tailwind decides the winner by the order the utilities appear in the generated stylesheet, not by the order you wrote them. So the override silently loses about half the time.
An elevated Card, three ways to give it the filled background
The first one doesn't change: an elevated card already renders bg-pp-surface-container-low, which comes after -highest in Tailwind's stylesheet. The ! version wins every time, but only replaces the one property. The attr is the real fix: it switches the whole look, and nothing conflicts.
Show code
<%!-- No change: the elevated card's own bg-pp-surface-container-low wins --%>
<.pp_card class="bg-pp-surface-container-highest">...</.pp_card>
<%!-- Wins: ! makes it !important (the shadow stays) --%>
<.pp_card class="!bg-pp-surface-container-highest">...</.pp_card>
<%!-- Best: the attr, no conflict --%>
<.pp_card variant="filled">...</.pp_card>
Adding utilities: just use class
Anything the component doesn't already set is safe as plain class: layout and spacing around it (margins, width, max-width, grid placement), positioning, a cursor, a transition. Nothing to override, so nothing to lose.
Width and margin the components don't set
Show code
<%!-- Nothing built in to conflict with: plain class --%>
<.pp_button class="w-full">Full width</.pp_button>
<.pp_card class="mt-2 max-w-sm">
<:title>max-w-sm</:title>
A card capped at a width, with some top margin.
</.pp_card>
Replacing a built-in utility: the ! modifier
To change something the component already sets (its background, text color, padding, radius, display), prefix your utility with ! to make it !important. It then wins regardless of stylesheet order. Variants go in front of it: hover:!bg-rose-700, md:!px-8. Tailwind v4 also accepts the suffix form, bg-rose-600!; both work.
A filled Button, recolored
Use it for the property you're replacing, not everything: a stray ! also beats your own responsive and state variants later. If you're reaching for several on one component, an attr or paperize={false} is probably the better tool.
Show code
<%!-- Replacing the button's own background/text color: ! on each --%>
<.pp_button class="!bg-rose-600 !text-white hover:!bg-rose-700">
Delete
</.pp_button>
<%!-- Same for any built-in utility, e.g. the outlined button's radius --%>
<.pp_button variant="outlined" class="!rounded-none">Square</.pp_button>
Reach for an attr first
Most of what people try to override through class has an attr that does it properly, works with dark mode and the theme tokens, and never conflicts. Check the component's options table before writing an override.
| Instead of class=... | Use |
|---|---|
class="rounded-lg" on pp_button
|
shape="square"
|
class="bg-pp-surface-container ..." on pp_card
|
variant="filled"
|
class="bg-pp-secondary-container ..." on pp_button
|
variant="tonal"
|
class="border ..." on pp_button
|
variant="outlined"
|
class="text-xs px-2" on pp_button
|
size="xs"
|
class="fixed inset-0" on pp_dialog
|
variant="fullscreen"
|
class="size-4" on pp_icon
|
size="sm"
|
class="fixed bottom-6 right-6" on pp_fab
|
position="fixed" class="bottom-6 right-6"
|
class="text-pp-on-primary" on a button in a colored bar
|
color="inherit"
|
paperize={false}: start from scratch
When you want a component's behavior but none of its look, pass paperize={false}. Every built-in class is dropped and only your class renders, so there's nothing to override and no ! needed. The structure the component needs to work (a checkbox's hidden input, a link vs. button root, ARIA attributes) stays.
Same component, paperize on and off
Show code
<.pp_button
paperize={false}
class="rounded-md border-2 border-dashed px-3 py-1 font-mono text-sm"
>
Fully custom
</.pp_button>
Styling inside a component
class lands on the component's root element. For the parts inside it, you have three options, in order of preference.
Show code
<%!-- 1. A dedicated attr --%>
<.pp_menu id="account-menu" trigger_class="ms-auto">
...
</.pp_menu>
<%!-- 2. Your own markup in a slot --%>
<.pp_card>
<:title><span class="text-pp-primary">Billing</span></:title>
...
</.pp_card>
<%!-- 3. A child variant on the root (! if it collides) --%>
<.pp_button variant="outlined" class="[&_svg]:!size-4">
<:start_icon><.pp_icon name="hero-trash" /></:start_icon>
Delete
</.pp_button>
Site-wide tweaks in app.css
To change a component everywhere at once, target its marker instead of editing every call site: each component renders a data-pp-component attribute (button, card, list-item, ...). Put the rule after the phoenix_paper import in app.css. For colors, change the --color-pp-* tokens instead (see Theming): they reach every component, in light and dark.
Show code
/* assets/css/app.css, after the phoenix_paper import */
/* Every card: a thin outline */
[data-pp-component="card"] {
border: 1px solid color-mix(in srgb, var(--color-pp-outline) 20%, transparent);
}
/* Every button: no uppercase-ish tracking */
[data-pp-component="button"] {
letter-spacing: normal;
}
Checklist
Before you write an override: