I'm happy to share vue3-highlight-text-color, a Vue 3 component for creating, saving, and restoring text highlights.
Text highlighting often looks simple at first: a user selects text, we wrap it with a <span>, apply a background color, and save the result. In practice, this approach quickly becomes difficult to maintain — especially when the text contains HTML, repeated phrases, or needs to be persisted and restored later.
A common first implementation is based on replace():
html = html.replace(
selectedText,
`<span class="highlight">${selectedText}</span>`,
);
This works for a basic demo, but breaks in real content.
For example, if the same phrase appears multiple times:
<p>Vue is great. Vue is flexible. Vue is everywhere.</p>
A replace()-based approach cannot reliably identify which specific Vue the user selected. It may highlight the first matching occurrence instead of the selected one, or every occurrence if using a global replacement.
Another common problem appears when a selection crosses HTML elements:
<p>Read the <strong>important documentation</strong> carefully.</p>
A user may select important documentation carefully, but that text is not present as one continuous substring in the original HTML. Replacing it requires parsing and rewriting the DOM structure, handling partial nodes, nested tags, existing highlights, and overlapping selections. The resulting HTML can become increasingly complex with every new highlight.
Saving the modified HTML also creates unnecessary coupling between content and annotations. Your storage ends up containing the original document plus generated highlight markup. Removing a highlight means editing HTML again, and changing the document structure can make saved annotations harder to restore correctly.
vue3-highlight-text-color takes a different approach.
Instead of saving generated HTML, it stores a highlight as a simple range in the source text:
{
textId: 42,
color: "#bae6fd",
range: {
start: 18,
end: 41
}
}
The component calculates the selected text’s character offsets based on the source HTML’s textContent. When rendering, it uses those offsets to apply highlights in the right place while keeping the original source HTML separate from the annotation data.
This gives two important advantages.
1. Annotation data stays small, serializable, and independent of rendering
Only numbers, a color, and an optional marker ID need to be stored. The frontend can keep them in local storage, Pinia, or another state manager; the backend can persist them in a regular database table or JSON field.
You do not need to save the entire article or its generated highlighted HTML for every annotation. For a document with many highlights, storage can look as simple as this:
[
{
"id": "marker-1",
"textId": 42,
"color": "#99f6e4",
"range": { "start": 18, "end": 41 }
},
{
"id": "marker-2",
"textId": 42,
"color": "#ddd6fe",
"range": { "start": 95, "end": 112 }
}
]
That data format is easy to transmit, validate, index, synchronize, and share across applications.
2. Your original content remains clean
The source HTML stays the source of truth. The component does not require you to mutate it before saving. Highlight spans are only a rendering concern, not part of your stored document.
This makes it easier to:
- render the same text without annotations when needed;
- load and restore annotations later;
- use your own backend and persistence strategy;
- remove highlights without editing the original HTML;
- keep content management separate from user-generated annotations;
- share the same marker format with potential future adapters outside Vue.
The library includes a default color picker, but its behavior can be replaced with a custom UI through the color-picker slot. You can also adapt highlight styles with CSS variables and customize colors per use case.
Install it with:
pnpm add vue3-highlight-text-color
A minimal integration:
<script setup lang="ts">
import { ref } from "vue";
import {
subtractMarkerRange,
TextHighlighter,
type Marker,
type MarkerRange,
type NewMarker,
} from "vue3-highlight-text-color";
import "vue3-highlight-text-color/style.css";
const article = `
<p>
Select any part of this <strong>HTML text</strong> to create a highlight.
</p>
`;
const highlights = ref<Marker[]>([]);
function addHighlight(highlight: NewMarker) {
highlights.value.push({
...highlight,
id: crypto.randomUUID(),
});
}
function removeHighlight(range: MarkerRange) {
highlights.value = subtractMarkerRange(highlights.value, range);
}
</script>
<template>
<TextHighlighter
:text="article"
:text-id="42"
:markers="highlights"
:colors="['#99f6e4', '#bae6fd', '#ddd6fe']"
@handle-new-highlight="addHighlight"
@handle-remove-highlight="removeHighlight"
/>
</template>
For restoration, the source text should remain compatible with the saved character ranges. If the document’s text changes substantially, applications may need a content-versioning or range-migration strategy — which is a concern for any persistent annotation system.
Try it in the live playground, explore the GitHub repository, or install it from npm.
The project is MIT licensed. Feedback, bug reports, feature requests, and contributions are very welcome.
Top comments (0)