ATOM

LinkButton

Inline text that shimmers as you hover, then navigates like a link. Secondary CTAs inside paragraphs or cards.

Preview

Sizes: xssmdefaultlgxl (default: default)

Incluye animación (GSAP / motion).

Editorial

<!-- F12c editorial — non-derivable only. Review: Karen. -->

## Ejemplos

Secondary CTA that navigates:

```tsx import { LinkButton } from '@/components/atoms/LinkButton';

<LinkButton href="https://docs.example.com" size="default" animated> Read the docs </LinkButton> ```

## Accesibilidad

- Ships as a real `<a href>` — keep a clear text label in `children` (string only). - Default `target="_blank"` + `rel="noopener noreferrer"`: warn users when leaving the app, or override `target` for same-tab in-app routes. - `disabled` sets `aria-disabled` but still renders an anchor — prefer removing the link or blocking navigation in the router when inactive.

### Correcto

- Siempre renderiza como `<a>` con href — es un link semantico, no un boton - target='_blank' + rel='noopener noreferrer' aplicados automaticamente - aria-disabled se agrega cuando `disabled={true}` - focus-visible muestra outline para navegacion por teclado - prefers-reduced-motion desactiva shimmer y transiciones

### Evitar

- No usar LinkButton para acciones (submit, delete) — usar Button con href o un `<button>` - No pasar JSX como children — solo string (necesario para el shimmer gradient) - No combinar con onClick para acciones — es un `<a>`, navega, no ejecuta

## Cuándo no usar

- In-page actions without navigation → `Button`. - Icon-only chrome → `IconButton`.

## Criterio de uso

- Úsalo cuando la acción cambia de ubicación y el texto debe fluir dentro de un párrafo, card o CTA secundario. - Conserva un `href` real. No conviertas el componente en un botón con `onClick`: perdería semántica, teclado y comportamiento esperado del navegador. - `animated` funciona mejor como énfasis puntual; déjalo apagado en listas legales, tablas o interfaces donde el usuario repite navegación.

## Gotchas

- El contenido animado debe ser texto; si necesitas composición rica o una acción, usa el componente apropiado. - Si el destino es externo y abre una pestaña nueva, comunica ese cambio en el contexto visible; si es navegación interna, configura el router para conservar la experiencia de la aplicación. - **Nota**: Los nombres de size son diferentes a Button: usa 'sm' y 'default' en vez de 's' y 'm'. Esto es porque LinkButton sigue la escala tipografica, no la escala de componentes interactivos. - **Nota**: CSS autocontenido. La animacion de underline y el shimmer son puro CSS — no necesitan JS. - **Nota**: Todas las animaciones son puro CSS. No requieren GSAP ni JS. El shimmer se desactiva automaticamente con prefers-reduced-motion: reduce.

Uso

import { LinkButton } from '@/components/atoms/LinkButton';

<LinkButton href="/docs" size="default" animated>
  Read the docs
</LinkButton>

Props

PropTipoDefaultRango / opcionesWhatHow
sizeselectdefault`xs`, `sm`, `default`, `lg`, `xl`Type and padding scale of the inline link control.default for body copy; sm/xs for dense meta; lg/xl for hero links. Default: default.
disabledbooleanfalse`true` / `false`Prevents navigation and dims the link.Prefer removing the link over a dead disabled style when possible. Default: false.
animatedbooleanfalse`true` / `false`Whether the shimmer / text-swap motion runs on hover.true for marketing CTAs in body; false in dense legal lists. Default: false.

Gotchas

  • a11y

    Requires a real href for keyboard and screen readers; do not fake links with onClick only.

  • react

    children must be a string for animated text-swap paths.

Anatomía CSS

<div class="link-button">
  <span class="link-button__text"></span>
</div>
ClasePropósito
link-buttonroot
link-button--animatedmodifier
link-button--defaultmodifier
link-button--disabledmodifier
link-button--lgmodifier
link-button--smmodifier
link-button--xlmodifier
link-button--xsmodifier
link-button__textelement

Tokens resueltos

Valores finales tras seguir la cadena de tokens. Derivados del source: si un token cambia, esta tabla cambia sola.

VariantePropValorToken
alllink.button.fg.disabled#525252link.button.fg.disabled

Animaciones

PropiedadDuraciónEasing
color0.3scubic-bezier(0.19
1
0.22
1)
transform0.735scubic-bezier(0.625
0.05
0
1)
@keyframes link-button-shimmer

CSS mínimo funcional

Autocontenido: sin imports ni tokens. Para previews y prototipos — en producción se consume el CSS del DS (@atom-uikit/css/components.css</code>, <code>@atom-uikit/tokens/tokens.css).

/* -------------------------------------------------------------------------
   Link Button

   Semantic link (<a>) for external navigation. Not a button.
   Blue text with animated underline on hover. No background, no border.

   Sizes:    xs, sm, default, lg, xl
   States:   enabled, hover, focus, disabled
   ------------------------------------------------------------------------- */

.link-button {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  padding: 0;
  background: none;
  border: none;
  font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
  font-weight: 500;
  line-height: 1;
  color: #0e7490;
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  text-decoration: none;
  transition: color 0.3s cubic-bezier(0.19, 1, 0.22, 1);
}

.link-button:hover:not(.link-button--disabled) {
  color: #0a0a0a;
}

.link-button:active:not(.link-button--disabled) {
  color: #0a0a0a;
}

