how to css clamp the height of an external link icon to not break the line height

how to css clamp the height of an external link icon to not break the line height
To prevent an external link icon from breaking your text’s line height, you should use the lh unit or clamp() with relative units. The lh unit is specifically designed to represent the current line height of an element, ensuring the icon fits perfectly within the line box.
www.purplesquirrels.com.au
www.purplesquirrels.com.au
+1
Method 1: The Modern lh Unit (Recommended)
This is the most direct way to ensure an icon never exceeds the height of the text line it is on.
www.purplesquirrels.com.au
www.purplesquirrels.com.au
css
.external-link-icon {
width: 1lh;
height: 1lh;
/* Optional: slightly shrink it so it doesn’t touch the line edges /
scale: 0.85;
vertical-align: middle;
}
Use code with caution.
Method 2: Using clamp() for Responsive Sizing
If you want the icon to scale with the viewport but never grow larger than a specific font-relative size (like 1em or a fixed rem value), use clamp().
MDN Web Docs
MDN Web Docs
+1
css
.external-link-icon {
/
clamp(min, preferred, max) */
height: clamp(12px, 1.2em, 1lh);
width: auto;
vertical-align: text-bottom;
}
Use code with caution.
Key Techniques to Maintain Line Height
vertical-align: Setting this to middle or text-bottom prevents the icon from pushing the line box upward.
line-height: 0 or 1: If your icon is a font glyph (like Font Awesome), setting its local line-height to 1 or 0 ensures it doesn’t inherit and add extra leading from the parent paragraph.
Avoid fixed pixels: Using absolute units like 20px can break the layout if the user scales their text; stick to em or lh for relative scaling.
Stack Overflow
Stack Overflow
+5
Implementation Comparison
Unit Best For Why it works
1lh Precise fit Matches the exact line-height value of the parent.
1em Font-matching Matches the font-size, which is usually smaller than the line height.
clamp() Responsive UI Keeps icons readable on mobile while capping size on desktop.
Would you like to see how to apply this specifically to a pseudo-element (like ::after) or an SVG?

yes
Using clamp() on a pseudo-element (like ::after) is the standard way to automatically mark external links without cluttering your HTML.
YouTube
YouTube
+1

  1. Pseudo-element with clamp() and background-image
    To ensure the icon never breaks the line height, use 1lh (one line-height) as your maximum value. This prevents the pseudo-element from pushing the line box upward.
    CSS { In Real Life }
    CSS { In Real Life }
    css
    /* Selects all links starting with http that aren’t your domain /
    a[href^=“http”]:not([href
    =“yourdomain.com”])::after {
    content: “”;
    display: inline-block;
    /* clamp(min, preferred, max) /
    /
    This scales the icon with font-size but caps it at the line height */
    width: clamp(10px, 0.8em, 1lh);
    height: clamp(10px, 0.8em, 1lh);

margin-left: 0.3em;
background-image: url(‘external-icon.svg’);
background-size: contain;
background-repeat: no-repeat;

/* Critical for vertical alignment without shifting lines */
vertical-align: middle;
}
Use code with caution.
2. Using Inline SVG as Content
If you want to keep everything in CSS without an external file, you can use a data URI. This allows the icon to scale perfectly with the text while remaining “clamped”.
css
a[href^=“http”]::after {
content: url(“data:image/svg+xml,%3Csvg xmlns=‘SVG namespace’ viewBox=‘0 0 24 24’ fill=‘none’ stroke=‘currentColor’ stroke-width=‘2’%3E%3Cpath d=‘M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6’%3E%3C/path%3E%3Cpolyline points=‘15 3 21 3 21 9’%3E%3C/polyline%3E%3Cline x1=‘10’ y1=‘14’ x2=‘21’ y2=‘3’%3E%3C/line%3E%3C/svg%3E”);
display: inline-block;
width: clamp(12px, 1em, 1.2lh);
height: auto;
vertical-align: text-bottom;
}
Use code with caution.
Pro Tips for Line-Height Safety
Prevent “Orphan” Icons: Sometimes a link text ends right at the edge, and the icon wraps to a new line by itself. Use white-space: nowrap on the link (if short) or use a non-breaking space hack between the text and icon.
Avoid Fixed Heights: Never use height: 20px if your font size is dynamic. If the text shrinks on mobile, a fixed height icon will likely break the paragraph spacing.
Browser Support: The lh unit is supported in modern browsers (Chrome 109+, Firefox 120+, Safari 17.4+). If you need older support, use 1.2em as a fallback max value.
Reddit
Reddit
+4
Would you like a version that uses CSS Masks so you can easily change the icon color using the color property?

