# Pixel Paragraph Inverse

> Paragraph for React and shadcn/ui set in a Geist pixel font, with chosen words switching to a sans or mono typeface.

Source: https://www.cult-ui.com/docs/components/pixel-paragraph-words-inverse

## Example

```tsx title="pixel-paragraph-words-inverse-demo.tsx"
"use client"

import { useState } from "react"

import { PixelParagraphInverse } from "@/components/ui/pixel-paragraph-words-inverse"

/* ─── Constants ─── */

const PIXEL_FONTS = ["square", "grid", "circle", "triangle", "line"] as const
type PixelFont = (typeof PIXEL_FONTS)[number]

const PLAIN_FONTS = ["sans", "mono"] as const
type PlainFont = (typeof PLAIN_FONTS)[number]

const WRAPPER_TAGS = ["p", "span", "div"] as const

const DEFAULT_TEXT =
  "54+ animated components and effects. Free, open source, and built to drop into any shadcn/ui project."
const DEFAULT_PLAIN_WORDS = "animated,shadcn/ui"

/* ─── Demo ─── */

export default function PixelParagraphWordsInverseDemo() {
  const [text, setText] = useState(DEFAULT_TEXT)
  const [plainWordsInput, setPlainWordsInput] = useState(DEFAULT_PLAIN_WORDS)
  const [pixelFont, setPixelFont] = useState<PixelFont>("square")
  const [plainFont, setPlainFont] = useState<PlainFont>("sans")
  const [wrapperTag, setWrapperTag] =
    useState<(typeof WRAPPER_TAGS)[number]>("p")

  const plainWords = plainWordsInput
    .split(",")
    .map((w) => w.trim())
    .filter(Boolean)

  return (
    <div className="w-full space-y-8 py-4">
      {/* ── Preview ── */}
      <div className="border-border/40 bg-background flex min-h-[120px] items-center justify-center rounded-lg border p-8">
        <PixelParagraphInverse
          text={text}
          plainWords={plainWords}
          as={wrapperTag}
          pixelFont={pixelFont}
          plainFont={plainFont}
          className="text-muted-foreground max-w-xl text-lg leading-relaxed"
          plainWordClassName="text-foreground font-medium"
        />
      </div>

      {/* ── Controls ── */}
      <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
        {/* Text */}
        <ControlGroup
          label="Paragraph Text"
          className="sm:col-span-2 lg:col-span-3"
        >
          <textarea
            value={text}
            onChange={(e) => setText(e.target.value)}
            rows={3}
            className="border-input placeholder:text-muted-foreground focus-visible:ring-ring w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus-visible:ring-1 focus-visible:outline-none"
            placeholder="Enter paragraph text"
          />
        </ControlGroup>

        {/* Plain Words */}
        <ControlGroup
          label="Plain Words (comma-separated)"
          className="sm:col-span-2 lg:col-span-3"
        >
          <input
            type="text"
            value={plainWordsInput}
            onChange={(e) => setPlainWordsInput(e.target.value)}
            className="border-input placeholder:text-muted-foreground focus-visible:ring-ring h-9 w-full rounded-md border bg-transparent px-3 text-sm shadow-sm focus-visible:ring-1 focus-visible:outline-none"
            placeholder="e.g. animated,shadcn/ui,open source"
          />
          <p className="text-muted-foreground text-xs">
            These words escape the pixel font and render in sans/mono
          </p>
        </ControlGroup>

        {/* Pixel Font */}
        <ControlGroup label="Pixel Font (base text)">
          <div className="flex flex-wrap gap-1.5">
            {PIXEL_FONTS.map((f) => (
              <button
                type="button"
                key={f}
                onClick={() => setPixelFont(f)}
                className={`rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
                  pixelFont === f
                    ? "bg-foreground text-background"
                    : "bg-muted text-muted-foreground hover:bg-muted/80"
                }`}
              >
                {f}
              </button>
            ))}
          </div>
        </ControlGroup>

        {/* Plain Font */}
        <ControlGroup label="Plain Font (escaped words)">
          <div className="flex flex-wrap gap-1.5">
            {PLAIN_FONTS.map((f) => (
              <button
                type="button"
                key={f}
                onClick={() => setPlainFont(f)}
                className={`rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
                  plainFont === f
                    ? "bg-foreground text-background"
                    : "bg-muted text-muted-foreground hover:bg-muted/80"
                }`}
              >
                {f}
              </button>
            ))}
          </div>
        </ControlGroup>

        {/* Wrapper Tag */}
        <ControlGroup label="Wrapper Element">
          <select
            value={wrapperTag}
            onChange={(e) =>
              setWrapperTag(e.target.value as (typeof WRAPPER_TAGS)[number])
            }
            className="border-input focus-visible:ring-ring h-9 w-full rounded-md border bg-transparent px-3 text-sm shadow-sm focus-visible:ring-1 focus-visible:outline-none"
          >
            {WRAPPER_TAGS.map((t) => (
              <option key={t} value={t}>
                {"<" + t + ">"}
              </option>
            ))}
          </select>
        </ControlGroup>
      </div>
    </div>
  )
}

/* ─── Shared control primitives ─── */

function ControlGroup({
  label,
  children,
  className,
}: {
  label: string
  children: React.ReactNode
  className?: string
}) {
  return (
    <div className={`space-y-2 ${className ?? ""}`}>
      <span className="text-muted-foreground block text-xs font-medium tracking-wider uppercase">
        {label}
      </span>
      {children}
    </div>
  )
}
```