.link-button:focus-visible {
  outline: 2px solid #0a0a0a;
  outline-offset: 1.5px;
  border-radius: 0.125em;
}

@media (prefers-reduced-motion: reduce) {
  .link-button {
    transition-duration: 0ms;
  }
}

/* ---- Sizes ---- */

.link-button--xs      { font-size: 10.24px; }
.link-button--sm      { font-size: 12.8px; }
.link-button--default { font-size: 16px; }
.link-button--lg      { font-size: 20px; }
.link-button--xl      { font-size: 25px; }

/* ---- States ---- */

.link-button--disabled {
  color: #525252;
  cursor: not-allowed;
  pointer-events: none;
}

/* ---- Parts ---- */

.link-button__text {
  display: inline;
  position: relative;
}

/* ---- Underline animation ---- */

.link-button__text::before {
  content: "";
  position: absolute;
  bottom: -0.0625em;
  left: 0;
  width: 100%;
  height: 0.0625em;
  background-color: currentColor;
  transition: transform 0.735s cubic-bezier(0.625, 0.05, 0, 1);
  transform-origin: right;
  transform: scaleX(0) rotate(0.001deg);
}

@media (hover: hover) and (pointer: fine) {
  .link-button--animated:hover:not(.link-button--disabled) .link-button__text::before,
  [data-hover]:hover .link-button--animated:not(.link-button--disabled) .link-button__text::before {
    transform-origin: left;
    transform: scaleX(1) rotate(0.001deg);
  }
}

/* ---- Animated variant: persistent underline + text shimmer ---- */

.link-button--animated:not(.link-button--disabled) .link-button__text::before {
  transform-origin: left;
  transform: scaleX(1) rotate(0.001deg);
}

.link-button--animated:not(.link-button--disabled) .link-button__text {
  background: linear-gradient(
    120deg,
    currentColor 0%,
    currentColor 35%,
    color-mix(in srgb, currentColor 40%, white) 50%,
    currentColor 65%,
    currentColor 100%
  );
  background-size: 200% 100%;
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: link-button-shimmer 3s ease-in-out infinite;
}

@keyframes link-button-shimmer {
  0%, 100% { background-position: 200% center; }
  50% { background-position: 0% center; }
}

@media (prefers-reduced-motion: reduce) {
  .link-button--animated:not(.link-button--disabled) .link-button__text {
    animation: none;
    background: none;
    -webkit-text-fill-color: currentColor;
  }
}

Codigo fuente

components-react
components/atoms/LinkButton.tsx
import { forwardRef, type AnchorHTMLAttributes } from 'react';

type LinkButtonSize = 'xs' | 'sm' | 'default' | 'lg' | 'xl';

export type LinkButtonProps = {
  size?: LinkButtonSize;
  disabled?: boolean;
  animated?: boolean;
  children: string;
  className?: string;
  href: string;
} & Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'children'>;

function cn(...classes: (string | false | undefined | null)[]) {
  return classes.filter(Boolean).join(' ');
}

export const LinkButton = forwardRef<HTMLAnchorElement, LinkButtonProps>(
  (
    {
      size = 'default',
      disabled = false,
      animated = false,
      children,
      className,
      href,
      ...props
    },
    ref,
  ) => {
    const classes = cn(
      'link-button',
      `link-button--${size}`,
      disabled && 'link-button--disabled',
      animated && 'link-button--animated',
      className,
    );

    return (
      <a
        ref={ref}
        className={classes}
        href={href}
        aria-disabled={disabled || undefined}
        target="_blank"
        rel="noopener noreferrer"
        {...props}
      >
        <span className="link-button__text">{children}</span>
      </a>
    );
  },
);

LinkButton.displayName = 'LinkButton';

Webflow

Webflowlink-button

Pega en el Designer como application/json, luego convierte a Component (Atom / LinkButton) y publica. Formato interno no documentado de Webflow — regenerable, no dependencia de runtime.

Setup del sitio (una vez)

Custom Code → Head:

<link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/tokens.css">
<link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/components.css">
<link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/webflow.css">

Unsupported (no silencioso)

  • :focus-visible on .link-button:focus-visiblepseudo-class not a safe Designer variant — moved to head Custom Code
  • selector on .link-button:hover:not(.link-button--disabled)compound/descendant selector — moved to head Custom Code (Designer styles are single-class)
  • selector on .link-button:active:not(.link-button--disabled)compound/descendant selector — moved to head Custom Code (Designer styles are single-class)
  • selector on .link-button__text::beforecompound/descendant selector — moved to head Custom Code (Designer styles are single-class)
  • selector on .link-button--animated:not(.link-button--disabled) .link-button__text::beforecompound/descendant selector — moved to head Custom Code (Designer styles are single-class)
  • selector on .link-button--animated:not(.link-button--disabled) .link-button__textcompound/descendant selector — moved to head Custom Code (Designer styles are single-class)
  • selector on .link-button:focus-visiblecompound/descendant selector — moved to head Custom Code (Designer styles are single-class)
  • @media on (prefers-reduced-motion: reduce)not a Designer breakpoint — moved to head Custom Code block

Tras pegar: Create component → nombre Atom / LinkButton → Publish.

Componentes relacionados

On this page

Detalles

Publicado14 de mayo de 2026
Categoriaactions
Lectura...
Visitas...
Ayuda?Slack

Componente

LinkButton

Source

components-react / cssDisponible via MCP: atom_uikit_source("link-button")
Abrir en Storybook