UIPackage
Menu

Highlight

highlight ui
Edit on GitHub

Highlights matching substrings in text for search results. Supports string or regex queries, case-sensitive and whole-word matching, custom highlight tag, and a max-highlight cap.

Also available for Vue ->

Installation

$ npx shadcn@latest add https://uipkge.dev/r/react/highlight.json
Named registry: npx shadcn@latest add @uipkge-react/highlight Installs to: components/ui/highlight/

Examples

Props

Name Type / Values Default Required
text

Text to search within.

string required
query

Query string or RegExp to highlight.

string | RegExp required
highlightTag

HTML tag used to wrap matched substrings. Default 'mark'.

'mark' | 'span' optional
highlightClass

Class applied to each highlight wrapper.

React.HTMLAttributes<HTMLSpanElement>['className'] optional
highlightStyle

Inline style applied to each highlight wrapper.

React.CSSProperties optional
caseSensitive

Case-sensitive matching. Default false.

boolean optional
wholeWord

Match whole words only. Default false.

boolean optional
maxHighlights

Cap the number of highlights rendered. 0 = unlimited. Default 0.

number optional
onMatchCount

Fired with the number of highlights actually rendered (after maxHighlights cap).

(count: number) => void optional
onTotalMatchCount

Fired with the total match count (before maxHighlights cap).

(count: number) => void optional

Schema

Type aliases from this item's source — use them to shape the data you pass in.

Segment
interface Segment {
  text: string
  match: boolean
}

Files installed (2)

  • components/ui/highlight/Highlight.tsx 4.3 kB
    import * as React from 'react'
    import { cn } from '@/lib/utils'
    
    export interface HighlightProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'children'> {
      /** Text to search within. */
      text: string
      /** Query string or RegExp to highlight. */
      query: string | RegExp
      /** HTML tag used to wrap matched substrings. Default 'mark'. */
      highlightTag?: 'mark' | 'span'
      /** Class applied to each highlight wrapper. */
      highlightClass?: React.HTMLAttributes<HTMLSpanElement>['className']
      /** Inline style applied to each highlight wrapper. */
      highlightStyle?: React.CSSProperties
      /** Case-sensitive matching. Default false. */
      caseSensitive?: boolean
      /** Match whole words only. Default false. */
      wholeWord?: boolean
      /** Cap the number of highlights rendered. 0 = unlimited. Default 0. */
      maxHighlights?: number
      /** Fired with the number of highlights actually rendered (after maxHighlights cap). */
      onMatchCount?: (count: number) => void
      /** Fired with the total match count (before maxHighlights cap). */
      onTotalMatchCount?: (count: number) => void
    }
    
    interface Segment {
      text: string
      match: boolean
    }
    
    function buildPattern(
      query: string | RegExp,
      caseSensitive: boolean,
      wholeWord: boolean,
    ): RegExp | null {
      if (query instanceof RegExp) {
        const flags = query.flags.includes('g') ? query.flags : query.flags + 'g'
        return new RegExp(query.source, flags)
      }
      if (!query) return null
      const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
      const body = wholeWord ? `\\b${escaped}\\b` : escaped
      const flags = caseSensitive ? 'g' : 'gi'
      return new RegExp(body, flags)
    }
    
    const Highlight = React.forwardRef<HTMLSpanElement, HighlightProps>(
      (
        {
          className,
          text,
          query,
          highlightTag = 'mark',
          highlightClass,
          highlightStyle,
          caseSensitive = false,
          wholeWord = false,
          maxHighlights = 0,
          onMatchCount,
          onTotalMatchCount,
          ...props
        },
        ref,
      ) => {
        const segments = React.useMemo<Segment[]>(() => {
          if (!text) return []
          if (!query) return [{ text, match: false }]
    
          const pattern = buildPattern(query, caseSensitive, wholeWord)
          if (!pattern) return [{ text, match: false }]
    
          const out: Segment[] = []
          let last = 0
          let count = 0
          let m: RegExpExecArray | null
          while ((m = pattern.exec(text)) !== null) {
            if (m.index > last) out.push({ text: text.slice(last, m.index), match: false })
            out.push({ text: m[0], match: true })
            last = m.index + m[0].length
            count++
            if (maxHighlights > 0 && count >= maxHighlights) break
            if (m[0] === '') pattern.lastIndex++
          }
          if (last < text.length) out.push({ text: text.slice(last), match: false })
    
          return out
        }, [text, query, caseSensitive, wholeWord, maxHighlights])
    
        const totalMatchCount = React.useMemo(() => {
          if (!text || !query) return 0
          const pattern = buildPattern(query, caseSensitive, wholeWord)
          if (!pattern) return 0
    
          let total = 0
          let m: RegExpExecArray | null
          while ((m = pattern.exec(text)) !== null) {
            total++
            if (m[0] === '') pattern.lastIndex++
          }
          return total
        }, [text, query, caseSensitive, wholeWord])
    
        const renderedMatchCount = React.useMemo(
          () => segments.filter((s) => s.match).length,
          [segments],
        )
    
        React.useEffect(() => {
          onMatchCount?.(renderedMatchCount)
        }, [renderedMatchCount, onMatchCount])
    
        React.useEffect(() => {
          onTotalMatchCount?.(totalMatchCount)
        }, [totalMatchCount, onTotalMatchCount])
    
        const Tag = highlightTag
    
        return (
          <span ref={ref} data-uipkge="" data-slot="highlight" className={cn(className)} {...props}>
            {segments.map((seg, i) =>
              seg.match ? (
                <Tag
                  key={i}
                  data-slot="highlight-match"
                  className={cn(
                    'bg-yellow-200 text-yellow-950 dark:bg-yellow-500/30 dark:text-yellow-100 rounded px-0.5 font-medium',
                    highlightClass,
                  )}
                  style={highlightStyle}
                >
                  {seg.text}
                </Tag>
              ) : (
                <React.Fragment key={i}>{seg.text}</React.Fragment>
              ),
            )}
          </span>
        )
      },
    )
    Highlight.displayName = 'Highlight'
    
    export { Highlight }
  • components/ui/highlight/index.ts 0.1 kB
    export { Highlight, type HighlightProps } from './Highlight'

Raw manifest: https://uipkge.dev/r/react/highlight.json