Pixel Paragraph Inverse is a React text component for shadcn/ui using Geist pixel fonts. Use it for landing page intros, taglines, or callouts where a few key terms should read as plain text.

## Installation

### CLI

```bash
npx shadcn@latest add @cult-ui/pixel-paragraph-words-inverse
```

### Manual

**Install the `geist` font package.**

```bash
npm install geist
```

**Copy and paste the following code into your project.**

````tsx title="pixel-paragraph-words-inverse.tsx"
/**
 * @module PixelParagraphInverse
 *
 * Renders a paragraph where the base text is in a pixel font and
 * specific words / phrases escape into sans or mono.
 *
 * This is the inverse of `PixelParagraph`: instead of highlighting
 * words *in* pixel, everything is pixel *except* the specified words.
 *
 * Setup — Geist Pixel Fonts with Tailwind CSS
 * =============================================
 *
 * All Geist fonts can be used through CSS variables:
 *
 *   GeistSans:          --font-geist-sans
 *   GeistMono:          --font-geist-mono
 *   GeistPixelSquare:   --font-geist-pixel-square
 *   GeistPixelGrid:     --font-geist-pixel-grid
 *   GeistPixelCircle:   --font-geist-pixel-circle
 *   GeistPixelTriangle: --font-geist-pixel-triangle
 *   GeistPixelLine:     --font-geist-pixel-line
 *
 * 1. Register the font variables in app/layout.js:
 *
 *   ```js
 *   import { GeistSans } from "geist/font/sans";
 *   import { GeistMono } from "geist/font/mono";
 *   import { GeistPixelSquare } from "geist/font/pixel";
 *
 *   export default function RootLayout({ children }) {
 *     return (
 *       <html
 *         lang="en"
 *         className={`${GeistSans.variable} ${GeistMono.variable} ${GeistPixelSquare.variable}`}
 *       >
 *         <body>{children}</body>
 *       </html>
 *     );
 *   }
 *   ```
 *
 * 2. Map the CSS variables in your Tailwind CSS v4 theme (tailwind.css):
 *
 *   ```css
 *   @theme {
 *     --font-sans: var(--font-geist-sans);
 *     --font-mono: var(--font-geist-mono);
 *     --font-pixel-square: var(--font-geist-pixel-square);
 *     --font-pixel-grid: var(--font-geist-pixel-grid);
 *     --font-pixel-circle: var(--font-geist-pixel-circle);
 *     --font-pixel-triangle: var(--font-geist-pixel-triangle);
 *     --font-pixel-line: var(--font-geist-pixel-line);
 *   }
 *   ```
 *
 * Once configured, the `font-pixel-*` and `font-sans` / `font-mono`
 * utility classes used by this component will resolve correctly.
 *
 * @example
 * <PixelParagraphInverse
 *   text="54+ animated components and effects. Free, open source, and built to drop into any shadcn/ui project."
 *   plainWords={["animated", "shadcn/ui"]}
 *   plainFont="sans"
 *   pixelFont="square"
 *   className="text-lg text-muted-foreground"
 * />
 */

import { cn } from "@/lib/utils"

/* ------------------------------------------------------------------ */
/* Font constants                                                      */
/* ------------------------------------------------------------------ */

type PlainFont = "sans" | "mono"
type PixelFont = "square" | "grid" | "circle" | "triangle" | "line"

const PLAIN_FONT_MAP: Record<PlainFont, string> = {
  sans: "font-sans",
  mono: "font-mono",
}

const PIXEL_FONT_MAP: Record<PixelFont, string> = {
  square: "font-pixel-square",
  grid: "font-pixel-grid",
  circle: "font-pixel-circle",
  triangle: "font-pixel-triangle",
  line: "font-pixel-line",
}

