# Styling & theming

> How to style the wb-* classes Mentionwell emits, override the defaults, or write your own theme from scratch.

Mentionwell's writer emits a small, stable set of class names on its HTML. You have three options for styling:

## 1. Use the default theme (fastest)

```ts
import "mentionwell-reader/styles";
```

Drops in a tasteful default that matches a typical content blog. Inherits your existing font, but provides spacing, code blocks, callouts, image captions.

## 2. Override the defaults

The default stylesheet uses CSS custom properties:

```css
:root {
  --wb-text: #111;
  --wb-text-muted: #555;
  --wb-accent: #2a72ff;
  --wb-bg-code: #f6f8fa;
  --wb-radius: 12px;
  --wb-line-height: 1.7;
  /* ... */
}
```

Override these in your global CSS to match your brand without forking the stylesheet.

## 3. Write your own (most flexible)

Don't import `mentionwell-reader/styles`. Instead, target the class names directly. The complete list:

```text
.wb-article       outermost wrapper
.wb-header        title + meta block
.wb-section       major content section
.wb-tldr          TL;DR card
.wb-toc           table of contents
.wb-faq           FAQ accordion
.wb-figure        image + caption
.wb-callout       inline callout
.wb-code          code block wrapper
.wb-cta           inline CTA button
```

Plus standard `<h2>`, `<h3>`, `<p>`, `<ul>`, `<ol>`, `<blockquote>`, `<table>`, `<a>`, `<img>` — style them however your site already does.

## Dark mode

The default theme respects `prefers-color-scheme`. To force light or dark, override the relevant custom properties inside a `[data-theme="dark"]` (or your own scope) block.

## Tailwind

Apply `@apply` to the `.wb-*` classes in a global stylesheet, or use the official `@tailwindcss/typography` plugin and wrap rendered content in `<article class="prose">`.

## Common pitfalls

