BorderBeam

BorderBeam (테두리 빔)

BorderBeam는 컨테이너에 장식용으로 흐르는 빔(beam) 효과를 더해 시각적 강조를 주고 싶을 때 사용하는 컴포넌트예요.

출처: 문서

본문

언제 사용하나요

  • 비즈니스 상태 의미를 도입하지 않으면서 컨테이너에 더 강한 시각적 강조가 필요할 때.
  • 로그인 패널, 추천 카드, AI 모듈, 핵심 CTA 블록에 적합해요.
  • 장식 효과로, 포커스 링, 검증 테두리, 상태 피드백을 대체해서는 안 돼요.

예제 (Examples)

기본 (Basic)

기본 사용법이에요. BorderBeam으로 어떤 컨테이너든 감싸면 그 테두리를 따라 연속적인 장식 빔 효과가 더해져요.

import React from 'react';
import { BorderBeam, Card } from 'antd';

const App: React.FC = () => (
  <div style={{ width: 360 }}>
    <BorderBeam>
      <Card title="Workspace overview">
        Review task status, deployment health, and recent automation activity in one panel.
      </Card>
    </BorderBeam>
  </div>
);

export default App;

호버 시 표시 (Show on hover)

기본적으로 테두리 빔을 숨기고, 컨테이너에 호버할 때 나타나게 해요.

import React from 'react';
import { BorderBeam, Card } from 'antd';
import { createStyles } from 'antd-style';

const useStyles = createStyles((props) => {
  const { css, prefixCls, cssVar } = props;
  return {
    card: css`
      width: 360px;
      .${prefixCls}-border-beam {
        opacity: 0;
        transition: opacity ${cssVar.motionDurationMid};
        &::before {
          animation-play-state: paused;
        }
      }
      &:hover {
        .${prefixCls}-border-beam {
          opacity: 1;
          &::before {
            animation-play-state: running;
          }
        }
      }
    `,
  };
});

const Demo: React.FC = () => {
  const { styles } = useStyles();
  return (
    <BorderBeam>
      <Card className={styles.card} title="Hover over the card">
        The border beam appears when the pointer moves over this card.
      </Card>
    </BorderBeam>
  );
};

export default Demo;

여러 빔 (Multiple beams)

count로 빔의 개수를 설정해요. 여러 빔은 컨테이너 테두리 주변에 고르게 분포돼요. 양의 정수여야 하며 기본값은 1이에요.

import React from 'react';
import { BorderBeam, Card, Flex } from 'antd';

const App: React.FC = () => (
  <Flex vertical gap="medium">
    <BorderBeam count={3}>
      <Card title="Multiple beams">
        Set count to distribute multiple beams evenly around the container border.
      </Card>
    </BorderBeam>
    <BorderBeam count={2}>
      <Card title="Multiple beams">
        Set count to distribute multiple beams evenly around the container border.
      </Card>
    </BorderBeam>
  </Flex>
);

export default App;

커스텀 컨테이너 (Custom container)

커스텀 컨테이너도 BorderBeam을 호스팅할 수 있어요. 빔 레이어는 자식 노드에 삽입되고 position: absolute로 컨테이너 가장자리를 따라 배치되므로, 호스트 요소가 포지셔닝 컨텍스트를 제공해야 해요. 대부분 position: relative를 설정하면 돼요.

import React from 'react';
import { BorderBeam } from 'antd';

const panelStyle: React.CSSProperties = {
  position: 'relative',
  width: 420,
  background: '#fff',
  border: '1px solid #f0f0f0',
  borderRadius: 8,
};

const contentStyle: React.CSSProperties = {
  minHeight: 160,
  padding: 24,
  color: 'rgba(0, 0, 0, 0.88)',
  lineHeight: 1.5715,
};

const App: React.FC = () => (
  <BorderBeam>
    <div style={panelStyle}>
      <div style={contentStyle}>
        Review task status, deployment health, and recent automation activity in one custom
        container.
      </div>
    </div>
  </BorderBeam>
);

export default App;

그라데이션 (Gradients)

여섯 가지 그라데이션 빔 팔레트를 보여주고 그 사이를 전환해요.

