npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rc-marked-input

v4.0.0

Published

React component for combine editable text with any component using annotated text

Readme

Marked Input · npm version min zipped size Storybook

A React component that lets you combine editable text with any component using annotated text.

Feature

  • Powerful annotations tool: add, edit, remove, visualize
  • Nested marks support
  • TypeScript
  • Support for any components
  • Flexible and customizable
  • Two ways to configure
  • Helpers for processing text
  • Hooks for advanced components
  • Button handling (Left, Right, Delete, Backspace, Esc)
  • Overlay with the suggestions component by default
  • Zero dependencies
  • Cross selection

Installation

You can install the package via npm:

npm install rc-marked-input

Usage

There are many examples available in the Storybook. You can also try a template on CodeSandbox.

Here are a few examples to get you started:

Static marks · sandbox

import {MarkedInput} from 'rc-marked-input'

const Mark = props => <mark onClick={_ => alert(props.meta)}>{props.value}</mark>

const Marked = () => {
    const [value, setValue] = useState('Hello, clickable marked @[world](Hello! Hello!)!')
    return <MarkedInput Mark={Mark} value={value} onChange={setValue} />
}

Configured · sandbox

The library allows you to configure the MarkedInput component in two ways.

Let's declare markups and suggestions data:

const Data = ['First', 'Second', 'Third', 'Fourth', 'Fifth', 'Sixth']
const AnotherData = ['Seventh', 'Eight', 'Ninth']
const Primary = '@[__value__](primary:__meta__)'
const Default = '@[__value__](default)'

Using the components

import {MarkedInput} from 'rc-marked-input'

export const App = () => {
    const [value, setValue] = useState(
        "Enter the '@' for creating @[Primary Mark](primary:Hello!) or '/' for @[Default mark](default)!"
    )

    return (
        <MarkedInput
            Mark={Button}
            value={value}
            onChange={setValue}
            options={[
                {
                    markup: Primary,
                    slotProps: {
                        mark: ({value, meta}) => ({label: value, primary: true, onClick: () => alert(meta)}),
                        overlay: {trigger: '@', data: Data},
                    },
                },
                {
                    markup: Default,
                    slotProps: {
                        overlay: {trigger: '/', data: AnotherData},
                    },
                },
            ]}
        />
    )
}

Using the createMarkedInput:

import {createMarkedInput} from 'rc-marked-input'

const ConfiguredMarkedInput = createMarkedInput({
    Mark: Button,
    options: [
        {
            markup: Primary,
            slotProps: {
                mark: ({value, meta}) => ({label: value, primary: true, onClick: () => alert(meta)}),
                overlay: {trigger: '@', data: ['First', 'Second', 'Third', 'Fourth', 'Fifth', 'Sixth']},
            },
        },
        {
            markup: Default,
            slotProps: {
                mark: ({value}) => ({label: value}),
                overlay: {trigger: '/', data: ['Seventh', 'Eight', 'Ninth']},
            },
        },
    ],
})

const App = () => {
    const [value, setValue] = useState(
        "Enter the '@' for creating @[Primary Mark](primary:Hello!) or '/' for @[Default mark](default)!"
    )
    return <ConfiguredMarkedInput value={value} onChange={setValue} />
}

Static Props with Objects

You can use slotProps.mark as a static object instead of a function. This is useful when you want to pass fixed props to your Mark component:

import {MarkedInput} from 'rc-marked-input'
import {Chip} from '@mui/material'

const App = () => {
    const [value, setValue] = useState('This is a @[static] chip!')

    return (
        <MarkedInput
            Mark={Chip}
            value={value}
            onChange={setValue}
            options={[
                {
                    markup: '@[__value__]',
                    slotProps: {
                        // Static object - passed directly to Chip
                        mark: {
                            variant: 'outlined',
                            color: 'primary',
                            size: 'small',
                        },
                    },
                },
            ]}
        />
    )
}

Key differences:

  • Object form: Props are passed directly to the Mark component (full replacement of MarkProps)
  • Function form: You can access and transform value, meta, nested, and children from the markup
// Object - static props
slotProps: { mark: { label: 'Fixed', color: 'primary' } }

// Function - dynamic props based on markup
slotProps: { mark: ({ value, meta }) => ({ label: value, onClick: () => alert(meta) }) }

Dynamic mark · sandbox

Marks can be dynamic: editable, removable, etc. via the useMark hook helper.

Editable

import {MarkedInput, useMark} from 'rc-marked-input'

const Mark = () => {
    const {label, change} = useMark()

    const handleInput = e => change({label: e.currentTarget.textContent ?? '', value: ' '}, {silent: true})

    return <mark contentEditable onInput={handleInput} children={label} />
}