/* ------------------------------------------------------------------ */
/* Text-splitting helper                                               */
/* ------------------------------------------------------------------ */

type Segment = { type: "pixel"; text: string } | { type: "plain"; text: string }

/**
 * Splits `text` into alternating pixel / plain segments based on the
 * provided `plainWords`.  Longer phrases are matched first so that
 * "shadcn/ui" wins over a hypothetical "ui" match.
 */
function splitTextByPlainWords(text: string, plainWords: string[]): Segment[] {
  if (plainWords.length === 0) return [{ type: "pixel", text }]

  // Sort by length descending so longer matches take priority
  const sorted = [...plainWords].sort((a, b) => b.length - a.length)

  // Escape regex-special characters in each word
  const escaped = sorted.map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))

  const pattern = new RegExp(`(${escaped.join("|")})`, "g")

  const segments: Segment[] = []
  let lastIndex = 0

  for (const match of text.matchAll(pattern)) {
    const matchStart = match.index ?? 0
    if (matchStart > lastIndex) {
      segments.push({ type: "pixel", text: text.slice(lastIndex, matchStart) })
    }
    segments.push({ type: "plain", text: match[0] })
    lastIndex = matchStart + match[0].length
  }

  if (lastIndex < text.length) {
    segments.push({ type: "pixel", text: text.slice(lastIndex) })
  }

  return segments
}

/* ------------------------------------------------------------------ */
/* PixelParagraphInverse                                               */
/* ------------------------------------------------------------------ */

export interface PixelParagraphInverseProps extends React.ComponentProps<"p"> {
  /** The paragraph text to render. */
  text: string
  /**
   * Words or phrases within `text` to render in a plain (sans/mono) font.
   * Everything else renders in the pixel font.
   * Matching is case-sensitive and longest-match-first.
   */
  plainWords?: string[]
  /** The wrapper element to render. @default "p" */
  as?: "p" | "span" | "div"
  /** The pixel font used for the base text. @default "square" */
  pixelFont?: PixelFont
  /** The plain font for highlighted words. @default "sans" */
  plainFont?: PlainFont
  /** Extra className applied to each plain-word span. */
  plainWordClassName?: string
}

/**
 * Paragraph where the base text is in a pixel font and specific words
 * or phrases escape into a sans or mono font.
 *
 * @example
 * <PixelParagraphInverse
 *   text="54+ animated components and effects. Free, open source, and built to drop into any shadcn/ui project."
 *   plainWords={["animated", "shadcn/ui"]}
 *   plainFont="sans"
 *   pixelFont="square"
 *   className="text-lg text-muted-foreground"
 * />
 */
export function PixelParagraphInverse({
  text,
  plainWords = [],
  as: Tag = "p",
  className,
  pixelFont = "square",
  plainFont = "sans",
  plainWordClassName,
  ...props
}: PixelParagraphInverseProps) {
  const segments = splitTextByPlainWords(text, plainWords)
  const pixelFontClass = PIXEL_FONT_MAP[pixelFont]
  const plainFontClass = PLAIN_FONT_MAP[plainFont]

  return (
    <Tag
      data-slot="pixel-paragraph-inverse"
      className={cn(pixelFontClass, className)}
      {...props}
    >
      {segments.map((segment, index) => {
        const key = `${segment.type}-${segment.text}-${index}`
        return segment.type === "plain" ? (
          <span
            key={key}
            data-slot="plain-word"
            className={cn(plainFontClass, plainWordClassName)}
          >
            {segment.text}
          </span>
        ) : (
          <span key={key}>{segment.text}</span>
        )
      })}
    </Tag>
  )
}
````

**Update the import paths to match your project setup.**

## Font Setup

This component requires the **Geist Pixel fonts**. Follow these steps to configure them in your project.

### 1. Register font variables in your root layout

Import the pixel font variants from `geist/font/pixel` and apply their CSS variable classes to `<body>`:

```tsx title="app/layout.tsx"
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import {
  GeistPixelSquare,
  GeistPixelGrid,
  GeistPixelCircle,
  GeistPixelTriangle,
  GeistPixelLine,
} from "geist/font/pixel";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body
        className={`${GeistSans.variable} ${GeistMono.variable} ${GeistPixelSquare.variable} ${GeistPixelGrid.variable} ${GeistPixelCircle.variable} ${GeistPixelTriangle.variable} ${GeistPixelLine.variable}`}
      >
        {children}
      </body>
    </html>
  );
}
```

### 2. Map CSS variables in your Tailwind CSS theme

