Skip to content
Docs
Metis text

Metis text

Hover draws an underline in from the left; on exit it retracts back out to the right.

requires interactionhover
Loading...

Installation

CLI

pnpm dlx shadcn@latest add https://animata.design/r/text/metis-text.json

The CLI installs metis-text.tsx and metis-text.css together.

Manual

Run the following command

mkdir -p animata/text && touch animata/text/metis-text.tsx animata/text/metis-text.css

Paste the code

@layer components {
  .metis-text::after {
    content: "";
    position: absolute;
    left: 0;
    top: 100%;
    width: 100%;
    height: 1px;
    background: currentcolor;
    pointer-events: none;
    transform-origin: 100% 50%;
    transform: scale3d(0, 1, 1);
    transition: transform 0.3s;
  }
 
  .metis-text:hover::after,
  .metis-text:focus-visible::after {
    transform-origin: 0% 50%;
    transform: scale3d(1, 1, 1);
  }
 
  .group:hover .metis-text--group::after,
  .group:focus-visible .metis-text--group::after {
    transform-origin: 0% 50%;
    transform: scale3d(1, 1, 1);
  }
 
  @media (prefers-reduced-motion: reduce) {
    .metis-text::after {
      transition: none;
    }
  }
}
import type React from "react";
 
import { cn } from "@/lib/utils";
 
import "./metis-text.css";
 
export interface MetisTextProps extends React.HTMLAttributes<HTMLElement> {
  children: React.ReactNode;
  as?: "span" | "a" | "p";
  /**
   * When true, the underline reacts to an ancestor with `.group` on
   * `:hover` / `:focus-visible` — useful inside card links.
   * @default false
   */
  groupHover?: boolean;
  /** Classes for the inner label wrapper — put `truncate` / `line-clamp-*` here. */
  labelClassName?: string;
}
 
/**
 * Codrops Metis underline — scaleX reveal with flipped transform-origin on enter/exit.
 * Keep truncation on `labelClassName`, not the root, so the line is not clipped.
 * @see https://github.com/codrops/LineHoverStyles/
 */
export default function MetisText({
  children,
  as: Tag = "span",
  groupHover = false,
  className,
  labelClassName,
  ...props
}: MetisTextProps) {
  return (
    <Tag
      className={cn(
        "metis-text relative inline-block max-w-full",
        groupHover && "metis-text--group",
        className,
      )}
      {...props}
    >
      <span className={cn("block", labelClassName)}>{children}</span>
    </Tag>
  );
}

Usage

import MetisText from "@/animata/text/metis-text";
 
<MetisText className="text-sm text-foreground">About us</MetisText>

Inside a card or list row, pass groupHover so the line follows the whole link, not just the label. Put truncate or line-clamp-* on labelClassName — if you clip the root, the underline gets clipped too.

<a href="..." className="group block">
  <MetisText groupHover labelClassName="truncate text-sm" className="text-foreground">
    Site title
  </MetisText>
</a>

How it works

Layout classes (relative, inline-block, max-w-full, block on the label) live on the TSX via Tailwind. The Metis scaleX underline — ::after, transform-origin flip on enter/exit, and .group hover — lives in metis-text.css because pseudo-elements and origin swaps are awkward in utility classes alone.

Credits

Built by sudha

Underline pattern from Codrops Line Hover Styles (Metis effect).