Lamp Glow
Лампа над сценой: тонкая перекладина света разгорается из середины, вниз идёт конус, и содержимое стоит в освещённом пятне. Конус — один conic-gradient с вершиной в перекладине, свечение — box-shadow, затухание — статичная маска: ни одного filter: blur, дышит только opacity.
Пропсы
Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.
| Проп | Тип | По умолчанию | Что это |
|---|---|---|---|
| backdrop? | string | false | BACKDROP | Подложка под светом: CSS-значение `background` или `false`. `false` нужен, когда запись стоит фоном СЕКЦИИ и подложку задаёт она. |
| hue? | number | 300 | Тон света, градусы oklch. |
| width? | number | 34 | Длина перекладины, cq-единицы ширины (доля контейнера). |
| strength? | number | 0.6 | Яркость свечения (0.2–1). |
| children? | ReactNode | — | Содержимое в конусе света (в витрине — заголовок, в секции обычно пусто). |
Установка
npx shadcn@latest add https://motiva.pages.dev/r/lamp-glow.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.
Экспорт
Код
.lg { container-type: size; }
/* лампа: тонкая перекладина света. База = финал: при animation: none лампа горит */
.lg-bar {
position: absolute;
left: 50%;
top: 38cqh;
width: var(--lg-w);
height: 5px;
margin-left: calc(var(--lg-w) / -2);
border-radius: 999px;
background: var(--lg-bar);
/* свечение перекладины — box-shadow, а не blur: растр кешируется композитором */
box-shadow: 0 0 16px var(--lg-halo1), 0 0 64px var(--lg-halo2);
opacity: 1;
animation: lg-bar-in 1.1s cubic-bezier(0.22, 1, 0.36, 1) both;
}
/* конус света вниз от лампы: один conic-gradient с вершиной в перекладине,
затухание по дальности — статичной маской (не анимируется — перерисовки нет) */
.lg-cone {
position: absolute;
left: 50%;
top: calc(38cqh + 2px);
width: 160cqw;
height: 64cqh;
margin-left: -80cqw;
background: conic-gradient(
from 0deg at 50% 0%,
transparent 142deg,
var(--lg-cone) 166deg,
var(--lg-cone) 194deg,
transparent 218deg
);
-webkit-mask-image: linear-gradient(to bottom, #000, transparent 94%);
mask-image: linear-gradient(to bottom, #000, transparent 94%);
opacity: 1;
animation: lg-fade 1.5s ease-out 0.15s both, lg-breathe 9s ease-in-out 1.65s infinite;
}
/* подсвеченный воздух над лампой: света вверх идёт мало — потолок, а не второй конус */
.lg-air {
position: absolute;
left: 50%;
top: 38cqh;
width: 64cqw;
height: 17cqh;
margin-left: -32cqw;
translate: 0 -100%;
background: radial-gradient(50% 100% at 50% 100%, var(--lg-halo2), transparent 70%);
opacity: 0.8;
animation: lg-fade 1.5s ease-out 0.3s both;
}
@keyframes lg-bar-in {
from { opacity: 0; scale: 0.25 1; }
to { opacity: 1; scale: 1 1; }
}
@keyframes lg-fade {
from { opacity: 0; }
to { opacity: 1; }
}
/* дыхание — только opacity: композитный канал, растры конуса не перерисовываются */
@keyframes lg-breathe {
0%, 100% { opacity: 1; }
50% { opacity: 0.86; }
}
@media (prefers-reduced-motion: reduce) {
.lg-bar, .lg-cone, .lg-air { animation: none; }
}
const css = `.lg { container-type: size; }
/* лампа: тонкая перекладина света. База = финал: при animation: none лампа горит */
.lg-bar {
position: absolute;
left: 50%;
top: 38cqh;
width: var(--lg-w);
height: 5px;
margin-left: calc(var(--lg-w) / -2);
border-radius: 999px;
background: var(--lg-bar);
/* свечение перекладины — box-shadow, а не blur: растр кешируется композитором */
box-shadow: 0 0 16px var(--lg-halo1), 0 0 64px var(--lg-halo2);
opacity: 1;
animation: lg-bar-in 1.1s cubic-bezier(0.22, 1, 0.36, 1) both;
}
/* конус света вниз от лампы: один conic-gradient с вершиной в перекладине,
затухание по дальности — статичной маской (не анимируется — перерисовки нет) */
.lg-cone {
position: absolute;
left: 50%;
top: calc(38cqh + 2px);
width: 160cqw;
height: 64cqh;
margin-left: -80cqw;
background: conic-gradient(
from 0deg at 50% 0%,
transparent 142deg,
var(--lg-cone) 166deg,
var(--lg-cone) 194deg,
transparent 218deg
);
-webkit-mask-image: linear-gradient(to bottom, #000, transparent 94%);
mask-image: linear-gradient(to bottom, #000, transparent 94%);
opacity: 1;
animation: lg-fade 1.5s ease-out 0.15s both, lg-breathe 9s ease-in-out 1.65s infinite;
}
/* подсвеченный воздух над лампой: света вверх идёт мало — потолок, а не второй конус */
.lg-air {
position: absolute;
left: 50%;
top: 38cqh;
width: 64cqw;
height: 17cqh;
margin-left: -32cqw;
translate: 0 -100%;
background: radial-gradient(50% 100% at 50% 100%, var(--lg-halo2), transparent 70%);
opacity: 0.8;
animation: lg-fade 1.5s ease-out 0.3s both;
}
@keyframes lg-bar-in {
from { opacity: 0; scale: 0.25 1; }
to { opacity: 1; scale: 1 1; }
}
@keyframes lg-fade {
from { opacity: 0; }
to { opacity: 1; }
}
/* дыхание — только opacity: композитный канал, растры конуса не перерисовываются */
@keyframes lg-breathe {
0%, 100% { opacity: 1; }
50% { opacity: 0.86; }
}
@media (prefers-reduced-motion: reduce) {
.lg-bar, .lg-cone, .lg-air { animation: none; }
}
`;
import type { CSSProperties, ReactNode } from "react";
/** Подложка по умолчанию — как в витрине: лампа без темноты не читается. */
const BACKDROP = "oklch(0.12 0.015 275)";
export interface LampGlowProps {
/**
* Подложка под светом: CSS-значение `background` или `false`.
* `false` нужен, когда запись стоит фоном СЕКЦИИ и подложку задаёт она.
*/
backdrop?: string | false;
/** Тон света, градусы oklch. */
hue?: number;
/** Длина перекладины, cq-единицы ширины (доля контейнера). */
width?: number;
/** Яркость свечения (0.2–1). */
strength?: number;
/** Содержимое в конусе света (в витрине — заголовок, в секции обычно пусто). */
children?: ReactNode;
}
/**
* Лампа над сценой: тонкая перекладина света, вниз от неё — конус, содержимое стоит
* в освещённом пятне. Конус — ОДИН conic-gradient с вершиной в перекладине, затухание
* по дальности — статичная маска; свечение перекладины — box-shadow. Ни одного
* filter: blur — все растры кешируются композитором, дышит только opacity.
*
* Появление — one-shot: перекладина разгорается из середины (scale по X), конус и
* воздух над лампой проявляются следом. База = финал: под `prefers-reduced-motion`
* и `data-shot` лампа просто горит.
*/
export function LampGlow({
backdrop = BACKDROP,
hue = 300,
width = 34,
strength = 0.6,
children,
}: LampGlowProps = {}) {
return (
<div
className="lg"
style={{
position: "absolute",
inset: 0,
overflow: "hidden",
...(backdrop === false ? null : { background: backdrop }),
}}
>
<style href="motiva-lamp-glow" precedence="medium">{css}</style>
<div
style={
{
position: "absolute",
inset: 0,
"--lg-w": `${width}cqw`,
"--lg-bar": `oklch(0.95 0.05 ${hue})`,
"--lg-halo1": `oklch(0.85 0.12 ${hue} / ${strength * 0.85})`,
"--lg-halo2": `oklch(0.74 0.15 ${hue} / ${strength * 0.45})`,
"--lg-cone": `oklch(0.83 0.11 ${hue} / ${strength * 0.4})`,
} as CSSProperties
}
>
<div className="lg-air" />
<div className="lg-cone" />
<div className="lg-bar" />
</div>
{children && (
<div
style={{
position: "absolute",
inset: 0,
display: "grid",
placeItems: "center",
paddingTop: "30cqh",
}}
>
{children}
</div>
)}
</div>
);
}