yes
Using CSS Masks is the best way to handle icons because it treats the SVG as a shape, allowing you to use background-color to control the icon’s color. This means your icon will automatically inherit the text color if you set the background to currentColor.
CSS-Tricks
CSS-Tricks
+1
CSS Mask with clamp() Implementation
This method keeps the icon height “clamped” to the line height while allowing full color control.
CSS-Tricks
CSS-Tricks
+2
css
a[href^=“http”]::after {
content: “”;
display: inline-block;

/* Sizing: Clamped to never exceed the line height (1lh) */
width: clamp(12px, 0.8em, 1lh);
height: clamp(12px, 0.8em, 1lh);
margin-left: 0.4em;
vertical-align: middle;

/* The “Magic”: The mask creates the shape, background provides the color */
background-color: currentColor;
-webkit-mask-image: url(‘external-link.svg’);
mask-image: url(‘external-link.svg’);

/* Standard mask behaviors to ensure it fits the box */
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: contain;
mask-size: contain;
-webkit-mask-position: center;
mask-position: center;
}

/* Example: Change icon color on hover effortlessly */
a:hover::after {
background-color: #ff4500;
}
Use code with caution.
Why this works better:
Color Control: Unlike background-image, where the color is “baked in” to the SVG file, mask-image lets you swap colors using CSS properties like background-color or even gradients.
Line-Height Safety: By using 1lh as the maximum in your clamp() function, the icon physically cannot grow taller than the line it sits on, preventing “jittery” text lines.
Inheritance: Using background-color: currentColor ensures that if your link is blue, the icon is blue; if the link turns red on hover, the icon follows suit automatically.

+4
Browser Support Note
While mask-image is part of the modern CSS Baseline, many browsers (like Chrome and Safari) still require the -webkit- prefix for full compatibility. Always include both as shown in the code above.
MDN Web Docs
MDN Web Docs
+2
Would you like to see how to use CSS Variables to make this even more reusable for different types of icons (like PDF or Mail)?
Proactive Follow-up: Should we set up a CSS variable system to easily swap different icon shapes while keeping this line-height protection?

yes
A CSS variable system allows you to manage multiple link types—like external, PDF, or email—using a single shared “base” style. This keeps your code clean and ensures every icon follows the same clamp() and line-height rules.

