# react-example-text-color

> Change text and background color.

A shadcn/ui block from the [@prosekit](https://prosekit.dev) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/prosekit/react-example-text-color).

- Page, with a live preview: https://shadcn.exchange/r/prosekit/react-example-text-color
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/prosekit/react-example-text-color-light-de8c7770efd9d3e3.png
- Registry item: https://prosekit.dev/r/react-example-text-color.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://prosekit.dev/r/react-example-text-color.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `prosekit`
- Registry items: `https://unpkg.com/prosekit-registry/dist/r/react-sample-sample-doc-text-color.json`, `https://unpkg.com/prosekit-registry/dist/r/react-ui-button.json`

## Files

### `registry/src/react/examples/text-color/editor.tsx`

```tsx
'use client'

import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'

import { createEditor, type NodeJSON } from 'prosekit/core'
import { ProseKit } from 'prosekit/react'
import { useMemo } from 'react'

import { sampleContent } from '../../sample/sample-doc-text-color.ts'

import { defineExtension } from './extension.ts'
import InlineMenu from './inline-menu.tsx'

interface EditorProps {
  initialContent?: NodeJSON
}

export default function Editor(props: EditorProps) {
  const defaultContent = props.initialContent ?? sampleContent
  const editor = useMemo(() => {
    const extension = defineExtension()
    return createEditor({ extension, defaultContent })
  }, [defaultContent])

  return (
    <ProseKit editor={editor}>
      <div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-[canvas] text-black dark:text-white">
        <div className="relative w-full flex-1 box-border overflow-y-auto">
          <div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
          <InlineMenu />
        </div>
      </div>
    </ProseKit>
  )
}
```

### `registry/src/react/examples/text-color/extension.ts`

```ts
import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineBackgroundColor } from 'prosekit/extensions/background-color'
import { defineTextColor } from 'prosekit/extensions/text-color'

export function defineExtension() {
  return union(defineBasicExtension(), defineTextColor(), defineBackgroundColor())
}

export type EditorExtension = ReturnType<typeof defineExtension>
```

### `registry/src/react/examples/text-color/index.ts`

```ts
export { default as ExampleEditor } from './editor.tsx'
```

### `registry/src/react/examples/text-color/inline-menu.tsx`

```tsx
'use client'

import type { Editor, Keymap } from 'prosekit/core'
import { useEditorDerivedValue, useKeymap } from 'prosekit/react'
import { InlinePopoverPopup, InlinePopoverPositioner, InlinePopoverRoot } from 'prosekit/react/inline-popover'
import { useMemo, useState } from 'react'

import { Button } from '../../ui/button/index.ts'

import type { EditorExtension } from './extension.ts'

const textColors = [
  { label: 'Gray', value: '#9ca3af' },
  { label: 'Brown', value: '#92400e' },
  { label: 'Orange', value: '#ea580c' },
  { label: 'Yellow', value: '#ca8a04' },
  { label: 'Green', value: '#16a34a' },
  { label: 'Blue', value: '#2563eb' },
  { label: 'Purple', value: '#9333ea' },
  { label: 'Magenta', value: '#c026d3' },
  { label: 'Red', value: '#dc2626' },
]

const backgroundColors = [
  { label: 'Gray', value: '#f3f4f6' },
  { label: 'Brown', value: '#fef3c7' },
  { label: 'Orange', value: '#ffedd5' },
  { label: 'Yellow', value: '#fef9c3' },
  { label: 'Green', value: '#d1fae5' },
  { label: 'Blue', value: '#dbeafe' },
  { label: 'Purple', value: '#e9d5ff' },
  { label: 'Pink', value: '#fce7f3' },
  { label: 'Red', value: '#fecaca' },
]

function getTextColorState(editor: Editor<EditorExtension>) {
  return [{
    label: 'Default',
    value: 'currentColor',
    isActive: !editor.marks.textColor.isActive(),
    onClick: () => editor.commands.removeTextColor(),
  }].concat(textColors.map((color) => ({
    label: color.label,
    value: color.value,
    isActive: editor.marks.textColor.isActive({ color: color.value }),
    onClick: () => editor.commands.addTextColor({ color: color.value }),
  })))
}

function getBackgroundColorState(editor: Editor<EditorExtension>) {
  return [{
    label: 'Default',
    value: 'canvas',
    isActive: !editor.marks.backgroundColor.isActive(),
    onClick: () => editor.commands.removeBackgroundColor(),
  }].concat(
    backgroundColors.map((color) => ({
      label: color.label,
      value: color.value,
      isActive: editor.marks.backgroundColor.isActive({ color: color.value }),
      onClick: () => editor.commands.addBackgroundColor({ color: color.value }),
    })),
  )
}

export default function InlineMenu() {
  const textColorState = useEditorDerivedValue(getTextColorState)
  const backgroundColorState = useEditorDerivedValue(getBackgroundColorState)
  const [open, setOpen] = useState(false)

  const keymap: Keymap = useMemo(() => ({
    Escape: () => {
      if (open) {
        setOpen(false)
        return true
      }
      return false
    },
  }), [open])

  useKeymap(keymap)

  return (
    <InlinePopoverRoot
      open={open}
      onOpenChange={(event) => setOpen(event.detail)}
    >
      <InlinePopoverPositioner className="block overflow-visible w-min h-min z-50 ease-out transition-transform duration-100 motion-reduce:transition-none">
        <InlinePopoverPopup className="box-border origin-(--transform-origin) transition-[opacity,scale] transition-discrete motion-reduce:transition-none data-[state=closed]:duration-150 data-[state=closed]:opacity-0 starting:opacity-0 data-[state=closed]:scale-95 starting:scale-95 duration-40 border border-gray-200 dark:border-gray-800 shadow-lg bg-[canvas] relative flex min-w-32 space-x-1 overflow-auto whitespace-nowrap rounded-lg p-1">
          <div className="flex flex-col gap-4 p-4 w-60">
            <div className="flex flex-col gap-2">
              <div className="text-sm">Text color</div>
              <div className="grid grid-cols-5 gap-1">
                {textColorState.map((color) => (
                  <Button
                    key={color.label}
                    pressed={color.isActive}
                    tooltip={`Text: ${color.label}`}
                    onClick={color.onClick}
                  >
                    <span
                      className="text-base font-medium"
                      style={{ color: color.value }}
                    >
                      A
                    </span>
                  </Button>
                ))}
              </div>
            </div>
            <div className="flex flex-col gap-2">
              <div className="text-sm">Background color</div>
              <div className="grid grid-cols-5 gap-1">
                {backgroundColorState.map((color) => (
                  <Button
                    key={color.label}
                    pressed={color.isActive}
                    tooltip={`Background: ${color.label}`}
                    onClick={color.onClick}
                  >
                    <div
                      className="w-6 h-6 rounded border border-gray-200 dark:border-gray-700"
                      style={{ backgroundColor: color.value }}
                    />
                  </Button>
                ))}
              </div>
            </div>
          </div>
        </InlinePopoverPopup>
      </InlinePopoverPositioner>
    </InlinePopoverRoot>
  )
}
```
