← каталог
Link Styles
Инвентарь ссылок для билдера: семь характеров подчёркивания и хода — выезд слева, рост из центра, «уходит и приходит», маркер-подложка, градиентная линия, стрелка и улетающая внешняя. Всё на transform/background-size, focus-visible = hover.
csshoverbasic0reduced-motion ✓
Пропсы
Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.
| Проп | Тип | По умолчанию | Что это |
|---|---|---|---|
| styleKind? | LinkStyle | "slide" | Характер ссылки: slide | center | exit | marker | gradient | arrow | external. |
| children | ReactNode | — |
Установка
npx shadcn@latest add https://motiva.pages.dev/r/link-styles.jsonЧто нужно в проекте до установки
components.jsonв корне — с алиасами в форме"components": "@/components". Нет файла — CLI не поймёт, куда класть.- Алиас
@объявлен дважды:"paths": {"@/*": ["./src/*"]}в корневомtsconfig.json(в Vite он часто содержит толькоreferences— тогда CLI создаёт папку@) иresolve.aliasв конфиге сборщика. - Токены подключены один раз на приложение:
import "@/styles/motiva-tokens.css"— они приезжают вместе с записью. Без них секция потеряет цвета, ритм и тему. - Своя типографика не должна спорить: если у вас на обёртке
text-align: center, он протечёт внутрь блоков. - Проект без TypeScript? Всё равно нужен минимальный
tsconfig.jsonсpathsи"tsx": trueвcomponents.json: CLI грузит tsconfig безусловно, а с"tsx": falseидёт заjsconfig.jsonи падает так же. Приехавший рядом.tsxможно не трогать — вам нужен.vanilla.js.
Экспорт
Код
import type { AnchorHTMLAttributes, ReactNode } from "react";
import "./link-styles.css";
export type LinkStyle = "slide" | "center" | "exit" | "marker" | "gradient" | "arrow" | "external";
export interface StyledLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
/** Характер ссылки: slide | center | exit | marker | gradient | arrow | external. */
styleKind?: LinkStyle;
children: ReactNode;
}
/**
* Инвентарь ссылок для билдера: семь характеров подчёркивания и хода — от классического
* «выезжает слева» до маркера-подложки и улетающей стрелки внешней ссылки. Все анимации
* на transform/background-size (без layout), focus-visible получает то же движение,
* что и hover. Токены с фолбэком — работает и с дизайн-системой, и без неё.
*/
export function LinkStyles({ styleKind = "slide", children, className, ...props }: StyledLinkProps) {
const tail =
styleKind === "arrow" ? (
<span className="msl-tail" aria-hidden="true">
→
</span>
) : styleKind === "external" ? (
<span className="msl-tail" aria-hidden="true">
<span>↗</span>
</span>
) : null;
return (
<a className={["msl", `msl--${styleKind}`, className ?? ""].filter(Boolean).join(" ")} {...props}>
{children}
{tail}
</a>
);
}
/* Инвентарь ссылок: каждый вариант — свой характер подчёркивания/хода.
Все анимации на transform/background-size, без layout; focus-visible = hover. */
.msl {
--msl-accent: var(--ds-accent, oklch(0.72 0.16 300));
--msl-fg: var(--ds-fg, oklch(0.92 0.01 265));
position: relative;
display: inline-flex;
align-items: baseline;
gap: 0.35em;
color: var(--msl-fg);
text-decoration: none;
font: inherit;
transition: color var(--ds-fast, 0.18s) var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
}
.msl:hover,
.msl:focus-visible {
color: var(--msl-accent);
}
.msl:focus-visible {
outline: 2px solid var(--msl-accent);
outline-offset: 3px;
border-radius: 2px;
}
/* линия-псевдоэлемент: общая заготовка для вариантов с подчёркиванием */
.msl--slide::after,
.msl--center::after,
.msl--exit::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -0.14em;
height: 1.5px;
background: currentColor;
transform: scaleX(0);
transition: transform var(--ds-base, 0.32s) var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
}
/* выезжает слева */
.msl--slide::after {
transform-origin: left;
}
.msl--slide:hover::after,
.msl--slide:focus-visible::after {
transform: scaleX(1);
}
/* растёт из центра */
.msl--center::after {
transform-origin: center;
}
.msl--center:hover::after,
.msl--center:focus-visible::after {
transform: scaleX(1);
}
/* уходит вправо, приходит слева: origin меняется между состояниями, и линия
не «сжимается обратно», а продолжает движение в ту же сторону */
.msl--exit::after {
transform: scaleX(1);
transform-origin: right;
}
.msl--exit:hover::after,
.msl--exit:focus-visible::after {
transform: scaleX(0);
}
.msl--exit:not(:hover):not(:focus-visible)::after {
animation: msl-return var(--ds-base, 0.32s) var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
}
@keyframes msl-return {
from { transform: scaleX(0); transform-origin: left; }
to { transform: scaleX(1); transform-origin: left; }
}
/* маркер: подчёркивание набирает высоту и превращается в подложку */
.msl--marker {
background: linear-gradient(color-mix(in oklab, var(--msl-accent) 32%, transparent), color-mix(in oklab, var(--msl-accent) 32%, transparent))
0 92% / 100% 0.12em no-repeat;
transition:
background-size var(--ds-base, 0.32s) var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1)),
color var(--ds-fast, 0.18s);
}
.msl--marker:hover,
.msl--marker:focus-visible {
color: var(--msl-fg);
background-size: 100% 88%;
}
/* градиентное подчёркивание, едет вбок */
.msl--gradient {
background:
linear-gradient(
90deg,
var(--msl-accent),
color-mix(in oklch longer hue, var(--msl-accent) 55%, oklch(0.7 0.15 220)),
var(--msl-accent)
)
0 100% / 200% 1.5px no-repeat;
transition: background-position var(--ds-slow, 0.6s) var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1)), color var(--ds-fast, 0.18s);
}
.msl--gradient:hover,
.msl--gradient:focus-visible {
background-position: 100% 100%;
}
/* стрелка выезжает, текст остаётся на месте */
.msl--arrow .msl-tail {
display: inline-block;
transform: translateX(0);
transition: transform var(--ds-base, 0.32s) var(--ds-ease-spring, cubic-bezier(0.34, 1.56, 0.64, 1));
}
.msl--arrow:hover .msl-tail,
.msl--arrow:focus-visible .msl-tail {
transform: translateX(0.3em);
}
/* внешняя ссылка: стрелка уходит по диагонали и возвращается копией — «улетела» */
.msl--external .msl-tail {
display: inline-block;
overflow: hidden;
width: 1em;
height: 1em;
line-height: 1;
}
.msl--external .msl-tail span {
display: inline-block;
transition: transform var(--ds-base, 0.32s) var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
}
.msl--external:hover .msl-tail span,
.msl--external:focus-visible .msl-tail span {
transform: translate(1.1em, -1.1em);
}
.msl--external .msl-tail span::after {
content: "↗";
position: absolute;
transform: translate(-1.1em, 1.1em);
}
@media (prefers-reduced-motion: reduce) {
.msl,
.msl::after,
.msl .msl-tail,
.msl .msl-tail span {
transition: none;
animation: none;
}
}
Похожие эффекты
лицензия: MITавтор: Motivaисточник: оригинал (Motiva)v1.0.0