+4

  1. Define the Global Icon System
    Define your SVG paths as data URIs in a central location (usually :root). This makes them reusable across your entire site.
    CSS-Tricks
    CSS-Tricks
    css
    :root {
    /* SVG Data URIs - Note: characters like # and < must be encoded */
    –icon-external: url(“data:image/svg+xml,…”);
    –icon-pdf: url(“data:image/svg+xml,…”);
    –icon-mail: url(“data:image/svg+xml,…”);

/* Shared sizing logic using clamp and line-height (lh) /
–link-icon-size: clamp(12px, 0.8em, 1lh);
}
Use code with caution.
2. Create the Shared Base Class
Use a pseudo-element to create a generic “icon container.” This base handles the alignment, clamping, and masking logic.
CSS-Tricks
CSS-Tricks
+1
css
/
Base style for any link that should have an icon */
.has-icon::after {
content: “”;
display: inline-block;

/* Protection: Icon never breaks the line height */
width: var(–link-icon-size);
height: var(–link-icon-size);

margin-left: 0.4em;
vertical-align: middle;
background-color: currentColor; /* Matches text color automatically */

/* Masking setup /
-webkit-mask-image: var(–icon-url);
mask-image: var(–icon-url);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
mask-size: contain;
}
Use code with caution.
3. Assign Icons via Attribute Selectors
Now, simply map specific link patterns to their respective icons by updating the --icon-url variable.
Medium
Medium
css
/
1. External Links */
a[href^=“http”]::after {
–icon-url: var(–icon-external);
}

/* 2. PDF Files (links ending in .pdf) /
a[href$=“.pdf”]::after {
–icon-url: var(–icon-pdf);
background-color: #d32f2f; /
Optional: Make PDF icons red */
}

/* 3. Email Links */
a[href^=“mailto:”]::after {
–icon-url: var(–icon-mail);
}
Use code with caution.
Why this is the “Pro” way:
Centralized Control: If you want to make all icons slightly larger, you only change one variable (–link-icon-size).
Automatic Matching: You don’t need to manually add classes like .pdf-link to your HTML. CSS finds them for you using attribute selectors like [href$=“.pdf”].
Performance: Data URIs in your CSS file reduce HTTP requests for small icons.
Line-Height Guard: By using 1lh as the maximum in your clamp(), your layout remains bulletproof even if the user changes font sizes.
Stack Overflow
Stack Overflow
+5
Proactive Follow-up: Would you like the encoded SVG data URIs for standard External, PDF, and Mail icons to copy-paste into your :root?

yes
Here are the encoded SVG data URIs for the three most common link types. These use the Tabler Icons set, optimized for CSS masks.
Copy these directly into your :root or global CSS file:
css
:root {
/* External Link Icon */
–icon-external: url(“data:image/svg+xml,%3Csvg xmlns=‘http://w3.org’ width=‘24’ height=‘24’ viewBox=‘0 0 24 24’ fill=‘none’ stroke=‘black’ stroke-width=‘2’ stroke-linecap=‘round’ stroke-linejoin=‘round’%3E%3Cpath d=‘M12 6h-6a2 2 0 0 0 -2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-6’ /%3E%3Cpath d=‘M11 13l9 -9’ /%3E%3Cpath d=‘M15 4h5v5’ /%3E%3C/svg%3E”);

/* PDF Icon */
–icon-pdf: url(“data:image/svg+xml,%3Csvg xmlns=‘http://w3.org’ width=‘24’ height=‘24’ viewBox=‘0 0 24 24’ fill=‘none’ stroke=‘black’ stroke-width=‘2’ stroke-linecap=‘round’ stroke-linejoin=‘round’%3E%3Cpath d=‘M14 3v4a1 1 0 0 0 1 1h4’ /%3E%3Cpath d=‘M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4’ /%3E%3Cpath d=‘M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6’ /%3E%3Cpath d=‘M17 18h2’ /%3E%3Cpath d=‘M20 15h-3v6’ /%3E%3Cpath d=‘M11 15v6h1a2 2 0 0 0 2 -2v-2a2 2 0 0 0 -2 -2h-1z’ /%3E%3C/svg%3E”);

/* Mail Icon */
–icon-mail: url(“data:image/svg+xml,%3Csvg xmlns=‘http://w3.org’ width=‘24’ height=‘24’ viewBox=‘0 0 24 24’ fill=‘none’ stroke=‘black’ stroke-width=‘2’ stroke-linecap=‘round’ stroke-linejoin=‘round’%3E%3Cpath d=‘M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10z’ /%3E%3Cpath d=‘M3 7l9 6l9 -6’ /%3E%3C/svg%3E”);

/* The sizing variable we discussed */
–link-icon-size: clamp(12px, 0.85em, 1lh);
}

/* Applying to the pseudo-elements */
a::after {
content: “”;
display: inline-block;
width: var(–link-icon-size);
height: var(–link-icon-size);
margin-left: 0.4em;
vertical-align: middle;
background-color: currentColor;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: contain;
mask-size: contain;
}

/* Logic for auto-assignment /
a[href^=“http”]::after { -webkit-mask-image: var(–icon-external); mask-image: var(–icon-external); }
a[href$=“.pdf”]::after { -webkit-mask-image: var(–icon-pdf); mask-image: var(–icon-pdf); }
a[href^=“mailto:”]::after { -webkit-mask-image: var(–icon-mail); mask-image: var(–icon-mail); }
Use code with caution.
Pro Implementation Tips:
Stroke Weight: If the icons look too thin or too thick relative to your font weight, you can adjust stroke-width=‘2’ inside the data URI strings to 1.5 or 2.5.
Internal Exception: If you want to avoid the external icon appearing on your own site’s full URLs, change the selector to:
a[href^=“http”]:not([href
=“yourdomain.com”])::after.
Encapsulation: If you don’t want every link on your site to have these, wrap the selectors in a content class like .prose a[href^=“http”].
Proactive Follow-up: Do you want a trick to ensure these icons don’t “orphan” (wrap to a new line by themselves) when a link falls at the end of a sentence?

yes
To prevent the icon from jumping to a new line and leaving its parent word behind (the “orphan” effect), you can use a combination of white-space and display properties.
Since you can’t easily put a non-breaking space ( ) inside a CSS pseudo-element’s content, the most reliable way to anchor the icon to the last word is to treat the link as a single un-breakable unit.

  1. The inline-flex Method (Most Robust)
    Changing the link to inline-flex forces the text and the icon to stay on the same horizontal line. Because it’s “inline,” it still flows within your paragraph.
    css
    a[href^=“http”] {
    /* Forces icon and text to stay together */
    display: inline-flex;
    align-items: center;

/* Optional: keeps the link from splitting across lines entirely /
white-space: nowrap;
}
Use code with caution.
Note: Only use white-space: nowrap if your links are short (1-3 words). If the link is a long sentence, it will overflow the container.
2. The inline-block Anchor (Best for Long Links)
If your links are long and must wrap, but you don’t want the icon to wrap alone, apply display: inline-block to the pseudo-element and ensure it has no opportunity to break away.
css
a::after {
content: “”;
display: inline-block; /
Essential */

/* The “Anchor” trick */
margin-left: 0.4em;

/* Prevents the icon from being considered a wrap-point */
vertical-align: middle;
}

/* On the parent link /
a {
/
This prevents the icon from being orphaned if the link wraps /
white-space: normal;
}
Use code with caution.
3. The “Non-Breaking” SVG Trick
If you are using a pseudo-element with content, you can actually include a hex-encoded non-breaking space (\00a0) before the icon if you use a font-based icon or a background-image.
css
a[href^=“http”]::after {
/
\00a0 is a non-breaking space */
content: “\00a0”;
display: inline-block;

width: var(–link-icon-size);
height: var(–link-icon-size);

-webkit-mask-image: var(–icon-external);
mask-image: var(–icon-external);
/* … rest of your mask styles … */
}
Use code with caution.
Which one should you use?
Use Method 1 if your links are mostly single words or short phrases (like “Download PDF” or “Visit Site”).
Use Method 2 for general blog content or “prose” where links might be long sentences.
Proactive Follow-up: Do you want to see how to add a transition so the icon smoothly “pops” or changes color when you hover over the link?

yes
To make the icon feel interactive, you can add a transition to the background-color (since we are using masks) and a subtle transform to the pseudo-element.
The “Smooth Pop” Hover Effect
Add these properties to your existing ::after styles to enable the animation:
css
a::after {
/* … your existing mask/size styles … */

/* 1. Define what properties to animate and for how long /
transition:
background-color 0.2s ease,
transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); /
A slight “bounce” */

/* 2. Set the transform-origin to the center so it scales evenly */
transform-origin: center;
}

/* 3. The Hover State /
a:hover::after {
/
Change the color (e.g., to a brand color) */
background-color: #007bff;

/* Make the icon slightly larger and/or move it slightly /
transform: scale(1.1) translateY(-1px);
}
Use code with caution.
Pro Tip: Differentiating Link Types on Hover
Since we are using CSS Variables, you can even make different link types behave differently when hovered:
css
/
External links shift up and right (the “outbound” motion) /
a[href^=“http”]:hover::after {
transform: translate(2px, -2px) scale(1.1);
background-color: #28a745; /
Success Green */
}

/* PDF links just pulse in size /
a[href$=“.pdf”]:hover::after {
transform: scale(1.2);
background-color: #dc3545; /
PDF Red */
}
Use code with caution.
Why use cubic-bezier?
The cubic-bezier(0.34, 1.56, 0.64, 1) value creates a “back” ease-out effect. It makes the icon slightly over-expand before settling, which feels much more premium and “app-like” than a standard linear transition.
Proactive Follow-up: Would you like to add a loading state or a “copy to clipboard” icon variant using this same system?