export const Dynamic = () => {
    const [value, setValue] = useState('Hello, dynamical mark @[world]( )!')
    return <MarkedInput Mark={Mark} value={value} onChange={setValue} />
}

Note: The silent option used to prevent re-rendering itself.

Removable

const RemovableMark = () => {
    const {label, remove} = useMark()
    return <mark onClick={remove} children={label} />
}

export const Removable = () => {
    const [value, setValue] = useState('I @[contain]( ) @[removable]( ) by click @[marks]( )!')
    return <MarkedInput Mark={RemovableMark} value={value} onChange={setValue} />
}

Focusable

If passed the ref prop of the useMark hook in ref of a component then it component can be focused by key operations.

Nested Marks

Marked Input supports nested marks, allowing you to create rich, hierarchical text structures. Nested marks enable complex formatting scenarios like markdown-style text, HTML-like tags, and multi-level annotations.

Enabling Nested Marks

To enable nesting, use the __nested__ placeholder in your markup pattern instead of __value__:

// ✅ Supports nesting
const NestedMarkup = '@[__nested__]'

// ❌ Does not support nesting (plain text only)
const FlatMarkup = '@[__value__]'

Key Differences:

  • __value__ - Content is treated as plain text, nested patterns are ignored
  • __nested__ - Content supports nested structures, nested patterns are parsed

Simple Nesting Example

import {MarkedInput} from 'rc-marked-input'

const NestedMark = ({children, style}: {value?: string; children?: ReactNode; style?: React.CSSProperties}) => (
    <span style={style}>{children}</span>
)