- **Missing `wb-article-host` wrapper.** Every `.wb-callout`, `.wb-youtube`, `.wb-quote`, `<thead>`, etc. style is scoped to this class. If you render `post.html` inside a wrapper without `className="wb-article-host"`, callouts appear as bare asides, YouTube cards have no border, and table headers lose their dark background. Always wrap: `<div className="wb-article-host" dangerouslySetInnerHTML={{ __html: post.html }} />`. You can layer your own classes alongside (`className="wb-article-host blog-content"`).
- **Forgetting to import `mentionwell-reader/styles`.** The `wb-article-host` wrapper is useless without the stylesheet. In your blog layout: `import "mentionwell-reader/styles";` BEFORE any destination CSS so destination overrides win on shared selectors.
- **Generic CSS selectors clobbering platform blocks.** Rules like `.blog-content aside { border-left: red }`, `.blog-content [class*="callout"] { background: cream }`, `.blog-content th { background: cream }`, or `.blog-content blockquote { border-left: red }` will hit the platform's `.wb-callout`, `<thead>`, and `.wb-quote` and either stamp red bars on top of the styled callouts or crash the table-header contrast (cream-on-cream invisible text). Add `:not()` guards: `.blog-content aside:not([class*="wb-"])`, `.blog-content blockquote:not(.wb-quote)`, `.blog-content:not(.wb-article-host) th`. The platform's contrast safety floor uses `!important` on TL;DR / CTA / table-header colors, but every other wb-* element is fair game for accidental overrides.
- **Duplicate chrome blocks.** `post.html` already contains `<aside class="wb-tldr">`, `<section class="wb-faq">`, `<aside class="wb-author">`, `<aside class="wb-cta">`, `<aside class="wb-footer-cta">`, `<nav class="wb-toc">`, `<header class="wb-header">`, and `<figure class="wb-hero">`. If you ALSO render bespoke versions from `post.tldr`, `post.faqs`, etc., everything renders twice (the live "Key takeaways shown twice" / "FAQ shown twice" bug). Either drop the bespoke versions, or strip the platform versions from `post.html` before render with a regex pass — see the [duplicate chrome guide](#duplicate-chrome) below.
- **Inline CTA placed too early.** A common mistake is `html.indexOf("</h2>")` to inject a CTA after the first H2. The first `</h2>` is the TL;DR's "Key takeaways" h2 — your CTA renders inside the takeaways box with broken contrast. Worse: a CTA right after section 1's heading interrupts the reader before they're invested. Inject the CTA as a peer block AFTER the closing `</section>` of the section near `floor(n/2)` of the body sections. Count `<section class="wb-section">` blocks first, find the middle one, insert after its `</section>`.
- **Sticky in-article elements (TOC, share rail) covered by the masthead.** If the destination has a sticky site header at `top: 0`, your in-article sticky element needs `top: ~6.5rem` (or whatever the masthead height is), `max-height: calc(100vh - 8rem); overflow-y: auto;`, and `z-index: 1` (below the masthead). Otherwise the masthead overlaps the TOC label every time the page scrolls.
- **`next/image` rejecting hostnames.** AI-generated featured images come from `*.fal.media`, stock photos from `images.unsplash.com`, uploaded assets from `*.cloudinary.com` / `*.supabase.co`, YouTube poster frames from `i.ytimg.com`. Add ALL of them to `next.config.ts` `images.remotePatterns` up front — section "Configure image hostnames" of the Next.js quickstart has the canonical block to paste.
- **Build hangs on cold Mentionwell API.** Wrap `fetch` calls inside your reader helper with a 10s `AbortController` timeout, and add `export const dynamic = "force-dynamic"` on the sitemap route. Without these guards a slow API cold start can exceed Next.js's 60s per-route prerender budget and fail the entire Vercel deploy.
- **Env vars set locally but not on the host.** Posting to `/blog/:slug` returning 404 with `getPost` returning null is almost always missing `MENTIONWELL_API_BASE` / `MENTIONWELL_API_KEY` on the production host. Check Vercel/Netlify/Railway environment-variable settings, not just `.env.local`. Redeploy after adding env vars — they only apply to NEW builds.
- **Double sanitisation.** `post.html` is already safe-to-inject. Don't run it through DOMPurify a second time — you'll strip your own headings.
- **Heading collision.** If your page already has an `<h1>`, Mentionwell's article `<h1>` will be a second one. Two options: drop your page-level `<h1>` (the article title is already an h1), or post-process the HTML to demote — e.g. `html.replace(/<h([1-5])\b/g, (_m, n) => `<h${Number(n) + 1}`)`.
- **Image hosting.** Featured images are hosted on Mentionwell's CDN by default. If you mirror them to your own CDN, rewrite `featuredImage` and `<img src>` inside `prepareArticleHtml`.

## Duplicate chrome — strip platform versions if you build bespoke ones

Two valid render strategies:

**A. Render `post.html` as-is.** Simplest. Drop in `<div className="wb-article-host" dangerouslySetInnerHTML={{ __html: post.html }} />` and you get the platform's TL;DR, FAQ, CTA, author block, etc. styled out of the box.

**B. Build bespoke versions from structured fields, strip the platform's versions from `post.html`.** Use this when you want the chrome to use your design system's components (your own breadcrumb, your own newsletter form). Run this strip pass before render:

```ts
function stripPlatformChrome(html: string): string {
  return html
    .replace(/<aside\b[^>]*class="[^"]*\bwb-tldr\b[^"]*"[^>]*>[\s\S]*?<\/aside>/gi, "")
    .replace(/<section\b[^>]*class="[^"]*\bwb-faq\b[^"]*"[^>]*>[\s\S]*?<\/section>/gi, "")
    .replace(/<aside\b[^>]*class="[^"]*\bwb-(?:cta|footer-cta)\b[^"]*"[^>]*>[\s\S]*?<\/aside>/gi, "")
    .replace(/<aside\b[^>]*class="[^"]*\bwb-author\b[^"]*"[^>]*>[\s\S]*?<\/aside>/gi, "")
    .replace(/<nav\b[^>]*class="[^"]*\bwb-toc\b[^"]*"[^>]*>[\s\S]*?<\/nav>/gi, "")
    .replace(/<header\b[^>]*class="[^"]*\bwb-header\b[^"]*"[^>]*>[\s\S]*?<\/header>/gi, "")
    .replace(/<figure\b[^>]*class="[^"]*\bwb-hero\b[^"]*"[^>]*>[\s\S]*?<\/figure>/gi, "");
}
```

Now render `{ __html: stripPlatformChrome(post.html) }` and use `post.tldr`, `post.faqs`, `post.author`, etc. for your bespoke blocks.

**Never strip `.wb-callout`, `.wb-youtube`, `.wb-quote`, `.wb-citations`, `.wb-steps`, or tables.** Those have no structured-field equivalents in the API — stripping them removes content the article needs.

**Never mix strategies for the same block.** If you render bespoke FAQ AND leave the platform's `wb-faq` in `post.html`, the FAQ section appears twice on the page.


---

Canonical URL: https://mentionwell.com/docs/styling
Live HTML version: https://mentionwell.com/docs/styling
Section: SDK reference
Site index for AI ingestion: https://mentionwell.com/llms.txt
Full reference: https://mentionwell.com/llms-full.txt