import React from 'react';
import { BorderBeam, Card, Flex, Segmented, Tag, Typography } from 'antd';
import type { BorderBeamGradient } from 'antd';

const presets: Array<{
  key: string;
  name: string;
  usage: string;
  description: string;
  color: BorderBeamGradient;
}> = [
  {
    key: 'ocean',
    name: 'Ocean',
    usage: 'Dashboard',
    description: 'A calm blue-green accent that works well for data views and cloud tooling.',
    color: [
      { color: '#1677ff', percent: 0 },
      { color: '#36cfc9', percent: 52 },
      { color: '#95de64', percent: 100 },
    ],
  },
  {
    key: 'sunset',
    name: 'Sunset',
    usage: 'Upgrade',
    description: 'A warm highlight for upgrade prompts, featured cards, and marketing blocks.',
    color: [
      { color: '#ff7a45', percent: 0 },
      { color: '#ff4d4f', percent: 49 },
      { color: '#ff85c0', percent: 100 },
    ],
  },
  {
    key: 'aurora',
    name: 'Aurora',
    usage: 'AI',
    description:
      'A vivid cool-toned beam suited for AI assistants, copilots, and automation panels.',
    color: [
      { color: '#7c3aed', percent: 0 },
      { color: '#06b6d4', percent: 57 },
      { color: '#67e8f9', percent: 100 },
    ],
  },
  {
    key: 'forest',
    name: 'Forest',
    usage: 'Recommendation',
    description:
      'A bright natural palette that feels good on recommendation and growth-oriented cards.',
    color: [
      { color: '#22c55e', percent: 0 },
      { color: '#a3e635', percent: 54 },
      { color: '#facc15', percent: 100 },
    ],
  },
  {
    key: 'ember',
    name: 'Ember',
    usage: 'Alert',
    description: 'A high-energy warm gradient for important alerts, launch cards, and hot paths.',
    color: [
      { color: '#fa541c', percent: 0 },
      { color: '#ff7875', percent: 46 },
      { color: '#ffd666', percent: 100 },
    ],
  },
  {
    key: 'nebula',
    name: 'Nebula',
    usage: 'Labs',
    description: 'A cool purple-pink mix that fits experimental modules and product lab surfaces.',
    color: [
      { color: '#2f54eb', percent: 0 },
      { color: '#722ed1', percent: 44 },
      { color: '#ff85c0', percent: 100 },
    ],
  },
];

const defaultPresetKey = presets[0].key;

const App: React.FC = () => {
  const [currentPresetKey, setCurrentPresetKey] = React.useState(defaultPresetKey);
  const currentPreset = presets.find((preset) => preset.key === currentPresetKey) ?? presets[0];

  return (
    <Flex vertical gap={16} style={{ maxWidth: 480 }}>
      <Segmented
        block
        options={presets.map((preset) => ({
          label: preset.name,
          value: preset.key,
        }))}
        value={currentPresetKey}
        onChange={(value) => setCurrentPresetKey(value as string)}
      />
      <BorderBeam color={currentPreset.color}>
        <Card
          title={currentPreset.name}
          extra={<Tag variant="filled">{currentPreset.usage}</Tag>}
          styles={{
            body: {
              display: 'flex',
              flexDirection: 'column',
              gap: 16,
            },
          }}
        >
          <Typography.Text type="secondary">{currentPreset.description}</Typography.Text>
          <Flex gap={8} wrap>
            {currentPreset.color.map((item) => (
              <Tag key={`${item.color}-${item.percent}`} color={item.color} variant="filled">
                {item.color} · {item.percent}%
              </Tag>
            ))}
          </Flex>
          <Typography.Text type="secondary">
            Stop positions use the public 0-100 input range.
          </Typography.Text>
        </Card>
      </BorderBeam>
    </Flex>
  );
};

export default App;

지속시간 (Duration)

duration으로 빔이 한 바퀴를 도는 데 걸리는 시간(초)을 제어해요. 기본값은 6초예요.

import React from 'react';
import { BorderBeam, Card, Flex, Tag, Typography } from 'antd';