const App = () => {
    const [value, setValue] = useState('This is **bold with *italic* inside**')

    return (
        <MarkedInput
            Mark={NestedMark}
            value={value}
            onChange={setValue}
            options={[
                {
                    markup: '**__nested__**',
                    slotProps: { mark: ({value, children}) => ({
                        value,
                        children,
                        style: {fontWeight: 'bold'},
                    }),
                },
                {
                    markup: '*__nested__*',
                    slotProps: { mark: ({value, children}) => ({
                        value,
                        children,
                        style: {fontStyle: 'italic'},
                    }),
                },
            ]}
        />
    )
}

HTML-like Tags with Two Values

ParserV2 supports two values patterns where a markup contains two __value__ placeholders that must match. This is perfect for HTML-like tags where opening and closing tags should be identical.

const HtmlLikeMark = ({children, value, nested}: {value?: string; children?: ReactNode; nested?: string}) => {
    // Use value as HTML element name (e.g., "div", "span", "mark")
    const Tag = value! as React.ElementType
    return <Tag>{children || nested}</Tag>
}

const App = () => {
    const [value, setValue] = useState(
        '<div>This is a div with <mark>a mark inside</mark> and <b>bold text with <del>nested del</del></b></div>'
    )

    return (
        <MarkedInput
            Mark={HtmlLikeMark}
            value={value}
            onChange={setValue}
            options={[
                // Two values pattern: both __value__ must be identical
                {markup: '<__value__>__nested__</__value__>'},
            ]}
        />
    )
}

Two Values Pattern Rules:

  • Contains exactly two __value__ placeholders
  • Both values must be identical (e.g., <div> and </div>)
  • If values don't match, the pattern won't be recognized
  • Perfect for HTML/XML-like structures where tags must match

Examples of valid two values patterns:

  • <__value__>__nested__</__value__> - HTML tags
  • [__value__]__nested__[/__value__] - BBCode-style tags
  • {{__value__}}__nested__{{/__value__}} - Template tags

Overlay

A default overlay is the suggestion component, but it can be easily replaced for any other.

Suggestions

export const DefaultOverlay = () => {
    const [value, setValue] = useState('Hello, default - suggestion overlay by trigger @!')
    return (
        <MarkedInput Mark={Mark} value={value} onChange={setValue} options={[{ slotProps: { overlay: { trigger: '@', data: ['First', 'Second', 'Third']}]} />
    )
}

Custom overlay · sandbox

const Overlay = () => <h1>I am the overlay</h1>
export const CustomOverlay = () => {
    const [value, setValue] = useState('Hello, custom overlay by trigger @!')
    return <MarkedInput Mark={Mark} Overlay={Overlay} value={value} onChange={setValue} />
}

Custom trigger

export const CustomTrigger = () => {
    const [value, setValue] = useState('Hello, custom overlay by trigger /!')
    return (
        <MarkedInput
            Mark={() => null}
            Overlay={Overlay}
            value={value}
            onChange={setValue}
            options={[{slotProps: {overlay: {trigger: '/'}}}]}
        />
    )
}

Positioned

The useOverlay has a left and right absolute coordinate of a current caret position in the style prop.

const Tooltip = () => {
    const {style} = useOverlay()
    return <div style={{position: 'absolute', ...style}}>I am the overlay</div>
}
export const PositionedOverlay = () => {
    const [value, setValue] = useState('Hello, positioned overlay by trigger @!')
    return <MarkedInput Mark={Mark} Overlay={Tooltip} value={value} onChange={setValue} />
}

Selectable

The useOverlay hook provide some methods like select for creating a new annotation.

const List = () => {
    const {select} = useOverlay()
    return (
        <ul>
            <li onClick={() => select({label: 'First'})}>Clickable First</li>
            <li onClick={() => select({label: 'Second'})}>Clickable Second</li>
        </ul>
    )
}

export const SelectableOverlay = () => {
    const [value, setValue] = useState('Hello, suggest overlay by trigger @!')
    return <MarkedInput Mark={Mark} Overlay={List} value={value} onChange={setValue} />
}

Note: Recommend to pass the ref for an overlay component. It used to detect outside click.

Slots

The slots and slotProps props allow you to customize internal components with type safety and flexibility.

Available Slots

  • container - Root div wrapper for the entire component
  • span - Text span elements for rendering text tokens

Basic Usage

<MarkedInput
    Mark={Mark}
    value={value}
    onChange={setValue}
    slotProps={{
        container: {
            onKeyDown: e => console.log('onKeyDown'),
            onFocus: e => console.log('onFocus'),
            style: {border: '1px solid #ccc', padding: '8px'},
        },
        span: {
            className: 'custom-text-span',
            style: {fontSize: '14px'},
        },
    }}
/>

Custom Components

You can also replace the default components entirely using the slots prop:

const CustomContainer = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>((props, ref) => (
    <div {...props} ref={ref} style={{...props.style, border: '2px solid blue'}} />
))

const CustomSpan = forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>((props, ref) => (
    <span {...props} ref={ref} style={{...props.style, fontWeight: 'bold'}} />
))

<MarkedInput
    Mark={Mark}
    value={value}
    onChange={setValue}
    slots={{
        container: CustomContainer,
        span: CustomSpan,
    }}
/>

See the MUI documentation for more information about the slots pattern.

Overall view

<MarkedInput
    Mark={Mark}
    Overlay={Overlay}
    value={value}
    onChange={setValue}
    options={[
        {
            markup: '@[__value__](__meta__)',
            slotProps: {
                mark: getCustomMarkProps,
                overlay: {trigger: '@', data: Data},
            },
        },
        {
            markup: '@(__value__)[__meta__]',
            slotProps: {
                mark: getAnotherCustomMarkProps,
                overlay: {trigger: '/', data: AnotherData},
            },
        },
    ]}
/>

Or

const MarkedInput = createMarkedInput({
    Mark,
    Overlay,
    options: [
        {
            markup: '@[__label__](__value__)',
            slotProps: {
                mark: getCustomMarkProps,
                overlay: {trigger: '@', data: Data},
            },
        },
        {
            markup: '@(__label__)[__value__]',
            slotProps: {
                mark: getAnotherCustomMarkProps,
                overlay: {trigger: '/', data: AnotherData},
            },
        },
    ],
})

const App = () => <MarkedInput value={value} onChange={setValue} />

API

MarkedInput

| Name | Type | Default | Description | | ------------- | ---------------------------- | ------------- | ---------------------------------------------- | | value | string | undefined | Annotated text with markups for mark | | defaultValue | string | undefined | Default value | | onChange | (value: string) => void | undefined | Change event | | Mark | ComponentType<T = MarkProps> | undefined | Component that used for render markups | | Overlay | ComponentType | Suggestions | Component that is rendered by trigger | | readOnly | boolean | undefined | Prevents from changing the value | | options | OptionProps[] | [{}] | Passed options for configure | | showOverlayOn | OverlayTrigger | change | Triggering events for overlay | | slots | Slots | undefined | Override internal components (container, span) | | slotProps | SlotProps | undefined | Props to pass to slot components |

Helpers

| Name | Type | Description | | ----------------- | ----------------------------------------------------------------------------------- | -------------------------------------------- | | createMarkedInput | <T = MarkToken>(configs: MarkedInputProps): ConfiguredMarkedInput | Create the configured MarkedInput component. | | annotate | (markup: Markup, params: {value: string, meta?: string}) => string | Make annotation from the markup | | denote | (value: string, callback: (mark: MarkToken) => string, markups: Markup[]) => string | Transform the annotated text | | useMark | () => MarkHandler | Allow to use dynamic mark | | useOverlay | () => OverlayHandler | Use overlay props | | useListener | (type, listener, deps) => void | Event listener |

Types

type OverlayTrigger = Array<'change' | 'selectionChange'> | 'change' | 'selectionChange' | 'none'
interface MarkToken {
    type: 'mark'
    content: string
    position: {start: number; end: number}
    descriptor: MarkupDescriptor
    value: string
    meta?: string
    nested?: {
        content: string
        start: number
        end: number
    }
    children: Token[] // Nested tokens (empty array if no nesting)
}

interface TextToken {
    type: 'text'
    content: string
    position: {start: number; end: number}
}

interface MarkProps {
    value?: string
    meta?: string
    nested?: string // Raw nested content as string
    children?: ReactNode // Rendered nested content
}

type Token = TextToken | MarkToken
interface OverlayHandler {
    /**
     * Style with caret absolute position. Used for placing an overlay.
     */
    style: {
        left: number
        top: number
    }
    /**
     * Used for close overlay.
     */
    close: () => void
    /**
     * Used for insert an annotation instead a triggered value.
     */
    select: (value: {value: string; meta?: string}) => void
    /**
     * Overlay match details
     */
    match: OverlayMatch
    ref: RefObject<HTMLElement>
}
interface MarkHandler<T> {
    /**
     * MarkToken ref. Used for focusing and key handling operations.
     */
    ref: RefObject<T>
    /**
     * Change mark.
     * @options.silent doesn't change itself value and meta, only pass change event.
     */
    change: (props: {value: string; meta?: string}, options?: {silent: boolean}) => void
    /**
     * Remove itself.
     */
    remove: () => void
    /**
     * Passed the readOnly prop value
     */
    readOnly?: boolean
    /**
     * Nesting depth of this mark (0 for root-level marks)
     */
    depth: number
    /**
     * Whether this mark has nested children
     */
    hasChildren: boolean
    /**
     * Parent mark token (undefined for root-level marks)
     */
    parent?: MarkToken
    /**
     * Array of child tokens (read-only)
     */
    children: Token[]
}
type OverlayMatch = {
    /**
     * Found value via a overlayMatch
     */
    value: string
    /**
     * Triggered value
     */
    source: string
    /**
     * Piece of text, in which was a overlayMatch
     */
    span: string
    /**
     * Html element, in which was a overlayMatch
     */
    node: Node
    /**
     * Start position of a overlayMatch
     */
    index: number
    /**
     * OverlayMatch's option
     */
    option: Option
}
export interface MarkProps {
    value?: string
    meta?: string
    nested?: string
    children?: ReactNode
}

export interface OverlayProps {
    trigger?: string
    data?: string[]
}

export interface Option<TMarkProps = MarkProps, TOverlayProps = OverlayProps> {
    /**
     * Template string instead of which the mark is rendered.
     * Must contain placeholders: `__value__`, `__meta__`, and/or `__nested__`
     *
     * Placeholder types:
     * - `__value__` - main content (plain text, no nesting)
     * - `__meta__` - additional metadata (plain text, no nesting)
     * - `__nested__` - content supporting nested structures
     *
     * @default "@[__value__](__meta__)"
     */
    markup?: Markup
    /**
     * Per-option slot components (mark and overlay).
     * If not specified, falls back to global Mark/Overlay components.
     *
     * Component Resolution Priority (for each slot):
     * 1. option.slots[slot] (per-option component)
     * 2. MarkedInputProps[slot] (global component)
     * 3. Default component (Suggestions for overlay, undefined for mark)
     *
     * This allows fine-grained control with global fallbacks.
     */
    slots?: {
        mark?: ComponentType<TMarkProps>
        overlay?: ComponentType<TOverlayProps>
    }
    /**
     * Props for slot components.
     */
    slotProps?: {
        /**
         * Props for the mark component. Can be either:
         * - A static object that completely replaces MarkProps
         * - A function that transforms MarkProps into component-specific props
         *
         * @example
         * // Static object
         * mark: { label: 'Click me', primary: true }
         *
         * @example
         * // Function
         * mark: ({ value, meta }) => ({ label: value, onClick: () => alert(meta) })
         */
        mark?: TMarkProps | ((props: MarkProps) => TMarkProps)
        /**
         * Props for the overlay component. Passed directly to the Overlay component.
         *
         * @example
         * overlay: {
         *   trigger: '@',
         *   data: ['Alice', 'Bob']
         * }
         */
        overlay?: TOverlayProps
    }
}

Contributing

If you want to contribute, you are welcome! Create an issue or start a discussion.