Bubble Text
TextSwells the letter under the cursor to the heaviest weight and lifts it; its neighbours follow at half and a quarter, and everything settles when the pointer leaves.
A link underline that enters from the left on hover and leaves to the right, so the exit continues the entrance instead of rewinding it. One background gradient, no pseudo-element, wraps across lines. Pure CSS; triggered by the link around it.
Enters from the left, leaves to the right, with one gradient and no pseudo-element.
Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.
Adjust · props are custom properties, set inline, so you edit the running instance
JavaScript of its own
0 B
CSS 4.9 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.
Browser support
every browser.
One command adds the integration, the base tokens and every component. Then import what you use.
npx astro add moonarcpnpm astro add moonarcbunx astro add moonarc---
import UnderlineHover from '@moonarc/core/UnderlineHover';
---
<a href="/docs/"><UnderlineHover>Read the docs</UnderlineHover></a>
<UnderlineHover as="a" href="/pro/" thickness="2px" color="var(--ma-ink)">Pro</UnderlineHover>Owns the file, no dependency. The registry item also installs the base tokens.
npx shadcn@latest add https://moonarc.dev/r/underline-hover.jsonThe CLI needs a components.json and the @/* alias, which Setup has. The file lands in src/components/moonarc/.
The whole component. Self-contained styles in a cascade layer so your classes always win; it imports nothing.
---
/**
* UnderlineHover — the line enters from the left and leaves to the right.
* One background gradient: its size grows on hover while anchored left,
* and shrinks on leave while anchored right, so the exit runs the same
* direction as the entrance. Works on the element or from the link or
* button around it. Zero JS.
*/
import type { HTMLAttributes, HTMLTag } from 'astro/types';
interface Props extends HTMLAttributes<'span'> {
/** Element to render. */
as?: HTMLTag;
/** Line thickness, any CSS length. */
thickness?: string;
/** Gap between the text and the line. */
offset?: string;
/** Line colour. */
color?: string;
}
const { as: Tag = 'span', thickness = '0.08em', offset = '0.12em', color = 'currentColor', class: className, style, ...rest } = Astro.props;
const vars = [`--ma-underline-w:${thickness}`, `--ma-underline-offset:${offset}`, `--ma-underline-color:${color}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
---
<Tag class:list={['ma-underline', className]} style={vars} {...rest}><slot /></Tag>
<style is:global>
@layer components {
:where(.ma-underline) {
text-decoration: none;
padding-bottom: var(--ma-underline-offset, 0.12em);
background-image: linear-gradient(var(--ma-underline-color, currentColor), var(--ma-underline-color, currentColor));
background-repeat: no-repeat;
background-size: 0% var(--ma-underline-w, 0.08em);
background-position: 100% 100%;
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
transition: background-size var(--ma-duration-fast) var(--ma-ease-out);
}
@media (hover: hover) {
:where(.ma-underline:hover),
:where(:is(a, button):hover .ma-underline) {
background-size: 100% var(--ma-underline-w, 0.08em);
background-position: 0% 100%;
transition-duration: var(--ma-duration);
transition-timing-function: var(--ma-ease);
}
}
:where(.ma-underline:focus-visible),
:where(:is(a, button):focus-visible .ma-underline) {
background-size: 100% var(--ma-underline-w, 0.08em);
background-position: 0% 100%;
}
@media (prefers-reduced-motion: reduce) {
:where(.ma-underline) {
transition: none;
}
}
}
</style>| Prop | Type | Default | Description |
|---|---|---|---|
as | HTMLTag | 'span' | Element to render. Use a or wrap it in one. |
thickness | string | '0.08em' | Line thickness. |
offset | string | '0.12em' | Gap between the text and the line. |
color | string | 'currentColor' | Line colour. |
The line appears and disappears without sliding.
ClientRouterCSS-only; nothing to rebind.
Replaces: underline hover effects (Animata, uiverse) · Framer Motion layout underlines. See the migration table.
Swells the letter under the cursor to the heaviest weight and lifts it; its neighbours follow at half and a quarter, and everything settles when the pointer leaves.
A highlighter sweeps across the phrases you marked, one after another, when the sentence scrolls into view.
A label rolls upward into a copy of itself on hover, letter by letter from the left, and rolls back when the pointer leaves.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown