Botón de línea editorial
Enlace minimalista: el texto rueda hacia su copia en cursiva, la línea se redibuja y la flecha cruza la esquina.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import type { CSSProperties, ReactNode } from "react";
/** Botón minimalista editorial: el texto rueda a su copia, la línea se redibuja y la flecha cruza. React + TypeScript, sin dependencias. Personaliza con --gclin-*. */
export type BotonLineaProps = {
children?: string;
href?: string;
onClick?: () => void;
icon?: ReactNode;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gclin-root { box-sizing: border-box; display: inline-block; max-width: 100%; color: var(--gclin-fg, #151515); font: 500 17px/1.2 "Newsreader", Georgia, ui-serif, serif; }
.gclin-root *, .gclin-root *::before, .gclin-root *::after { box-sizing: border-box; }
.gclin-root button { font: inherit; -webkit-tap-highlight-color: transparent; }
.gclin-root :focus-visible { outline: 2px solid var(--gclin-fg, #151515); outline-offset: 6px; }
@media (prefers-reduced-motion: reduce) { .gclin-root *, .gclin-root *::before, .gclin-root *::after { animation: none !important; transition-duration: .01ms !important; } }
.gclin-link { position: relative; display: inline-flex; align-items: center; gap: 14px; padding: 12px 0; border: 0; background: none; color: inherit; text-decoration: none; cursor: pointer; }
.gclin-line { position: absolute; left: 0; right: 0; bottom: 6px; height: 1px; background: currentColor; transform-origin: right; transition: transform .5s cubic-bezier(.76, 0, .24, 1); }
.gclin-link:hover .gclin-line, .gclin-link:focus-visible .gclin-line { animation: gclin-redraw .7s cubic-bezier(.76, 0, .24, 1); }
.gclin-roll { position: relative; display: inline-block; overflow: hidden; height: 1.2em; }
.gclin-roll > span { display: block; transition: transform .5s cubic-bezier(.76, 0, .24, 1); }
.gclin-roll > span + span { position: absolute; left: 0; top: 100%; font-style: italic; }
.gclin-link:hover .gclin-roll > span, .gclin-link:focus-visible .gclin-roll > span { transform: translateY(-100%); }
.gclin-arrow { position: relative; overflow: hidden; width: 22px; height: 22px; flex-shrink: 0; }
.gclin-arrow svg { position: absolute; inset: 0; transition: transform .5s cubic-bezier(.76, 0, .24, 1); }
.gclin-arrow svg + svg { transform: translate(-130%, 130%); }
.gclin-link:hover .gclin-arrow svg:first-child, .gclin-link:focus-visible .gclin-arrow svg:first-child { transform: translate(130%, -130%); }
.gclin-link:hover .gclin-arrow svg + svg, .gclin-link:focus-visible .gclin-arrow svg + svg { transform: translate(0, 0); }
@keyframes gclin-redraw { 0% { transform: scaleX(1); transform-origin: right; } 45% { transform: scaleX(0); transform-origin: right; } 46% { transform: scaleX(0); transform-origin: left; } 100% { transform: scaleX(1); transform-origin: left; } }`;
const Arrow = (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M6 18 18 6M8 6h10v10" />
</svg>
);
export function BotonLinea({ children = "Ver el proyecto", href, onClick, icon, className = "", style }: BotonLineaProps) {
const inner = (
<>
<span className="gclin-roll" aria-hidden="true">
<span>{children}</span>
<span>{children}</span>
</span>
<span className="sr-only" style={{ position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" }}>{children}</span>
<span className="gclin-arrow">
{icon ?? Arrow}
{icon ?? Arrow}
</span>
<span className="gclin-line" />
</>
);
return (
<span className={`gclin-root ${className}`} style={style}>
<style>{CSS}</style>
{href ? (
<a className="gclin-link" href={href} onClick={onClick}>
{inner}
</a>
) : (
<button className="gclin-link" type="button" onClick={onClick}>
{inner}
</button>
)}
</span>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/boton-lineaEjemplo de uso de Botón de línea editorial
Después de copiar boton-linea.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { BotonLinea } from "./boton-linea";
export default function Ejemplo() {
return <BotonLinea children="Ejemplo">Contenido</BotonLinea>;
}Propiedades de BotonLinea
BotonLinea acepta 6 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
children? | string | Opcional. |
href? | string | Opcional. |
onClick? | () => void | Opcional. |
icon? | ReactNode | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Personaliza Botón de línea editorial con variables CSS
Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.
| Variable | Valor por defecto |
|---|---|
--gclin-fg | #151515 |
Accesibilidad y rendimiento
El archivo tiene 71 líneas (4 KB) y define 1 animación con @keyframes. Esto es lo que hace Botón de línea editorial por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Es un componente de cliente de Next.js: incluye la directiva "use client".