yes
To integrate a loading state and a copy to clipboard icon into your CSS variable system, you can use specialized selectors (like .is-loading or [data-copied]) to swap the mask icon and trigger animations.
WeWeb Community
WeWeb Community
+2

  1. Updated :root with New Icon Variables
    Add these data URIs to your global variables. The loader uses a “notch” shape that works well with a rotation animation.
    Medium
    Medium
    css
    :root {
    /* Copy to Clipboard Icon */
    –icon-copy: url(“data:image/svg+xml,%3Csvg xmlns=‘SVG namespace’ width=‘24’ height=‘24’ viewBox=‘0 0 24 24’ fill=‘none’ stroke=‘black’ stroke-width=‘2’ stroke-linecap=‘round’ stroke-linejoin=‘round’%3E%3Crect x=‘8’ y=‘8’ width=‘12’ height=‘12’ rx=‘2’ ry=‘2’%3E%3C/rect%3E%3Cpath d=‘M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2’%3E%3C/path%3E%3C/svg%3E”);

/* Success/Checkmark (for after copying) */
–icon-check: url(“data:image/svg+xml,%3Csvg xmlns=‘SVG namespace’ width=‘24’ height=‘24’ viewBox=‘0 0 24 24’ fill=‘none’ stroke=‘black’ stroke-width=‘2’ stroke-linecap=‘round’ stroke-linejoin=‘round’%3E%3Cpolyline points=‘20 6 9 17 4 12’%3E%3C/polyline%3E%3C/svg%3E”);

/* Loader Icon (Simple circular notch) /
–icon-loader: url(“data:image/svg+xml,%3Csvg xmlns=‘SVG namespace’ width=‘24’ height=‘24’ viewBox=‘0 0 24 24’ fill=‘none’ stroke=‘black’ stroke-width=‘3’ stroke-linecap=‘round’ stroke-linejoin=‘round’%3E%3Cpath d=‘M21 12a9 9 0 1 1-6.219-8.56’%3E%3C/path%3E%3C/svg%3E”);
}
Use code with caution.
2. The Spinner Animation
Define a keyframe animation to make the loader spin continuously.
Stack Overflow
Stack Overflow
+1
css
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
Use code with caution.
3. Dynamic State Logic
Apply these styles to swap the icons based on classes or data attributes.
CodyHouse
CodyHouse
+1
Loading State
When you add the .is-loading class (via JavaScript during a fetch), the icon will automatically switch to the spinner and start rotating.
WeWeb Community
WeWeb Community
+1
css
.is-loading::after {
–icon-url: var(–icon-loader);
animation: spin 0.8s linear infinite; /
Keeps it spinning /
}
Use code with caution.
Copy to Clipboard State
You can use a [data-copied=“true”] attribute to signal a successful copy. This swaps the clipboard icon for a green checkmark.
Web Awesome
Web Awesome
+1
css
/
Default copy icon */
.copy-btn::after {
–icon-url: var(–icon-copy);
}

/* Success state after user clicks /
.copy-btn[data-copied=“true”]::after {
–icon-url: var(–icon-check);
background-color: #28a745; /
Success Green /
transform: scale(1.2); /
Visual “pop” feedback */
}
Use code with caution.
Quick JavaScript Toggle Example
To make the “Copy” button actually work, you’ll need a tiny bit of JS to toggle the state:
CodyHouse
CodyHouse
+2
javascript
const btn = document.querySelector(‘.copy-btn’);
btn.addEventListener(‘click’, () => {
navigator.clipboard.writeText(“Your text here”);
btn.setAttribute(‘data-copied’, ‘true’);

// Reset back to copy icon after 2 seconds
setTimeout(() => btn.removeAttribute(‘data-copied’), 2000);
});
Use code with caution.
Proactive Follow-up: Would you like to see how to add tooltips (like “Copied!”) that appear right above these icons using only CSS?