const durations = [
  {
    name: 'Fast',
    seconds: 3,
    description: 'A quick loop for temporary highlights and active modules.',
  },
  {
    name: 'Default',
    seconds: 6,
    description: 'The original pacing for most emphasized containers.',
  },
  {
    name: 'Slow',
    seconds: 12,
    description: 'A calmer loop for persistent panels and ambient surfaces.',
  },
];

const App: React.FC = () => (
  <Flex gap={16} wrap>
    {durations.map(({ name, seconds, description }) => (
      <div key={name} style={{ width: 220 }}>
        <BorderBeam duration={seconds}>
          <Card title={name} extra={<Tag variant="filled">{seconds}s</Tag>}>
            <Typography.Text type="secondary">{description}</Typography.Text>
          </Card>
        </BorderBeam>
      </div>
    ))}
  </Flex>
);

export default App;

크기 (Size)

size로 보이는 빔 세그먼트의 크기를 제어해요. 기본값은 100px이고, 숫자는 픽셀로 처리돼요.

import React from 'react';
import { BorderBeam, Card, Tag, Typography } from 'antd';

const sizes: Array<{
  name: string;
  size?: number | string;
  bodyMinHeight: number;
  description: string;
  spanFull?: boolean;
}> = [
  {
    name: 'Default',
    bodyMinHeight: 112,
    description: 'Uses the default 100px visible beam segment.',
  },
  {
    name: 'Compact',
    size: 56,
    bodyMinHeight: 112,
    description: 'Keeps the highlight shorter for dense card groups.',
  },
  {
    name: 'Extended',
    size: 160,
    bodyMinHeight: 192,
    description: 'Creates a longer highlight for wider feature panels.',
    spanFull: true,
  },
];

const App: React.FC = () => (
  <div
    style={{
      display: 'grid',
      gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
      gap: 32,
      maxWidth: 960,
    }}
  >
    {sizes.map(({ name, size, bodyMinHeight, description, spanFull }) => (
      <div key={name} style={{ gridColumn: spanFull ? '1 / -1' : undefined }}>
        <BorderBeam size={size}>
          <Card
            title={name}
            extra={<Tag variant="filled">{size ?? 100}px</Tag>}
            styles={{ body: { minHeight: bodyMinHeight, display: 'flex', alignItems: 'center' } }}
          >
            <Typography.Text type="secondary">{description}</Typography.Text>
          </Card>
        </BorderBeam>
      </div>
    ))}
  </div>
);

export default App;

선 너비 (Line width)

lineWidth로 개별 BorderBeam의 빔 너비를 조절해요. 기본값은 1px이고, 숫자는 픽셀로 처리돼요.

import React from 'react';
import { BorderBeam, Card } from 'antd';

const App: React.FC = () => (
  <div style={{ width: 360 }}>
    <BorderBeam lineWidth={2}>
      <Card title="Custom line width" style={{ borderWidth: 2 }}>
        Set lineWidth to match the border width of this container.
      </Card>
    </BorderBeam>
  </div>
);

export default App;

API

공통 props 참고: Common props

BorderBeam

속성 설명 타입 기본값 버전 전역 설정
children 장식할 콘텐츠 ReactNode - 6.4.0 ×
color 빔 색 설정. 단일 색 문자열 또는 그라데이션 스톱을 지원. percent는 0 ~ 100 입력 범위를 사용하고 BorderBeam은 투명 페이드용으로 꼬리 공간을 남겨 둠 string | { color: string; percent: number }[] - 6.4.0 ×
count 빔 개수 number 1 6.6.0 ×
duration 빔이 한 바퀴 도는 시간(초) number 6 6.5.0 ×
lineWidth 빔 선의 너비. 숫자는 픽셀로 처리 number | string 1px 6.5.0 ×
outset 빔 레이어의 컨테이너 가장자리로부터 바깥 거리. 잘린(clipped) 컨테이너에는 0으로 설정 number | string - 6.4.0 ×
size 보이는 빔 세그먼트의 크기. 숫자는 픽셀로 처리 number | string 100 6.5.0 ×

디자인 토큰 (Design Token)

전역 토큰 (Global Token)