```css title="globals.css (Tailwind v4)"
@theme {
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);
  --font-pixel-square: var(--font-geist-pixel-square);
  --font-pixel-grid: var(--font-geist-pixel-grid);
  --font-pixel-circle: var(--font-geist-pixel-circle);
  --font-pixel-triangle: var(--font-geist-pixel-triangle);
  --font-pixel-line: var(--font-geist-pixel-line);
}
```

For **Tailwind v3**, use the `extend` key in `tailwind.config.ts`:

```ts title="tailwind.config.ts (Tailwind v3)"
export default {
  theme: {
    extend: {
      fontFamily: {
        sans: ["var(--font-geist-sans)"],
        mono: ["var(--font-geist-mono)"],
        "pixel-square": ["var(--font-geist-pixel-square)"],
        "pixel-grid": ["var(--font-geist-pixel-grid)"],
        "pixel-circle": ["var(--font-geist-pixel-circle)"],
        "pixel-triangle": ["var(--font-geist-pixel-triangle)"],
        "pixel-line": ["var(--font-geist-pixel-line)"],
      },
    },
  },
};
```

## Usage

```tsx
import { PixelParagraphInverse } from "@/components/ui/pixel-paragraph-words-inverse";
```

### Basic

Base text in pixel, specific words escape to sans:

```tsx
<PixelParagraphInverse
  text="54+ animated components and effects. Free, open source, and built to drop into any shadcn/ui project."
  plainWords={["animated", "shadcn/ui"]}
  plainFont="sans"
  pixelFont="square"
  className="text-lg text-muted-foreground"
/>
```

### Mono Escape

Use mono instead of sans for the escaped words:

```tsx
<PixelParagraphInverse
  text="Build beautiful interfaces with pixel-perfect typography."
  plainWords={["beautiful", "pixel-perfect"]}
  plainFont="mono"
  className="text-lg text-muted-foreground"
/>
```

### Different Pixel Fonts

Change the base pixel font:

```tsx
<PixelParagraphInverse
  text="Grid-based pixel typography with highlighted words."
  plainWords={["highlighted"]}
  pixelFont="grid"
  plainFont="sans"
  className="text-lg"
/>
```

### Styling Plain Words

Apply custom classes to the escaped words:

```tsx
<PixelParagraphInverse
  text="Important words can be visually distinct from the base text."
  plainWords={["Important", "visually distinct"]}
  plainFont="sans"
  plainWordClassName="text-foreground font-semibold"
  className="text-lg text-muted-foreground"
/>
```

### As a Different Element

Render as a `<span>` or `<div>` instead of `<p>`:

```tsx
<PixelParagraphInverse
  as="span"
  text="Inline pixel text with plain escapes."
  plainWords={["plain escapes"]}
  className="text-sm"
/>
```

## API Reference

### PixelParagraphInverse Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` | `string` | — | The paragraph text to render |
| `plainWords` | `string[]` | `[]` | Words/phrases to render in plain (sans/mono) font. Matching is case-sensitive, longest-first |
| `as` | `"p" \| "span" \| "div"` | `"p"` | The wrapper element to render |
| `pixelFont` | `"square" \| "grid" \| "circle" \| "triangle" \| "line"` | `"square"` | The pixel font used for the base text |
| `plainFont` | `"sans" \| "mono"` | `"sans"` | The font for escaped words |
| `plainWordClassName` | `string` | — | Extra className applied to each plain-word span |
| `className` | `string` | — | Additional CSS classes applied to the wrapper element |

## How It Works

1. The `text` string is split into alternating segments: **pixel** (base) and **plain** (escaped words)
2. Pixel segments render in the chosen pixel font as static text
3. Plain segments render as `<span>` elements with the chosen plain font class (`font-sans` or `font-mono`)
4. Matching is **longest-first** — if `plainWords` contains both `"ui"` and `"shadcn/ui"`, the longer phrase takes priority
5. Matching is **case-sensitive** — `"React"` will not match `"react"`

## Features

- **Inverse design** — base text is pixel, highlighted words are sans/mono
- **Static font assignment** — no hover/cycling complexity, just clean font mixing
- **Server-compatible** — no `"use client"` needed, no state or effects
- **Five pixel font variants** — Square, Grid, Circle, Triangle, and Line
- **Longest-match-first** — multi-word phrases match correctly without partial collisions
- **Composable** — renders as `<p>`, `<span>`, or `<div>` with full className support

## Pixel Paragraph vs Pixel Paragraph Inverse

| Feature | Pixel Paragraph | Pixel Paragraph Inverse |
| --- | --- | --- |
| Base text | Normal font (sans) | Pixel font |
| Highlighted words | Pixel font | Sans/mono font |
| Best for | Accenting key words in normal body text | Full pixel paragraphs with readable escapes |
