Skip to content
Chapter 18Lesson 7

Quiz - Tailwind v4 inside the React component

Quiz progress

0 / 0

A <Badge> is supposed to take a brand color and tint itself. It renders fine in the editor, but in the deployed build every badge comes out with no background at all — and there’s no error anywhere:

const Badge = ({ tone }: { tone: 'red' | 'green' }) => (
<span className={`bg-${tone}-500`}>{tone}</span>
);

What went wrong?

Tailwind scans your source as plain text at build time and never runs your code, so it only sees the literal string bg-${tone}-500 — never bg-red-500 or bg-green-500. Those classes are assembled at runtime, so their CSS is never generated. The fix is a lookup map that contains each full class name as literal source text.

Template literals aren’t allowed in className — React strips interpolated class strings during the production build for security, which is why it works in dev but not in the deployed bundle.

The classes exist, but bg-red-500 and bg-green-500 aren’t on this project’s theme scale, so the build drops them. Switching to semantic tokens like bg-primary would emit them.

You add this to app/globals.css and reach for bg-brand in a component, but the utility doesn’t exist — IntelliSense doesn’t even suggest it:

@theme {
--brand-color: oklch(0.62 0.19 256);
}

What’s the first thing to check?

The namespace prefix. A @theme token mints utilities by its namespace, and brand isn’t one Tailwind recognizes — there’s no --brand-* family. Lead with the real namespace: --color-brand mints bg-brand, text-brand, border-brand. Same name, same value; only the order is wrong.

The token needs to be registered as a utility. Defining a CSS variable isn’t enough on its own — you also have to declare @utility bg-brand { ... } so Tailwind knows to generate the class.

The value. oklch(...) colors can’t be used in @theme; tokens there must be hex or RGB, and the OKLCH value is silently rejected, taking the whole token with it.

A reusable <Button> sets px-4 internally. A consumer passes className="px-8" to make it wider. With the naive className={`... px-4 py-2 ${className}`}, both px-4 and px-8 end up on the element. Why is “the consumer’s px-8 comes later in the string, so it wins” the wrong mental model — and what actually fixes it?

Class order in the class attribute doesn’t decide the winner — the cascade does, and among equal-specificity rules the last one in the generated stylesheet wins, in an order Tailwind fixes independently of your markup. So the outcome is build-dependent and silent. cn() fixes it because tailwind-merge deletes the losing px-4, leaving only px-8 on the element.

The model is right about order but cn() is still needed for performance — concatenation rebuilds the whole string on every render, while cn() memoizes the result so the merge runs only once.

It’s wrong because the first class in the attribute always wins in CSS, so px-4 beats px-8. cn() fixes it by reversing the string so the consumer’s class lands first.

The model is wrong because px-4 and px-8 don’t actually conflict — they set different properties. cn() simply keeps both, and the browser adds the two paddings together.

Here’s the corrected <Button>. Why must className be the last argument to cn() — and what breaks if you move it first?

tailwind-merge keeps the last of each conflicting utility, so last position is exactly what lets a consumer’s px-8 delete the component’s px-4. Move className first and the component’s own defaults land last and win — consumer overrides silently stop working.

Argument order is just a style convention; tailwind-merge resolves conflicts by specificity, not position, so className wins from anywhere in the call. Putting it last is purely for readability.

className must be last because cn() only reads its final argument for consumer overrides and treats every earlier argument as internal-only defaults that can’t be overridden.

A teammate is building a settings panel and reaches for useState + handlers for each of these. Which ones can be done with a Tailwind variant instead — no state, no handler — because the DOM already tracks the fact? Select all that apply.

A form’s outer border turns red while any field inside it is invalid.

A “Delete” button hidden inside a card fades in when the card is hovered.

A radio-card highlights when the radio it wraps is the checked one.

A “Saved!” banner appears two seconds after the server confirms the write succeeded.

You write aria-current:font-semibold on the active nav link to bold it, but it never applies — no error, no style. The element really does carry aria-current="page". What’s wrong?

aria-current has no built-in shorthand variant (its value can be page, step, location, …, so there’s no single boolean case to target), so aria-current: compiles to nothing. You need the arbitrary form: aria-[current=page]:font-semibold.

ARIA attributes can’t be styled by Tailwind at all — they exist only for assistive tech. You have to mirror the active state into a data-active attribute and write data-active:font-semibold.

aria-current="page" is a string, but the aria-current: variant matches the boolean true, so it only fires for aria-current="true". Set the attribute to "true" and the shorthand works.

Across your app a single card surface keeps appearing as bg-white dark:bg-slate-800, and the dimmer “last edited” text shows up as dark:text-slate-400 on two different pages. The chapter’s dark-mode model says one of these patterns should stay an inline dark: and the other should become a semantic token. Which is which, and what’s the rule?

Both should become tokens. The test is reuse, not the kind of property: the surface recurs across components and the dimmer text is already duplicated on two pages — both are past the two-component threshold, so promoting them gives each one source of truth and stops the darks drifting.

Both should stay inline dark:. Per-utility dark: is the senior default; semantic tokens are an advanced opt-in you reach for only once a design system is formally documented.

The surface becomes a token (backgrounds always do); the dimmer text stays inline, because text color is a one-off styling concern that doesn’t belong in the shared palette.

A theme setup reads the saved theme inside a useEffect and adds .dark to <html> after the component mounts. A user with a dark-set OS reloads and sees a white flash that snaps to dark. Why can’t the effect-based approach ever fix this, and what does?

Effects run after hydration, which runs after the first paint — so by the time the effect sets the class, the wrong (light) pixels are already on screen. You can’t repair a paint that already happened; you have to set the class before paint, with a synchronous <head> script (what next-themes injects for you).

The effect runs fine, but useEffect can’t mutate <html> because it sits outside React’s root — switching to a useLayoutEffect that targets document.documentElement sets the class early enough to kill the flash.

The server renders .dark correctly, but the effect removes it on mount and re-adds it a frame later. Deleting the redundant effect entirely lets the server-rendered class stand, with no flash.

The root layout for the theme setup carries suppressHydrationWarning on <html>. Which statements correctly explain why it belongs there — and only there? Select all that apply.

The pre-paint inline script rewrites <html>’s class before React hydrates, so React is guaranteed to find a class the server’s HTML never had. The mismatch is intentional, not a defect, and this prop acknowledges it.

It’s shallow — it silences mismatch warnings for <html>’s own attributes only, not its descendants, so it can’t mask a real mismatch deeper in the tree.

It makes React skip hydrating <html> entirely, so its class can never conflict.

Theme-token utilities like bg-background need it too, or they won’t match between server and client render.

Quiz complete

Score by topic