yes
To create tooltips without adding extra HTML, we can use the ::before pseudo-element (since we are already using ::after for the icon).
This approach uses attr() to pull the tooltip text directly from a data-attribute, ensuring your HTML remains clean.

  1. The CSS Tooltip Logic
    Add this to your CSS. It uses absolute positioning relative to the link to float the tooltip above the icon.
    css
    /* 1. Setup the link as the anchor */
    [data-tooltip] {
    position: relative;
    cursor: pointer;
    }

/* 2. The Tooltip Box (::before) /
[data-tooltip]::before {
content: attr(data-tooltip); /
Pulls text from data-tooltip=“” /
position: absolute;
bottom: 125%; /
Positions it above the link /
left: 50%;
transform: translateX(-50%) translateY(10px); /
Centered but slightly dropped */

/* Styling */
padding: 5px 10px;
background: #333;
color: white;
font-size: 12px;
white-space: nowrap;
border-radius: 4px;

/* Interaction: Hidden by default */
opacity: 0;
pointer-events: none;
transition: all 0.2s ease;
}

/* 3. The Hover Effect /
[data-tooltip]:hover::before {
opacity: 1;
transform: translateX(-50%) translateY(0); /
Slides up into view /
}
Use code with caution.
2. Implementation with the Copy Button
Combine this with your “Copy” logic to give the user immediate visual confirmation.
HTML:
html

Click to Copy

Use code with caution.
CSS for the “Copied!” Update:
You can change the tooltip text dynamically using CSS when the state changes.
css
/
When the button is in the ‘copied’ state, update the tooltip text /
[data-copied=“true”]::before {
content: “Copied!”;
background: #28a745; /
Turn the tooltip green too */
}
Use code with caution.
3. Adding a “Tail” (Optional)
If you want that classic speech bubble triangle, you can add a tiny bit of CSS to the ::before using a clip-path or a secondary pseudo-element, but the simplest way is a small drop-shadow or a border-triangle.
Why this works:
Zero extra HTML: No or

