I Wanted Rounded Text Selection for Years. Here's How I Finally Built It in Angular (With AI Help)
There is one incredibly small visual detail I've wanted to implement on my websites for years. Rounded text selection. Not rounded text backgrounds. Not predefined highlighted spans. I mean the actual selection you see
There is one incredibly small visual detail I've wanted to implement on my websites for years.
Rounded text selection.
Not rounded text backgrounds. Not predefined highlighted spans. I mean the actual selection you see when dragging your cursor across text, similar to the selection appearance found in editors such as Visual Studio Code.
It sounds ridiculously simple.
It wasn't.
After years of occasionally searching for a solution, revisiting old discussions, and experimenting with ChatGPT, I finally have a working implementation.
Here's how it works.
The problem with CSS
Normally, customizing text selection is straightforward:
::selection {
background-color: #8c52ff;
color: white;
}
But what if we want this?
::selection {
background-color: #8c52ff;
color: white;
border-radius: 5px;
}
Unfortunately, border-radius isn't supported by the native ::selection pseudo-element. Browsers simply ignore it.
This limitation sent me searching for alternative solutions.
An old Stack Overflow question
One of the most relevant things I found was this question from 2014:
How to apply rounded borders to highlight/selection
The accepted answer by coma provides an interesting solution involving JavaScript, jQuery, Rangy, and individual <span> elements.
It preprocesses the text, wraps characters in spans, and uses CSS classes to create rounded selection boundaries.
While interesting, I wanted something that didn't require restructuring the website's existing text.
My goal was a reusable implementation that could automatically handle dynamically generated Angular content without additional markup.
Eventually, with ChatGPT's assistance, we arrived at a different approach.
The solution: An Angular directive using SVG
Instead of modifying the original HTML, the directive uses the browser's native selection geometry to generate a decorative SVG overlay.
The process works like this:
- Listen for changes to the user's text selection.
- Retrieve the native selection using the Selection API.
- Identify selected text nodes using
TreeWalker. - Calculate their positions using the Range API.
- Merge adjacent fragments belonging to the same visual line.
- Generate rounded SVG paths, including connecting segments between nearby lines.
- Hide the native selection background only when the custom overlay is ready.
Visual representation
The important distinction is that the original DOM remains untouched.
We aren't wrapping every character in additional elements. Instead, the directive creates an SVG representation of the selected text.
Retrieving the selection geometry
The implementation starts with the browser's native Selection API:
const selection = this.doc.getSelection();
if (
!selection ||
selection.isCollapsed ||
!selection.rangeCount
) {
this.clearSelection();
return;
}
const range = selection.getRangeAt(0);
After identifying the relevant text nodes, the directive creates individual ranges and retrieves their coordinates:
const part = this.doc.createRange();
part.selectNodeContents(node);
for (const rect of Array.from(part.getClientRects())) {
if (rect.width <= 0 || rect.height <= 0) continue;
// Collect the fragment geometry.
}
These are simplified excerpts. The full implementation also clips each text-node range to the actual selection boundaries.
Drawing the rounded highlights
Once the geometry is collected, adjacent fragments are merged and converted into SVG paths.
The corners are generated using quadratic BΓ©zier curves, with additional connecting segments where appropriate.
Rather than painting numerous overlapping semi-transparent HTML rectangles, the resulting geometry is combined into SVG paths grouped by selection color.
This produces a more connected selection appearance without the darker overlapping areas that separate highlight elements can create.
Making it global in Angular
One of my main requirements was not having to manually apply the effect whenever I added a new component.
Angular's hostDirectives makes this relatively straightforward.
In the root component:
import { Component } from '@angular/core';
import { RoundedSelectionDirective }
from './directives/rounded-selection.directive';
@Component({
selector: 'app-root',
standalone: true,
hostDirectives: [RoundedSelectionDirective],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {}
That's it.
The directive attaches to the application's root element and handles ordinary text within it.
It also uses MutationObserver and ResizeObserver to respond to relevant content and layout changes.
For performance, selection updates are scheduled using requestAnimationFrame, preventing unnecessary repeated calculations within the same animation frame.
Styling and theme support
The SVG overlay is decorative and doesn't interfere with mouse interactions.
A simplified version of its styling looks like this:
:root {
--sel-bg: #8c52ff;
}
::selection {
background-color: var(--sel-bg);
color: currentColor;
}
html.rounded-selection-enabled ::selection {
background-color: transparent !important;
color: currentColor !important;
}
.rounded-selection-overlay {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
z-index: 2147483647;
pointer-events: none;
user-select: none;
}
The directive reads the existing --sel-bg variable from the selected element's computed style, allowing websites to preserve their individual theme colors.
This was particularly important for my projects, including Zovanion.com and my personal portfolio.
For standalone pages outside Angular, I also adapted the selection engine into a JavaScript implementation.
What about performance and accessibility?
A visual enhancement shouldn't interfere with basic website functionality.
The implementation therefore includes several safeguards:
- Native selection remains available for inputs, textareas, and editable content.
- Touch devices retain native selection behavior.
- Forced-colors environments use native highlighting.
- Large selections have processing limits and can fall back to the browser's default selection.
- Keyboard selection and copying text remain native.
I've tested the implementation in an isolated Chromium environment with dynamically generated content, nested formatting, multiple paragraphs, theme switching, and various fallback scenarios.
However, more extensive cross-browser testing, particularly in Firefox, Safari, and on physical mobile devices, is still needed.
It isn't a finished, production-ready library just yet.
A note about AI assistance
I also want to be transparent about something.
This implementation was developed with the assistance of ChatGPT.
I came with the visual requirement, the original Stack Overflow reference, and the constraints I wanted to solve for my existing projects.
ChatGPT helped generate, troubleshoot, and adapt a substantial amount of the code.
I didn't independently write every line of the directive, and I won't pretend otherwise.
I'm currently going through the implementation to better understand the browser APIs, SVG geometry, Angular lifecycle, and how everything interacts.
For me, that's also part of learning software development: finding a problem, experimenting, understanding why something works, and eventually being able to maintain and improve the code yourself.
AI-generated code shouldn't automatically be treated as good code. It still needs to be understood, reviewed, and tested.
And that's exactly what I intend to do.
References and inspiration:
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.