토큰 이름 설명 타입 기본값
colorPrimary 브랜드 색. 제품의 특성과 커뮤니케이션을 반영하는 가장 직접적인 시각 요소. 선택하면 완전한 색 팔레트가 자동 생성 string
colorPrimaryHover 기본 색 그라데이션 아래의 호버 상태. string
lineWidth 기본 컴포넌트의 테두리 너비 number

FAQ

축소 모션(reduced motion)이 켜지면 BorderBeam은 어떻게 동작하나요? {#faq-reduced-motion}

BorderBeam은 빔을 장식 효과로 취급해요. prefers-reduced-motion: reduce가 활성화되면 빔 효과가 숨겨져요.

color의 percent는 무엇을 의미하나요? {#faq-color-percent}

percent는 작성자가 지정한 스톱 위치를 나타내며 0부터 100까지의 값을 받아요. BorderBeam은 그 스톱들을 보이는 빔 세그먼트에 매핑하고, 움직이는 꼬리가 계속 보이도록 투명 페이드아웃용으로 뒤쪽 공간을 남겨 둬요.

size 제한 {#faq-size-limit}

BorderBeam은 한 변의 길이가 size인 정사각형 그라데이션 레이어로 빔을 만들어요. 레이어는 컨테이너 테두리를 따라 이동하고, 마스크가 테두리와 겹치는 영역을 노출해요. size는 테두리 경로 길이와 무관하게 한 변의 길이를 설정해요.

가로 가장자리를 따라 그라데이션 레이어는 가장자리의 양쪽으로 약 size / 2만큼 확장돼요. size가 마스크 오버레이 높이의 두 배에 가까워지거나 넘어가면 정사각형이 위·아래 가장자리를 모두 덮을 수 있어요. 빔이 세로 가장자리를 따라 이동할 때도 너비에 같은 기하가 적용돼요.

size를 마스크 오버레이의 짧은 쪽의 두 배보다 훨씬 작게 유지하세요: size < 2 × min(width, height). 마스크 오버레이는 보통 장식된 컨테이너와 크기가 비슷하고, outset이 그 크기를 바꿔요. 테두리 반지름, lineWidth, 그라데이션의 투명 영역도 겹침이 보이기 시작하는 지점에 영향을 줘요.

BorderBeam이 동작하지 않는 이유는? {#faq-not-working}

BorderBeam은 children에서 실제 DOM 노드를 찾아 그 노드에 빔 레이어를 삽입해야 해요. 감싼 콘텐츠가 네이티브 DOM 요소이거나, ref를 DOM 요소로 올바르게 전달하는 React 컴포넌트인지 확인하세요. 그렇지 않으면 BorderBeam이 실제 컨테이너를 찾을 수 없어 빔을 렌더링할 수 없어요.

빔 레이어는 position: absolute로 배치되므로, 찾은 DOM 노드도 포지셔닝 컨텍스트를 제공해야 해요. 대부분 감싼 요소에 position: relative를 설정하면 돼요. BorderBeam은 자식의 포지셔닝 스타일을 검사하거나 고쳐 주지 않아요.

성능상의 이유로 children이 빔을 호스팅할 수 있는지와 그 포지셔닝 정보는 초기화 중에 해석되며, 나중에 자식 구조나 포지셔닝 스타일이 바뀌어도 계속 갱신되지 않아요.

빔 반지름을 내 컨테이너와 어떻게 맞추나요? {#faq-radius}

BorderBeam은 빔 레이어를 실제 컨테이너의 자식으로 렌더링하고 border-radius: inherit로 그 반지름을 직접 상속해요. Card 같은 단일 컨테이너 자식의 경우 빔은 자동으로 컨테이너 반지름을 따라요. 더 복잡한 자식 트리에서는 실제 컨테이너 루트에 반지름이 설정되어 있는지 확인하세요.

반지름은 CSS 상속을 통해 동기화되며, 초기화 중에 읽거나 측정되지 않아요. className, 반응형 스타일, CSS 변수로 나중에 변경해도 빔 레이어에 자동으로 반영돼요.

더 알아보기 (Learn more)