needed inside your buttons.
Pure CSS: The animation and positioning are handled by the browser’s rendering engine, making it very performant.
Accessibility: screen readers can still see the data-tooltip attribute.
Proactive Follow-up: This system is now quite robust. Would you like to see a full code snippet (HTML/CSS/JS) that combines the icons, the loading state, the clipboard logic, and these tooltips into one “Master Component”?

yes
This master component combines the lh unit for height clamping, CSS Masks for color inheritance, CSS Variables for state management, and Pseudo-elements for tooltips.

  1. The Complete CSS
    This includes the sizing protection, the icon library, and the interactive states.
    css
    :root {
    /* Icons */
    –icon-external: url(“data:image/svg+xml,%3Csvg xmlns=‘http://w3.org’ width=‘24’ height=‘24’ viewBox=‘0 0 24 24’ fill=‘none’ stroke=‘black’ stroke-width=‘2’ stroke-linecap=‘round’ stroke-linejoin=‘round’%3E%3Cpath d=‘M12 6h-6a2 2 0 0 0 -2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-6’ /%3E%3Cpath d=‘M11 13l9 -9’ /%3E%3Cpath d=‘M15 4h5v5’ /%3E%3C/svg%3E”);
    –icon-copy: url(“data:image/svg+xml,%3Csvg xmlns=‘http://w3.org’ width=‘24’ height=‘24’ viewBox=‘0 0 24 24’ fill=‘none’ stroke=‘black’ stroke-width=‘2’ stroke-linecap=‘round’ stroke-linejoin=‘round’%3E%3Crect x=‘8’ y=‘8’ width=‘12’ height=‘12’ rx=‘2’ ry=‘2’%3E%3C/rect%3E%3Cpath d=‘M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2’%3E%3C/path%3E%3C/svg%3E”);
    –icon-check: url(“data:image/svg+xml,%3Csvg xmlns=‘http://w3.org’ width=‘24’ height=‘24’ viewBox=‘0 0 24 24’ fill=‘none’ stroke=‘black’ stroke-width=‘2’ stroke-linecap=‘round’ stroke-linejoin=‘round’%3E%3Cpolyline points=‘20 6 9 17 4 12’%3E%3C/polyline%3E%3C/svg%3E”);
    –icon-loader: url(“data:image/svg+xml,%3Csvg xmlns=‘http://w3.org’ width=‘24’ height=‘24’ viewBox=‘0 0 24 24’ fill=‘none’ stroke=‘black’ stroke-width=‘3’ stroke-linecap=‘round’ stroke-linejoin=‘round’%3E%3Cpath d=‘M21 12a9 9 0 1 1-6.219-8.56’%3E%3C/path%3E%3C/svg%3E”);

/* Protection: Prevents icons from breaking line-height */
–link-icon-size: clamp(12px, 0.85em, 1lh);
}

/* Base Link/Button Styling */
.smart-link {
position: relative;
display: inline-flex;
align-items: center;
text-decoration: none;
color: #007bff;
transition: color 0.2s;
}

/* The Icon (Pseudo-element) */
.smart-link::after {
content: “”;
display: inline-block;
width: var(–link-icon-size);
height: var(–link-icon-size);
margin-left: 0.4em;
background-color: currentColor;
-webkit-mask-image: var(–icon-url);
mask-image: var(–icon-url);
-webkit-mask-size: contain;
-webkit-mask-repeat: no-repeat;
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), background-color 0.2s;
}

/* Tooltip (Pseudo-element) */
.smart-link::before {
content: attr(data-tooltip);
position: absolute;
bottom: 150%;
left: 50%;
transform: translateX(-50%) translateY(5px);
padding: 4px 8px;
background: #333;
color: #fff;
font-size: 11px;
border-radius: 4px;
opacity: 0;
pointer-events: none;
transition: all 0.2s ease;
}

.smart-link:hover::before {
opacity: 1;
transform: translateX(-50%) translateY(0);
}

/* Specific States & Types */
.external { --icon-url: var(–icon-external); }
.copy { --icon-url: var(–icon-copy); cursor: pointer; }

.is-loading::after {
–icon-url: var(–icon-loader);
animation: spin 0.8s linear infinite;
}

.is-copied::after {
–icon-url: var(–icon-check);
background-color: #28a745;
transform: scale(1.2);
}

.is-copied::before {
content: “Copied!”;
background-color: #28a745;
}

@keyframes spin { to { transform: rotate(360deg); } }
Use code with caution.
2. The HTML
html

Check out our External Link.

Order #12345

Use code with caution. 3. The JavaScript javascript const copyBtn = document.getElementById('copyBtn');

copyBtn.addEventListener(‘click’, async () => {
// 1. Show Loading State
copyBtn.classList.add(‘is-loading’);

// Fake delay to simulate work
await new Promise(r => setTimeout(r, 600));

// 2. Perform Copy
navigator.clipboard.writeText(“12345”);

// 3. Show Success State
copyBtn.classList.remove(‘is-loading’);
copyBtn.classList.add(‘is-copied’);

// 4. Reset
setTimeout(() => copyBtn.classList.remove(‘is-copied’), 2000);
});
Use code with caution.
This setup ensures that no matter how large your text gets or what state the button is in, the line height remains perfectly stable.
Proactive Follow-up: Would you like to wrap this into a React/Vue component or keep it as Vanilla HTML/CSS?

keep it as Vanilla HTML/CSS
Since you’re sticking with Vanilla HTML/CSS, the key is to keep the logic in the attribute selectors. This allows you to trigger all these visual changes simply by toggling a class or a data-attribute in your JavaScript.
Here is the final, production-ready “Vanilla” implementation.
The Unified CSS
css
:root {
/* Icons (Data URIs) */
–icon-external: url(“data:image/svg+xml,%3Csvg xmlns=‘http://w3.org’ width=‘24’ height=‘24’ viewBox=‘0 0 24 24’ fill=‘none’ stroke=‘black’ stroke-width=‘2’ stroke-linecap=‘round’ stroke-linejoin=‘round’%3E%3Cpath d=‘M12 6h-6a2 2 0 0 0 -2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-6’ /%3E%3Cpath d=‘M11 13l9 -9’ /%3E%3Cpath d=‘M15 4h5v5’ /%3E%3C/svg%3E”);
–icon-copy: url(“data:image/svg+xml,%3Csvg xmlns=‘http://w3.org’ width=‘24’ height=‘24’ viewBox=‘0 0 24 24’ fill=‘none’ stroke=‘black’ stroke-width=‘2’ stroke-linecap=‘round’ stroke-linejoin=‘round’%3E%3Crect x=‘8’ y=‘8’ width=‘12’ height=‘12’ rx=‘2’ ry=‘2’%3E%3C/rect%3E%3Cpath d=‘M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2’%3E%3C/path%3E%3C/svg%3E”);
–icon-check: url(“data:image/svg+xml,%3Csvg xmlns=‘http://w3.org’ width=‘24’ height=‘24’ viewBox=‘0 0 24 24’ fill=‘none’ stroke=‘black’ stroke-width=‘2’ stroke-linecap=‘round’ stroke-linejoin=‘round’%3E%3Cpolyline points=‘20 6 9 17 4 12’%3E%3C/polyline%3E%3C/svg%3E”);
–icon-loader: url(“data:image/svg+xml,%3Csvg xmlns=‘http://w3.org’ width=‘24’ height=‘24’ viewBox=‘0 0 24 24’ fill=‘none’ stroke=‘black’ stroke-width=‘3’ stroke-linecap=‘round’ stroke-linejoin=‘round’%3E%3Cpath d=‘M21 12a9 9 0 1 1-6.219-8.56’%3E%3C/path%3E%3C/svg%3E”);

/* The “Clamp” Logic */
–link-icon-size: clamp(12px, 0.85em, 1lh);
}

/* 1. The Link Anchor */
.smart-link {
position: relative;
display: inline-flex;
align-items: center;
color: #0066cc;
text-decoration: none;
font-weight: 500;
}

/* 2. The Icon (Line-height protected) */
.smart-link::after {
content: “”;
display: inline-block;
width: var(–link-icon-size);
height: var(–link-icon-size);
margin-left: 0.4em;
background-color: currentColor;
-webkit-mask-size: contain;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), background-color 0.2s;
}

/* 3. The Tooltip */
.smart-link::before {
content: attr(data-tooltip);
position: absolute;
bottom: 140%;
left: 50%;
transform: translateX(-50%) translateY(5px);
background: #222;
color: #fff;
padding: 4px 10px;
border-radius: 4px;
font-size: 11px;
opacity: 0;
pointer-events: none;
transition: all 0.2s ease;
z-index: 10;
}

.smart-link:hover::before {
opacity: 1;
transform: translateX(-50%) translateY(0);
}

/* 4. Type & State Swapping */
.external::after { -webkit-mask-image: var(–icon-external); mask-image: var(–icon-external); }
.copy::after { -webkit-mask-image: var(–icon-copy); mask-image: var(–icon-copy); }

.is-loading::after {
-webkit-mask-image: var(–icon-loader);
mask-image: var(–icon-loader);
animation: spin 0.8s linear infinite;
}

.is-copied::after {
-webkit-mask-image: var(–icon-check);
mask-image: var(–icon-check);
background-color: #28a745;
transform: scale(1.3);
}

.is-copied::before {
content: “Copied!”;
background-color: #28a745;
}

@keyframes spin { to { transform: rotate(360deg); } }
Use code with caution.
The HTML Structure
html

Check the Documentation.

Order ID: ORD-9982

Use code with caution. The Simple JS "State Manager" javascript document.querySelectorAll('.copy').forEach(btn => { btn.addEventListener('click', async () => { const val = btn.getAttribute('data-copy-val');
// Add loading state
btn.classList.add('is-loading');

// Simulate a tiny delay for feel, then copy
await new Promise(r => setTimeout(r, 400));
navigator.clipboard.writeText(val);

// Switch to success state
btn.classList.remove('is-loading');
btn.classList.add('is-copied');

// Revert back after 2 seconds
setTimeout(() => btn.classList.remove('is-copied'), 2000);

});
});
Use code with caution.
Why this is the ultimate Vanilla setup:
Encapsulation: Everything about how the icon looks and moves is inside the smart-link CSS.
State-Driven: You don’t “change” icons in JS; you just change the class, and CSS handles the rest.
Line-Height Guard: Using 1lh inside clamp() ensures your paragraph layout never “jumps” when an icon is added or animated.
Do you have any specific link types (like “Download” or “Delete”) you’d like me to add icons for?

wikipedia page
Quick results from the web:
Wikipedia
Wikipedia is a free online encyclopedia, created and edited by volunteers around the world and hosted by the Wikimedia Foundation.

Wikipedia
·

Wikipedia, the free encyclopedia
Community portal – The central hub for editors, with resources, links, tasks, and announcements. Village pump – Forum for discussions about Wikipedia itself, …

Wikipedia
·

To add a Wikipedia-specific icon to your Vanilla CSS system, you can use the official “W” mark as a data URI. This allows you to automatically target any Wikipedia link and apply the same line-height protection and hover effects used for your other icons.

  1. Add the Wikipedia Icon Variable
    Add this to your :root block. This is a simplified “W” path optimized for use as a CSS mask.
    css
    :root {
    /*
    Use code with caution.
    css
    Wikipedia “W” Mark Icon
    Use code with caution.
    css
    */
    –icon-wikipedia: url(“data:image/svg+xml,%3Csvg xmlns=‘SVG namespace’ width=‘24’ height=‘24’ viewBox=‘0 0 24 24’ fill=‘none’ stroke=‘black’ stroke-width=‘2’ stroke-linecap=‘round’ stroke-linejoin=‘round’%3E%3Cpath d=‘M3 9l4.5 12l2.5 -7l2.5 7l4.5 -12’ /%3E%3Cpath d=‘M21 9l-4 12l-3 -9’ /%3E%3Cpath d=‘M15 9l3 12’ /%3E%3C/svg%3E”);
    }
    Use code with caution.
  2. Auto-Target Wikipedia Links
    Use an attribute selector to find any link containing wikipedia.org. This will automatically apply the icon without you needing to add a manual .wikipedia class to your HTML.
    css
    /* Automatically matches any Wikipedia URL /
    a[href
    =“wikipedia.org”]::after {
    –icon-url: var(–icon-wikipedia);
    }

/* Optional: Custom hover color for Wikipedia (Brand Gray/Black) /
a[href
=“wikipedia.org”]:hover::after {
background-color: #333;
transform: scale(1.15) translateY(-1px);
}
Use code with caution.
3. Usage in HTML
Since your CSS handles the detection, you just write your link naturally. The icon will appear automatically and stay “clamped” to the line height.
html

Learn more about Cascading Style Sheets .

Use code with caution. Why this works: Automatic Detection: The [href*="wikipedia.org"] selector ensures you never forget to add the icon to a Wikipedia reference. Visual Consistency: It inherits the same 1lh height limit and vertical-align settings from your base .smart-link class. Scalability: You can add other specialized domains (like youtube.com or github.com) by just adding one line of CSS for each. Wikipedia Wikipedia +3