# Pixel Paragraph

> Paragraph for React and shadcn/ui that renders chosen words in a Geist pixel font to highlight key terms in hero copy.

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

## Example

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

import { useState } from "react"

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

/* ─── Constants ─── */

const PIXEL_FONTS = ["square", "grid", "circle", "triangle", "line"] as const
type PixelFont = (typeof PIXEL_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_PIXEL_WORDS = "animated,shadcn/ui"

/* ─── Demo ─── */

export default function PixelParagraphWordsDemo() {
  const [text, setText] = useState(DEFAULT_TEXT)
  const [pixelWordsInput, setPixelWordsInput] = useState(DEFAULT_PIXEL_WORDS)
  const [font, setFont] = useState<PixelFont>("square")
  const [wrapperTag, setWrapperTag] =
    useState<(typeof WRAPPER_TAGS)[number]>("p")

  const pixelWords = pixelWordsInput
    .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">
        <PixelParagraph
          text={text}
          pixelWords={pixelWords}
          as={wrapperTag}
          font={font}
          className="text-muted-foreground max-w-xl text-lg leading-relaxed"
          pixelWordClassName="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>

        {/* Pixel Words */}
        <ControlGroup
          label="Pixel Words (comma-separated)"
          className="sm:col-span-2 lg:col-span-3"
        >
          <input
            type="text"
            value={pixelWordsInput}
            onChange={(e) => setPixelWordsInput(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 render in a pixel font while the rest stays in the
            normal typeface
          </p>
        </ControlGroup>

        {/* Pixel Font */}
        <ControlGroup label="Pixel Font">
          <div className="flex flex-wrap gap-1.5">
            {PIXEL_FONTS.map((f) => (
              <button
                type="button"
                key={f}
                onClick={() => setFont(f)}
                className={`rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
                  font === 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 is a React text component for shadcn/ui using Geist pixel fonts. Use it to highlight key terms in hero descriptions, feature copy, or product taglines.

## Installation

### CLI

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

### Manual

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

```bash
npm install geist
```

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

````tsx title="pixel-paragraph-words.tsx"
/**
 * @module PixelParagraph
 *
 * Renders a paragraph where specific words / phrases use a pixel font
 * while the rest of the text stays in the normal font.
 *
 * 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-*` utility classes used by this
 * component will resolve correctly.
 *
 * @example
 * <PixelParagraph
 *   text="54+ animated components and effects. Free, open source, and built to drop into any shadcn/ui project."
 *   pixelWords={["animated", "shadcn/ui"]}
 *   font="square"
 *   className="text-lg text-muted-foreground"
 * />
 */

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

/* ------------------------------------------------------------------ */
/* Pixel-font constants                                                */
/* ------------------------------------------------------------------ */

type PixelFont = "square" | "grid" | "circle" | "triangle" | "line"

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: "plain"; text: string } | { type: "pixel"; text: string }

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

  // Sort by length descending so longer matches take priority
  const sorted = [...pixelWords].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: "plain", text: text.slice(lastIndex, matchStart) })
    }
    segments.push({ type: "pixel", text: match[0] })
    lastIndex = matchStart + match[0].length
  }

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

  return segments
}

/* ------------------------------------------------------------------ */
/* PixelParagraph                                                      */
/* ------------------------------------------------------------------ */

export interface PixelParagraphProps extends React.ComponentProps<"p"> {
  /** The paragraph text to render. */
  text: string
  /**
   * Words or phrases within `text` to render in a pixel font.
   * Matching is case-sensitive and longest-match-first.
   */
  pixelWords?: string[]
  /** The wrapper element to render. @default "p" */
  as?: "p" | "span" | "div"
  /** The pixel font for highlighted words. @default "square" */
  font?: PixelFont
  /** Extra className applied to each pixel-word span. */
  pixelWordClassName?: string
}

/**
 * Paragraph that renders specific words / phrases in a pixel font
 * while the rest stays in the normal typeface.
 *
 * @example
 * <PixelParagraph
 *   text="54+ animated components and effects. Free, open source, and built to drop into any shadcn/ui project."
 *   pixelWords={["animated", "shadcn/ui"]}
 *   font="square"
 *   className="text-lg text-muted-foreground"
 * />
 */
export function PixelParagraph({
  text,
  pixelWords = [],
  as: Tag = "p",
  className,
  font = "square",
  pixelWordClassName,
  ...props
}: PixelParagraphProps) {
  const segments = splitTextByPixelWords(text, pixelWords)
  const fontClass = PIXEL_FONT_MAP[font]

  return (
    <Tag data-slot="pixel-paragraph" className={cn(className)} {...props}>
      {segments.map((segment, index) => {
        const key = `${segment.type}-${segment.text}-${index}`
        return segment.type === "pixel" ? (
          <span
            key={key}
            data-slot="pixel-word"
            className={cn(fontClass, pixelWordClassName)}
          >
            {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 { PixelParagraph } from "@/components/ui/pixel-paragraph-words";
```

### Basic

Highlight specific words in a pixel font:

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

### Different Pixel Fonts

Choose any of the five pixel font variants:

```tsx
<PixelParagraph
  text="Triangle-based pixel typography with highlighted words."
  pixelWords={["Triangle-based", "highlighted"]}
  font="triangle"
  className="text-lg"
/>
```

### Styling Pixel Words

Apply custom classes to the pixel words:

```tsx
<PixelParagraph
  text="Important words stand out from the base text."
  pixelWords={["Important", "stand out"]}
  font="grid"
  pixelWordClassName="text-foreground font-semibold"
  className="text-lg text-muted-foreground"
/>
```

### As a Different Element

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

```tsx
<PixelParagraph
  as="div"
  text="Block-level pixel text with highlighted words."
  pixelWords={["highlighted words"]}
  className="text-sm"
/>
```

## API Reference

### PixelParagraph Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` | `string` | — | The paragraph text to render |
| `pixelWords` | `string[]` | `[]` | Words/phrases to render in a pixel font. Matching is case-sensitive, longest-first |
| `as` | `"p" \| "span" \| "div"` | `"p"` | The wrapper element to render |
| `font` | `"square" \| "grid" \| "circle" \| "triangle" \| "line"` | `"square"` | The pixel font for highlighted words |
| `pixelWordClassName` | `string` | — | Extra className applied to each pixel-word span |
| `className` | `string` | — | Additional CSS classes applied to the wrapper element |

### Available Pixel Fonts

| Font Name  | Tailwind Class        | CSS Variable                  |
| ---------- | --------------------- | ----------------------------- |
| `square`   | `font-pixel-square`   | `--font-geist-pixel-square`   |
| `grid`     | `font-pixel-grid`     | `--font-geist-pixel-grid`     |
| `circle`   | `font-pixel-circle`   | `--font-geist-pixel-circle`   |
| `triangle` | `font-pixel-triangle` | `--font-geist-pixel-triangle` |
| `line`     | `font-pixel-line`     | `--font-geist-pixel-line`     |

## How It Works

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

## Features

- **Static font assignment** — pixel words get the chosen font, no hover/cycling complexity
- **Five pixel font variants** — Square, Grid, Circle, Triangle, and Line
- **Longest-match-first** — multi-word phrases match correctly without partial collisions
- **Server-compatible** — no `"use client"` needed, no state or effects
- **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 |
