# Border Beam (/ui/components/react/border-beam)




## Usage [#usage]

```tsx
import { BorderBeam } from '@appica/ui-react/border-beam'
```

```tsx
<BorderBeam>
  <YourContent />
</BorderBeam>
```

`BorderBeam` is a `<div>` wrapper that traces a single comet of light around its own border: a bright
head with a tail fading to transparent, looping forever. The beam is painted as a conic sweep clipped to
a hairline ring, so it follows the wrapper's own `border-radius` exactly, from a subtle `rounded-xl` card
to a fully rounded pill, with no radius prop to keep in sync.

Give the wrapper the same size and radius as the content it wraps (that's what the beam traces) and tune
the look with `color`, `length` (how much of the lap the comet spans), `thickness`, and `speed`. The beam
layer is `aria-hidden` and `pointer-events-none`, so it never reaches assistive tech or intercepts
clicks, and the wrapper forwards `className`, `style`, and any native `<div>` attributes.

## Examples [#examples]

### Basic beam [#basic-beam]

The default beam is a 1px comet in the `--primary` color, spanning 10% of the border and taking 5
seconds per lap. Match the wrapper's radius to your content's (`rounded-2xl` here) so the beam hugs the
corners.

```tsx
import { BorderBeam } from '@appica/ui-react/border-beam'

export default function BorderBeamBasic() {
  return (
    <BorderBeam className="w-full max-w-70 rounded-2xl">
      <div className="bg-background border-border text-foreground-intense flex min-h-32 items-center justify-center rounded-2xl border px-6 text-center text-sm font-medium">
        Syncing your workspace
      </div>
    </BorderBeam>
  )
}
```

### Color [#color]

`color` takes any CSS color - a hex, `rgb()`, `oklch()`, or a design token - and becomes the bright head
of the gradient, which always fades to transparent along the tail. The default, `var(--primary)`, is
near-black in light mode and white in dark mode, so it reads on either surface. For an accent, reach for
a token's `-emphasis` step (`var(--success-emphasis)`): those hold up against both a light and a dark
background, which the base step doesn't. Over a colored or dark child surface, pick a color with
contrast against that surface rather than against the page.

```tsx
import { BorderBeam } from '@appica/ui-react/border-beam'

const CARD =
  'bg-background border-border text-foreground flex min-h-24 items-center justify-center rounded-2xl border px-5 text-center text-sm'

export default function BorderBeamColor() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-6">
      <BorderBeam className="w-40 rounded-2xl" length={20}>
        <div className={CARD}>Primary</div>
      </BorderBeam>
      <BorderBeam className="w-40 rounded-2xl" color="var(--secondary-emphasis)" length={20}>
        <div className={CARD}>Secondary</div>
      </BorderBeam>
      <BorderBeam className="w-40 rounded-2xl" color="var(--success-emphasis)" length={20}>
        <div className={CARD}>Success</div>
      </BorderBeam>
      <BorderBeam className="w-40 rounded-2xl" color="#A78BFA" length={20}>
        <div className={CARD}>Custom</div>
      </BorderBeam>
    </div>
  )
}
```

### Length, thickness, and speed [#length-thickness-and-speed]

`length` is a percentage of one lap rather than a pixel value, so a comet keeps its proportions whatever
the element's size: a small value reads as a traveling spark, a large one as a long sweeping streak.
`thickness` is in px, and `speed` is the seconds one lap takes.

```tsx
import { BorderBeam } from '@appica/ui-react/border-beam'

const CARD =
  'bg-background border-border text-foreground flex min-h-24 items-center justify-center rounded-2xl border px-5 text-center text-sm'

export default function BorderBeamSizeAndSpeed() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-6">
      <BorderBeam className="w-44 rounded-2xl" length={3} speed={3}>
        <div className={CARD}>Short and quick</div>
      </BorderBeam>
      <BorderBeam className="w-44 rounded-2xl" length={30} speed={10}>
        <div className={CARD}>Long and slow</div>
      </BorderBeam>
      <BorderBeam className="w-44 rounded-2xl" thickness={3}>
        <div className={CARD}>3px thick</div>
      </BorderBeam>
    </div>
  )
}
```

### Staggering a group [#staggering-a-group]

A row of beams starting together looks mechanical. Pass a negative `delay` to start a beam mid-lap, which
spreads a group out around its own cycle.

```tsx
import { BorderBeam } from '@appica/ui-react/border-beam'

const CARD =
  'bg-background border-border text-foreground flex min-h-28 items-center justify-center rounded-2xl border px-5 text-center text-sm'

const DELAYS = [0, -2, -4]

export default function BorderBeamStagger() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-6">
      {DELAYS.map((delay) => (
        <BorderBeam key={delay} className="w-40 rounded-2xl" delay={delay}>
          <div className={CARD}>delay={delay}</div>
        </BorderBeam>
      ))}
    </div>
  )
}
```

### Reveal on interaction [#reveal-on-interaction]

Set `revealOn` to keep the beam hidden until an interaction, then fade it in (the beam resumes from
paused, so it always starts its lap from the top). Accepts `'hover'` (pointer-only, like Tailwind's
`hover:` variant) and `'press'` (works on touch), or an array to combine them. Since `'hover'` can't fire
on touch, opt into `showOnTouch` to keep the beam persistently visible there instead of hidden. Add
`pressScale` so the beam shrinks with a button's own active-press scale.

```tsx
import { BorderBeam } from '@appica/ui-react/border-beam'
import { buttonVariants } from '@appica/ui-react/button'
import { cn } from '@/lib/utils'

export default function BorderBeamRevealOnInteraction() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-8">
      <BorderBeam revealOn="hover" showOnTouch pressScale className="rounded-full">
        <button type="button" className={cn(buttonVariants({ variant: 'outline', size: 'lg' }), 'rounded-full')}>
          Hover me
        </button>
      </BorderBeam>
      <BorderBeam revealOn="press" pressScale className="rounded-full">
        <button type="button" className={cn(buttonVariants({ variant: 'outline', size: 'lg' }), 'rounded-full')}>
          Press me
        </button>
      </BorderBeam>
    </div>
  )
}
```

### Controlled (loading state) [#controlled-loading-state]

For event-driven states (a request in flight, a deploy running) drive the beam with the controlled
`reveal` boolean instead. It's OR-ed with any `revealOn` interaction, so you can combine "beam on hover"
with "beam while loading". Click the button to kick off a mock request.

```tsx
'use client'

import { useRef, useState } from 'react'
import { BorderBeam } from '@appica/ui-react/border-beam'
import { buttonVariants } from '@appica/ui-react/button'
import { Spinner } from '@appica/ui-react/spinner'
import { Sparkle } from '@appica/icons-react'
import { cn } from '@/lib/utils'

export default function BorderBeamLoading() {
  const [loading, setLoading] = useState(false)
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null)

  function run() {
    if (timer.current) clearTimeout(timer.current)
    setLoading(true)
    timer.current = setTimeout(() => setLoading(false), 5000)
  }

  return (
    <BorderBeam reveal={loading} color="#A78BFA" length={15} thickness={1.5} speed={2} className="rounded-full">
      <button
        type="button"
        onClick={run}
        className={cn(buttonVariants({ variant: 'outline', size: 'lg' }), 'rounded-full')}
      >
        {loading ? (
          <Spinner variant="sparkle" currentColor data-icon="start" className="text-xl" />
        ) : (
          <Sparkle data-icon="start" />
        )}
        {loading ? 'Thinking…' : 'Ask AI'}
      </button>
    </BorderBeam>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

The beam is mirrored under `dir="rtl"`, so it laps counterclockwise and still leads with its bright head,
matching the reading direction. No prop is involved; the layer follows the `dir` attribute. For setup
details and caveats, see the [RTL guide](/ui/docs/react/rtl).


## API reference [#api-reference]

| Prop          | Type | Default            | Description                                                                                 |
| ------------- | ------------------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| `color`       | `string`                                    | `'var(--primary)'` | Beam color. It lights the head of the comet and fades to transparent along the tail, so pass a solid color rather than a gradient. |
| `length`      | `number`                                    | `10`               | How much of the border the comet spans, in percent of one lap.                                                                     |
| `thickness`   | `number`                                    | `1`                | Beam thickness in px.                                                                                                              |
| `speed`       | `number`                                    | `5`                | Seconds for one full lap around the border.                                                                                        |
| `delay`       | `number`                                    | `0`                | Seconds before the first lap starts. Negative values start the beam mid-lap, which is how you desynchronize a group of cards.      |
| `revealOn`    | `'hover' \| 'press' \| Array`    | -                  | Reveal only on interaction. `'hover'` is pointer-only; `'press'` works on touch. Combine via an array. Omit for always-on.         |
| `reveal`      | `boolean`                                   | -                  | Controlled visibility for programmatic states (loading, etc.); OR-ed with `revealOn`.                                              |
| `showOnTouch` | `boolean`                                   | `false`            | With `revealOn="hover"`, keep the beam visible on touch devices (which have no hover) instead of hidden.                           |
| `pressScale`  | `boolean`                                   | `false`            | Scale the beam down while pressed, to track a child `Button`'s own active-press scale.                                             |
| `className`   | `string`                                    | -                  | Extra classes, merged via `tailwind-merge`. Set the size and radius here - the beam traces the wrapper.                            |
| `style`       | `CSSProperties`                             | -                  | Inline styles on the wrapper.                                                                                                      |

`BorderBeam` renders a `<div>` and forwards every remaining native `<div>` attribute. The beam layer is
`aria-hidden` and `pointer-events-none`.

## Accessibility [#accessibility]

* The beam layer is `aria-hidden` and `pointer-events-none`, so it's skipped by assistive tech and never
  intercepts clicks.
* Content is wrapped, not replaced - your `children` stay in the normal flow and fully accessible.
* The motion honors `prefers-reduced-motion`: a looping beam has no static resting state to fall back
  to, so the layer is hidden entirely for reduced-motion users rather than parked at one corner. Never
  make the beam the only signal for a state (loading, recording); pair it with text, a spinner, or a
  status message.
* The beam is purely decorative - keep your content's own border and text contrast intact so nothing
  depends on it.
