Border Beam
Светящийся луч бежит по периметру рамки: анимированный угол conic-gradient (@property) + маска-кольцо, чистый CSS.
Пропсы
Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.
| Проп | Тип | По умолчанию | Что это |
|---|---|---|---|
| children? | ReactNode | — | |
| duration? | number | 5 | Длительность оборота, сек. |
| color? | string | "oklch(0.75 0.16 300)" | |
| style? | CSSProperties | — |
Установка
npx shadcn@latest add https://motiva.pages.dev/r/border-beam.jsonЧто нужно в проекте до установки
Файл
components.jsonв корнеИз него CLI узнаёт, куда класть файлы. Алиасы — в такой форме:
"aliases": { "components": "@/components", "lib": "@/lib" }Нет файла — установка не начнётся вовсе.
Алиас
@объявлен дваждыВ корневом
tsconfig.jsonи в конфиге сборщика. В Vite корневой tsconfig часто содержит толькоreferences— этого мало."paths": { "@/*": ["./src/*"] } // tsconfig.json resolve.alias = { "@": "/src" } // vite.config.tsБез
pathsCLI молча создаёт папку с именем@.Токены подключены один раз на приложение
Они приезжают вместе с записью — достаточно импортировать их в точке входа:
import "@/styles/motiva-tokens.css";Без них блок потеряет цвета, ритм и тему.
Своя типографика не спорит с блоком
Наследуемые свойства протекают внутрь:
text-align: centerна обёртке отцентрует содержимое секции. То же касаетсяfont-familyиline-height.Проект без TypeScript? всё равно нужен tsconfig
CLI грузит
tsconfig.jsonбезусловно, а при"tsx": falseидёт заjsconfig.jsonи падает так же. Минимум — файл сpathsи"tsx": trueвcomponents.json.Приехавший рядом
.tsxможно не трогать — вам нужен.vanilla.js.
Экспорт
Код
@property --bb-angle {
syntax: "<angle>"; inherits: false; initial-value: 0deg;
}
.bb-box {
position: relative;
padding: 1.6rem 2.2rem;
border-radius: 16px;
background: oklch(0.18 0.02 270);
border: 1px solid oklch(0.3 0.02 270);
color: oklch(0.92 0.01 265);
isolation: isolate;
}
.bb-box::before {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
padding: 2px;
background: conic-gradient(from var(--bb-angle),
transparent 0deg 300deg, var(--bb-color) 335deg, transparent 360deg);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
mask-composite: exclude;
animation: bb-spin var(--bb-dur) linear infinite;
pointer-events: none;
}
@keyframes bb-spin { to { --bb-angle: 360deg; } }
@media (prefers-reduced-motion: reduce) {
.bb-box::before { animation: none; }
}
const css = `@property --bb-angle {
syntax: "<angle>"; inherits: false; initial-value: 0deg;
}
.bb-box {
position: relative;
padding: 1.6rem 2.2rem;
border-radius: 16px;
background: oklch(0.18 0.02 270);
border: 1px solid oklch(0.3 0.02 270);
color: oklch(0.92 0.01 265);
isolation: isolate;
}
.bb-box::before {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
padding: 2px;
background: conic-gradient(from var(--bb-angle),
transparent 0deg 300deg, var(--bb-color) 335deg, transparent 360deg);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
mask-composite: exclude;
animation: bb-spin var(--bb-dur) linear infinite;
pointer-events: none;
}
@keyframes bb-spin { to { --bb-angle: 360deg; } }
@media (prefers-reduced-motion: reduce) {
.bb-box::before { animation: none; }
}
`;
import type { CSSProperties, ReactNode } from "react";
export interface BorderBeamProps {
children?: ReactNode;
/** Длительность оборота, сек. */
duration?: number;
color?: string;
style?: CSSProperties;
}
/**
* Светящийся «луч» бежит по периметру рамки: анимированный угол conic-gradient
* (@property --bb-angle) + маска-кольцо. Чистый CSS. Reduced-motion/data-shot — луч статичен.
*/
export function BorderBeam({ children, duration = 5, color = "oklch(0.75 0.16 300)", style }: BorderBeamProps) {
return (
<div
className="bb-box"
style={{ "--bb-dur": `${duration}s`, "--bb-color": color, ...style } as CSSProperties}
>
<style href="motiva-border-beam" precedence="medium">{css}</style>
{children ?? (
<div style={{ display: "grid", gap: 6, textAlign: "center" }}>
<strong style={{ fontSize: "1.05rem" }}>Border Beam</strong>
<span style={{ fontSize: "0.85rem", color: "oklch(0.7 0.02 265)" }}>луч бежит по рамке</span>
</div>
)}
</div>
);
}