Mantine Border Animate

Logo

@gfazioli/mantine-border-animate

Mantine component offering five border animation variants (beam, glow, pulse, draw, dash) with a progress-driven border, dashed patterns, hover/focus/in-view triggers and full animation control, perfect for creating dynamic, visually engaging UI elements.

Upgrading from an older major version? See the Upgrade guide for breaking changes and step-by-step migration instructions.

Installation

yarn add @gfazioli/mantine-border-animate

After installation import package styles at the root of your application:

import '@gfazioli/mantine-border-animate/styles.css';

You can import styles within a layer @layer mantine-border-animate by importing @gfazioli/mantine-border-animate/styles.layer.css file.

import '@gfazioli/mantine-border-animate/styles.layer.css';

Usage

The BorderAnimate component wraps any element to add stunning animated border effects. It's perfect for creating eye-catching UI elements like cards, buttons, input fields, alerts, and more.

Key Features

  • Five animation variants - beam, glow, pulse, draw and dash
  • Three beam modes - dot (a traveling dot), wedge (a rotating gradient) or comet (a head with a tail)
  • A border that measures - variant="draw" with progress turns the border into a progress indicator
  • Trigger on interaction - animate on hover, on focus, or only while the component is on screen
  • Fully customizable - Control colors, duration, size, blur, offset and more
  • Works with any element - Wrap buttons, cards, inputs, or any Mantine component
  • Performance optimized - Pure CSS and SVG animations, no JavaScript per frame
  • Custom color stops - Multi-color gradients with colorStops
  • Pause on hover - Pause animations when hovering with pauseOnHover
  • Accessible - Honors prefers-reduced-motion through the Mantine theme

Simply wrap your content with BorderAnimate and customize it using props:

Animate Border

This is an example of BorderAnimate component

Dash cap
Size
Radius
Border width
Blur
Offset
W
H
Duration
Phase
Progress
Spread
Tail
Border opacity
Dash size
Dash gap
Count
Color from
Color to
Track color
import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Flex, Text, Title } from '@mantine/core';

function Demo() {
  return (
    <BorderAnimate  blur="xs" duration={5} trackColor="gray.6">
      <Flex flex={1} direction="column" align="center" justify="center" h="100%" style={{ borderRadius: 'inherit', backgroundColor: 'var(--mantine-color-default)',}}>
        <Title>Animate Border</Title>
        <Text>This is an example of BorderAnimate component</Text>
      </Flex>
    </BorderAnimate>
  );
}

Controlled

You can control the border animation using the show prop.

This is a title

This is a paragraph inside the BorderAnimate component.

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Button, Flex, Paper, Stack, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [show, { toggle }] = useDisclosure(true);

  return (
    <Flex>
      <BorderAnimate show={show} size="lg">
        <Paper withBorder shadow="md" radius="md" p="md">
          <Stack>
            <Title>This is a title</Title>
            <p>This is a paragraph inside the BorderAnimate component.</p>
            <Button onClick={toggle}>Toggle BorderAnimate</Button>
          </Stack>
        </Paper>
      </BorderAnimate>
    </Flex>
  );
}

Animated

You can also control the border animation using the animate prop. If animate is set to true, the border will animate continuously. If animate is set to false, the border will be static. The progress prop then decides where along the perimeter the effect sits, as a percentage from 0 to 100.

This is a title

This is a paragraph inside the BorderAnimate component.

import { useState } from 'react';
import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Button, Flex, Paper, Slider, Stack, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [animate, { toggle }] = useDisclosure(true);
  const [value, setValue] = useState(0);

  return (
    <Flex>
      <BorderAnimate animate={animate} progress={value} size="lg">
        <Paper withBorder shadow="md" radius="md" p="md">
          <Stack>
            <Title>This is a title</Title>
            <p>This is a paragraph inside the BorderAnimate component.</p>
            <Button onClick={toggle}>Toggle Animation</Button>
          </Stack>
        </Paper>
      </BorderAnimate>
      <Slider
        w={260}
        aria-label="Progress along the border"
        value={value}
        onChange={setValue}
        label={(value) => `${value}%`}
      />
    </Flex>
  );
}

Variant

The BorderAnimate component supports five animation variants, each creating a unique visual effect. Use the variant prop to switch between them.

This is a title

This is a paragraph inside the BorderAnimate component.

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Flex, Paper, Stack, Title } from '@mantine/core';

function Demo() {
  return (
    <Flex justify="center" align="center" direction="column" h={400}>
      <BorderAnimate  size="lg">
        <Paper withBorder shadow="md" radius="md" p="md">
          <Stack>
            <Title>This is a title</Title>
            <p>This is a paragraph inside the BorderAnimate component.</p>
          </Stack>
        </Paper>
      </BorderAnimate>
    </Flex>
  );
}

Beam

The beam variant (default) creates an animated glow that moves around the border. It supports three rendering modes via the beamMode prop:

  • dot (default) — a radial-gradient dot that travels along the border perimeter via CSS offset-path. Uniform size at every position and constant speed along the perimeter.
  • wedge — a rotating conic-gradient wedge. The rotation is smooth and continuous, but the visible width varies on rectangular containers (wider at the corners, narrower along the edges) because the sweep is angular rather than linear.
  • comet — a stroked head with a fading tail, drawn along the real perimeter. It is the only mode with a trail, and unlike wedge its speed is constant on any shape.

dot

size="md"

wedge

spread=36 (default)

wedge

spread=140

comet

tail=25 (default)

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Flex, Paper, Stack, Text } from '@mantine/core';

function Demo() {
  return (
    <Flex gap="lg" wrap="wrap" justify="center">
      {/* A soft dot traveling the perimeter: size is its diameter in pixels */}
      <Stack align="center" gap={6}>
        <BorderAnimate borderWidth="sm" size="md" duration={4}>
          <Paper radius="md" p="md" w={140} ta="center">
            <Text size="sm" fw={600}>dot</Text>
          </Paper>
        </BorderAnimate>
        <Text size="xs" c="dimmed">size="md"</Text>
      </Stack>

      {/* A rotating wedge: spread is its width in degrees */}
      <Stack align="center" gap={6}>
        <BorderAnimate beamMode="wedge" borderWidth="sm" duration={4}>
          <Paper radius="md" p="md" w={140} ta="center">
            <Text size="sm" fw={600}>wedge</Text>
          </Paper>
        </BorderAnimate>
        <Text size="xs" c="dimmed">spread={36} (default)</Text>
      </Stack>

      <Stack align="center" gap={6}>
        <BorderAnimate beamMode="wedge" borderWidth="sm" duration={4} spread={140}>
          <Paper radius="md" p="md" w={140} ta="center">
            <Text size="sm" fw={600}>wedge</Text>
          </Paper>
        </BorderAnimate>
        <Text size="xs" c="dimmed">spread={140}</Text>
      </Stack>

      {/* A stroked head with a tail, at constant speed on any shape */}
      <Stack align="center" gap={6}>
        <BorderAnimate beamMode="comet" borderWidth="sm" duration={4}>
          <Paper radius="md" p="md" w={140} ta="center">
            <Text size="sm" fw={600}>comet</Text>
          </Paper>
        </BorderAnimate>
        <Text size="xs" c="dimmed">tail={25} (default)</Text>
      </Stack>
    </Flex>
  );
}

Key props for the beam variant:

  • beamMode - Rendering mode: 'dot' (default), 'wedge' or 'comet'
  • size - Pixel size of the traveling dot (dot mode)
  • spread - Visible width of the wedge in degrees (wedge mode, default 36)
  • tail - Length of the comet tail as a percentage of the perimeter (comet mode, default 25)
  • blur - Softens the effect. It matters most on the comet: SVG cannot fade a stroke along its own path, so the tail is built from stacked strokes, and a small blur — the default — melts their joints together
  • duration - Controls how fast the beam travels
  • colorFrom / colorTo - Colors of the beam
  • colorStops - Full control over the gradient (overrides colorFrom/colorTo)

tail=15

A quick spark

tail=25

The default

tail=50

Half the perimeter

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Flex, Paper, Text } from '@mantine/core';

function Demo() {
  return (
    <Flex gap="xl" wrap="wrap" justify="center">
      {/* Short tail, thin stroke */}
      <BorderAnimate beamMode="comet" borderWidth="sm" tail={15} duration={3}>
        <Paper radius="md" p="md" w={170}>
          <Text fw={600}>tail={15}</Text>
          <Text size="xs" c="dimmed">A quick spark</Text>
        </Paper>
      </BorderAnimate>

      {/* Default tail */}
      <BorderAnimate beamMode="comet" borderWidth="sm" duration={4}>
        <Paper radius="md" p="md" w={170}>
          <Text fw={600}>tail={25}</Text>
          <Text size="xs" c="dimmed">The default</Text>
        </Paper>
      </BorderAnimate>

      {/* Half the perimeter, thicker and slower */}
      <BorderAnimate
        beamMode="comet"
        borderWidth="md"
        tail={50}
        duration={6}
        blur="sm"
        colorFrom="pink.4"
        colorTo="grape.7"
      >
        <Paper radius="md" p="md" w={170}>
          <Text fw={600}>tail={50}</Text>
          <Text size="xs" c="dimmed">Half the perimeter</Text>
        </Paper>
      </BorderAnimate>
    </Flex>
  );
}

Glow

The glow variant produces a pulsating glow effect that fades in and out. The entire border area illuminates with a soft, diffused light that pulses rhythmically. This is perfect for attention-grabbing elements or notification indicators.

Key props for glow variant:

  • duration - Controls the pulse speed
  • blur - Adjusts the softness of the glow
  • borderOpacity - Controls the maximum opacity of the glow

Pulse

The pulse variant creates a subtle scaling animation combined with opacity changes. The border gently expands and fades, creating a breathing effect. This variant is excellent for subtle emphasis without being too distracting.

Key props for pulse variant:

  • duration - Controls the pulse rhythm
  • blur - Softens the border effect

Draw

The draw variant turns the border into a value. progress says how much of the perimeter is drawn, from 0 to 100, and the stroke follows the real border — corners included — so 50% is exactly half of the way around whatever the aspect ratio of the element is.

Because the drawn length is a transition, changing progress animates smoothly on its own. Set withTrack to render the rest of the perimeter underneath, the way a progress bar shows its remaining part.

Key props for the draw variant:

  • progress - How much of the border is drawn (0-100, default 100)
  • withTrack / trackColor - Render and color the untouched part of the perimeter
  • duration - Duration of the transition when progress changes (default 1s for this variant)

Uploading assets

The border is the progress bar: 65% complete

import { useState } from 'react';
import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Group, Paper, Slider, Stack, Switch, Text, Title } from '@mantine/core';

function Demo() {
  const [progress, setProgress] = useState(65);
  const [withTrack, setWithTrack] = useState(true);

  return (
    <Stack w={360}>
      <BorderAnimate
        variant="draw"
        progress={progress}
        withTrack={withTrack}
        trackColor="gray.6"
        borderWidth="sm"
        colorFrom="blue.5"
        colorTo="cyan.4"
      >
        <Paper radius="md" p="md" w="100%">
          <Title order={4}>Uploading assets</Title>
          <Text size="sm" c="dimmed">
            The border is the progress bar: {progress}% complete
          </Text>
        </Paper>
      </BorderAnimate>

      <Group justify="space-between">
        <Slider
          aria-label="Upload progress"
          w={220}
          value={progress}
          onChange={setProgress}
          label={(v) => `${v}%`}
        />
        <Switch
          label="Track"
          checked={withTrack}
          onChange={(event) => setWithTrack(event.currentTarget.checked)}
        />
      </Group>
    </Stack>
  );
}

Dash

The dash variant paints a dashed border and marches it around the perimeter. Lengths are percentages of the perimeter, not pixels, so the pattern stays even on any size or aspect ratio.

Key props for the dash variant:

  • dashSize / dashGap - Length of each dash and of each gap, as percentages of the perimeter (default 4 and 4). Both are nudged by up to half a period so that the pattern divides the perimeter exactly: otherwise the last dash would be cut in half right where the perimeter closes, leaving a visible snag at the first corner
  • count - Number of segments spread evenly along the perimeter; it overrides the absolute lengths and keeps only their ratio
  • dashCap - 'butt' (default) or 'round'. A tiny dashSize with a round cap gives you dots
  • withTrack / trackColor - Render the full perimeter underneath the dashes

Marching ants

dashSize 4 · dashGap 4

Dots

dashCap="round"

Four segments

count=4

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Flex, Paper, Text } from '@mantine/core';

function Demo() {
  return (
    <Flex gap="xl" wrap="wrap" justify="center">
      {/* Marching ants */}
      <BorderAnimate variant="dash" borderWidth="sm" duration={4}>
        <Paper radius="md" p="md" w={160}>
          <Text fw={600}>Marching ants</Text>
          <Text size="xs" c="dimmed">dashSize 4 · dashGap 4</Text>
        </Paper>
      </BorderAnimate>

      {/* Dots: a tiny dash with a round cap */}
      <BorderAnimate
        variant="dash"
        borderWidth="md"
        dashSize={0.5}
        dashGap={5}
        dashCap="round"
        duration={6}
        colorFrom="teal.4"
        colorTo="teal.4"
      >
        <Paper radius="md" p="md" w={160}>
          <Text fw={600}>Dots</Text>
          <Text size="xs" c="dimmed">dashCap="round"</Text>
        </Paper>
      </BorderAnimate>

      {/* Four segments, evenly spaced whatever the size */}
      <BorderAnimate variant="dash" borderWidth="md" count={4} dashCap="round" duration={8}>
        <Paper radius="md" p="md" w={160}>
          <Text fw={600}>Four segments</Text>
          <Text size="xs" c="dimmed">count={4}</Text>
        </Paper>
      </BorderAnimate>
    </Flex>
  );
}

Triggers

By default the animation runs forever. The trigger prop lets an interaction start it instead, which is usually what you want on something a person can actually touch:

  • always (default) - animates continuously
  • hover - fades in and animates while the pointer is over the wrapper
  • focus-within - the same, but driven by focus, so it works with real form controls
  • inView - animates only while the component is inside the viewport
  • never - renders the resting state and never animates

The transition lasts duration, and for variant="draw" the border literally draws itself as the trigger becomes active.

trigger="hover"

Point at me

trigger="inView"

Idle when scrolled away

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Flex, Paper, Text, TextInput } from '@mantine/core';

function Demo() {
  return (
    <Flex gap="xl" wrap="wrap" justify="center">
      {/* The border draws itself while the pointer is over the wrapper */}
      <BorderAnimate variant="draw" trigger="hover" borderWidth="sm">
        <Paper radius="md" p="md" w={190}>
          <Text fw={600}>trigger="hover"</Text>
          <Text size="xs" c="dimmed">Point at me</Text>
        </Paper>
      </BorderAnimate>

      {/* Follows the focus of anything inside, so it works with real form controls */}
      <BorderAnimate trigger="focus-within" borderWidth="sm" radius="sm">
        <TextInput w={190} placeholder="Click into the field" label='trigger="focus-within"' />
      </BorderAnimate>

      {/* Animates only while the component is on screen */}
      <BorderAnimate variant="dash" trigger="inView" borderWidth="sm" duration={4}>
        <Paper radius="md" p="md" w={190}>
          <Text fw={600}>trigger="inView"</Text>
          <Text size="xs" c="dimmed">Idle when scrolled away</Text>
        </Paper>
      </BorderAnimate>
    </Flex>
  );
}

Color Stops

The colorStops prop gives you full control over the beam gradient. Each stop has a color (any Mantine color) and a position (0-100%). When provided, it overrides colorFrom/colorTo.

For dot mode, colorStops creates a radial-gradient with concentric color rings inside the traveling dot. For wedge mode, it creates a custom conic-gradient — use transparent stops for a wedge, or fill the entire circle for a rotating gradient border. For the draw and dash variants the stops become a real SVG gradient along the stroke. The comet mode ignores colorStops: it has no gradient to paint into, because SVG cannot fade a stroke along its own path, so its tail is a stack of strokes each mixed between colorFrom and colorTo.

Wedge beam

Wedge with colorStops

Rainbow

Wedge full gradient

Dot beam

Dot with colorStops

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Box, Flex, Text, Stack } from '@mantine/core';

function Content({ children }: { children: React.ReactNode }) {
  return (
    <Box
      w="100%"
      h="100%"
      p="md"
      style={{
        backgroundColor: 'var(--mantine-color-default)',
        borderRadius: 'var(--mantine-radius-md)',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
      }}
    >
      {children}
    </Box>
  );
}

function Demo() {
  return (
    <Flex gap="xl" align="center" wrap="wrap">
      {/* Wedge beam with a custom spread */}
      <Stack align="center" gap="xs">
        <BorderAnimate
          w={200}
          h={200}
          variant="beam"
          beamMode="wedge"
          size="sm"
          duration={5}
          colorStops={[
            { color: 'transparent', position: 0 },
            { color: 'green', position: 20 },
            { color: 'cyan', position: 40 },
            { color: 'yellow', position: 60 },
            { color: 'red', position: 80 },
            { color: 'transparent', position: 100 },
          ]}
        >
          <Content>
            <Text size="sm">Wedge beam</Text>
          </Content>
        </BorderAnimate>
        <Text size="xs" c="dimmed">Wedge with colorStops</Text>
      </Stack>

      {/* Wedge beam as a full rotating gradient */}
      <Stack align="center" gap="xs">
        <BorderAnimate
          w={200}
          h={200}
          variant="beam"
          beamMode="wedge"
          duration={4}
          colorStops={[
            { color: 'red', position: 0 },
            { color: 'orange', position: 17 },
            { color: 'yellow', position: 33 },
            { color: 'green', position: 50 },
            { color: 'cyan', position: 67 },
            { color: 'blue', position: 83 },
            { color: 'red', position: 100 },
          ]}
        >
          <Content>
            <Text size="sm">Rainbow</Text>
          </Content>
        </BorderAnimate>
        <Text size="xs" c="dimmed">Wedge full gradient</Text>
      </Stack>

      {/* Dot beam with colorStops */}
      <Stack align="center" gap="xs">
        <BorderAnimate
          w={200}
          h={200}
          variant="beam"
          beamMode="dot"
          size="md"
          duration={5}
          colorStops={[
            { color: 'red', position: 0 },
            { color: 'yellow', position: 25 },
            { color: 'cyan', position: 50 },
            { color: 'transparent', position: 70 },
          ]}
        >
          <Content>
            <Text size="sm">Dot beam</Text>
          </Content>
        </BorderAnimate>
        <Text size="xs" c="dimmed">Dot with colorStops</Text>
      </Stack>
    </Flex>
  );
}

Animation Control

Direction and Phase

reverse flips the direction of travel. phase shifts the animation in time: a value of 2 makes the border start as if it had already been running for two seconds, which is how you keep several borders from moving in lockstep. It is a phase offset, not a delay — nothing ever waits.

phase=0

phase=1.33

phase=2.66

reverse

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Box, Flex, Stack, Text } from '@mantine/core';

function Card({ children }: { children: React.ReactNode }) {
  return (
    <Box
      w={130}
      h={70}
      p="xs"
      style={{
        backgroundColor: 'var(--mantine-color-default)',
        borderRadius: 'inherit',
        display: 'grid',
        placeItems: 'center',
      }}
    >
      {children}
    </Box>
  );
}

function Demo() {
  return (
    <Stack align="center" gap={40}>
      {/* Same animation, three phases: the heads stay spread apart forever */}
      <Flex gap="lg" wrap="wrap" justify="center">
        <BorderAnimate beamMode="comet" borderWidth="sm" duration={4} phase={0}>
          <Card>
            <Text size="xs">phase={0}</Text>
          </Card>
        </BorderAnimate>

        <BorderAnimate beamMode="comet" borderWidth="sm" duration={4} phase={1.33}>
          <Card>
            <Text size="xs">phase={1.33}</Text>
          </Card>
        </BorderAnimate>

        <BorderAnimate beamMode="comet" borderWidth="sm" duration={4} phase={2.66}>
          <Card>
            <Text size="xs">phase={2.66}</Text>
          </Card>
        </BorderAnimate>
      </Flex>

      {/* Two rings on the same element, running against each other */}
      <BorderAnimate beamMode="comet" borderWidth="sm" duration={5} offset={10}>
        <BorderAnimate beamMode="comet" borderWidth="sm" duration={5} reverse colorFrom="cyan.4" colorTo="blue.6">
          <Card>
            <Text size="xs">reverse</Text>
          </Card>
        </BorderAnimate>
      </BorderAnimate>
    </Stack>
  );
}

Pause on Hover

Set pauseOnHover to pause the animation when the user hovers over the component. This is useful for interactive elements where the animation might be distracting during interaction.

It is ignored when trigger="hover", since the two would fight over the same gesture: one wants to start the animation on hover, the other wants to stop it.

Hover me!

Beam pauses on hover

Hover me!

Glow pauses on hover

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Box, Flex, Text, Stack } from '@mantine/core';

function Content({ children }: { children: React.ReactNode }) {
  return (
    <Box
      w="100%"
      h="100%"
      p="md"
      style={{
        backgroundColor: 'var(--mantine-color-default)',
        borderRadius: 'var(--mantine-radius-md)',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
      }}
    >
      {children}
    </Box>
  );
}

function Demo() {
  return (
    <Flex gap="xl" align="center">
      <Stack align="center" gap="xs">
        <BorderAnimate w={200} h={150} pauseOnHover>
          <Content>
            <Text size="sm">Hover me!</Text>
          </Content>
        </BorderAnimate>
        <Text size="xs" c="dimmed">Beam pauses on hover</Text>
      </Stack>

      <Stack align="center" gap="xs">
        <BorderAnimate w={200} h={150} variant="glow" blur="sm" pauseOnHover duration={3}>
          <Content>
            <Text size="sm">Hover me!</Text>
          </Content>
        </BorderAnimate>
        <Text size="xs" c="dimmed">Glow pauses on hover</Text>
      </Stack>
    </Flex>
  );
}

Timing Function

The timingFunction prop controls the CSS animation timing function. By default, beam uses linear (constant speed), while glow and pulse use ease-in-out (smooth acceleration). You can override this with any valid CSS timing function.

linear

Constant speed

ease-in-out

Smooth acceleration

steps(8)

Stepped / retro

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Box, Flex, Text, Stack } from '@mantine/core';

function Content({ children }: { children: React.ReactNode }) {
  return (
    <Box
      w="100%"
      h="100%"
      p="md"
      style={{
        backgroundColor: 'var(--mantine-color-default)',
        borderRadius: 'var(--mantine-radius-md)',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
      }}
    >
      {children}
    </Box>
  );
}

function Demo() {
  return (
    <Flex gap="xl" align="center">
      <Stack align="center" gap="xs">
        <BorderAnimate w={180} h={120} timingFunction="linear">
          <Content>
            <Text size="xs">linear</Text>
          </Content>
        </BorderAnimate>
        <Text size="xs" c="dimmed">Constant speed</Text>
      </Stack>

      <Stack align="center" gap="xs">
        <BorderAnimate w={180} h={120} timingFunction="ease-in-out">
          <Content>
            <Text size="xs">ease-in-out</Text>
          </Content>
        </BorderAnimate>
        <Text size="xs" c="dimmed">Smooth acceleration</Text>
      </Stack>

      <Stack align="center" gap="xs">
        <BorderAnimate w={180} h={120} timingFunction="steps(8)">
          <Content>
            <Text size="xs">steps(8)</Text>
          </Content>
        </BorderAnimate>
        <Text size="xs" c="dimmed">Stepped / retro</Text>
      </Stack>
    </Flex>
  );
}

Geometry

Three props decide where the ring is drawn and how thick it is.

borderWidth is the thickness of the band the effect is painted into — a Mantine size token (xs is 1px, xl is 8px) or any number. Widening it makes every variant more substantial, not just longer: the beam becomes a broad sweep, the dashes become blocks, the drawn border becomes a stroke you cannot miss.

radius should match the radius of the content you are wrapping, and then the ring stays glued to it. radius="100%" gives a circle on a square element, an ellipse on anything else.

offset pushes the ring outwards, leaving a gap between the content and the animated border. The radius grows by the same amount, so the ring stays concentric with the element instead of looking pinched at the corners.

offset=0

offset=8

offset="lg"

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Flex, Paper, Text } from '@mantine/core';

function Demo() {
  return (
    <Flex gap={48} p={48} wrap="wrap" justify="center">
      {/* On the element bounds, as usual */}
      <BorderAnimate borderWidth="sm" beamMode="comet">
        <Paper radius="md" p="md" w={150}>
          <Text fw={600}>offset={0}</Text>
        </Paper>
      </BorderAnimate>

      {/* Detached: the ring grows its radius by the same amount to stay concentric */}
      <BorderAnimate borderWidth="sm" beamMode="comet" offset={8}>
        <Paper radius="md" p="md" w={150}>
          <Text fw={600}>offset={8}</Text>
        </Paper>
      </BorderAnimate>

      <BorderAnimate borderWidth="sm" beamMode="comet" offset="lg">
        <Paper radius="md" p="md" w={150}>
          <Text fw={600}>offset="lg"</Text>
        </Paper>
      </BorderAnimate>
    </Flex>
  );
}

Mask and Background Effects

The withMask and zIndex props allow you to create advanced visual effects, including background glows and layered animations.

withMask

By default, withMask is true, which clips the animated effect to the border area only. Set withMask={false} to let the effect extend beyond the border, creating a softer, more diffused look.

The glow variant is the exception: it defaults to withMask={false}, because its whole point is a halo that spreads outwards. Pass withMask explicitly to clip it into a blurred ring instead.

Dot + mask

Clipped to border

Dot no mask

Glow extends outward

Wedge + mask

Wedge clipped

Wedge no mask

Wedge full gradient

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Box, Flex, Text, Stack } from '@mantine/core';

function Content({ children }: { children: React.ReactNode }) {
  return (
    <Box
      w="100%"
      h="100%"
      p="md"
      style={{
        backgroundColor: 'var(--mantine-color-default)',
        borderRadius: 'var(--mantine-radius-md)',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
      }}
    >
      {children}
    </Box>
  );
}

function Demo() {
  return (
    <Flex gap="xl" align="center" wrap="wrap">
      {/* Dot mode: withMask={true} */}
      <Stack align="center" gap="xs">
        <BorderAnimate w={200} h={150} withMask size="lg" blur={4}>
          <Content>
            <Text size="xs">Dot + mask</Text>
          </Content>
        </BorderAnimate>
        <Text size="xs" c="dimmed">Clipped to border</Text>
      </Stack>

      {/* Dot mode: withMask={false} */}
      <Stack align="center" gap="xs">
        <BorderAnimate w={200} h={150} withMask={false} size="lg" blur={4}>
          <Content>
            <Text size="xs">Dot no mask</Text>
          </Content>
        </BorderAnimate>
        <Text size="xs" c="dimmed">Glow extends outward</Text>
      </Stack>

      {/* Wedge mode: withMask={true} */}
      <Stack align="center" gap="xs">
        <BorderAnimate w={200} h={150} beamMode="wedge" withMask size="md" blur="xs">
          <Content>
            <Text size="xs">Wedge + mask</Text>
          </Content>
        </BorderAnimate>
        <Text size="xs" c="dimmed">Wedge clipped</Text>
      </Stack>

      {/* Wedge mode: withMask={false} */}
      <Stack align="center" gap="xs">
        <BorderAnimate w={200} h={150} beamMode="wedge" withMask={false} size="md" blur="xs">
          <Content>
            <Text size="xs">Wedge no mask</Text>
          </Content>
        </BorderAnimate>
        <Text size="xs" c="dimmed">Wedge full gradient</Text>
      </Stack>
    </Flex>
  );
}

zIndex

The zIndex prop controls the stacking order of the border effect. By default, the border appears in front of the content (zIndex={1}). Set zIndex={-1} combined with withMask={false} to create beautiful background glow effects that appear behind your content.

zIndex=1

Border in front

zIndex=-1

Background glow

Layered

Front + background

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Box, Flex, Text, Stack } from '@mantine/core';

function Content({ children }: { children: React.ReactNode }) {
  return (
    <Box
      w="100%"
      h="100%"
      p="md"
      style={{
        backgroundColor: 'var(--mantine-color-default)',
        borderRadius: 'var(--mantine-radius-md)',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
      }}
    >
      {children}
    </Box>
  );
}

function Demo() {
  return (
    <Flex gap="xl" align="center">
      {/* Default: zIndex={1} - border is in front */}
      <Stack align="center" gap="xs">
        <BorderAnimate w={200} h={150} zIndex={1} size="lg" blur={4}>
          <Content>
            <Text size="sm">zIndex=1</Text>
          </Content>
        </BorderAnimate>
        <Text size="xs" c="dimmed">Border in front</Text>
      </Stack>

      {/* zIndex={-1} with withMask={false} - creates background glow effect */}
      <Stack align="center" gap="xs">
        <BorderAnimate w={200} h={150} zIndex={-1} withMask={false} size="xl" blur={14} borderOpacity={0.5}>
          <Content>
            <Text size="sm">zIndex=-1</Text>
          </Content>
        </BorderAnimate>
        <Text size="xs" c="dimmed">Background glow</Text>
      </Stack>

      {/* Combined: layered effect with multiple borders */}
      <Stack align="center" gap="xs">
        <BorderAnimate w={200} h={150} size="md">
          <BorderAnimate w={200} h={150} zIndex={-1} withMask={false} size="xl" blur={20} borderOpacity={0.3} colorFrom="#ff6b6b" colorTo="#2b00ff" duration={8}>
            <Content>
              <Text size="sm">Layered</Text>
            </Content>
          </BorderAnimate>
        </BorderAnimate>
        <Text size="xs" c="dimmed">Front + background</Text>
      </Stack>
    </Flex>
  );
}

Use Cases

BorderAnimate can be used with virtually any Mantine component. Here are some common use cases to inspire your implementations.

Buttons

Add animated borders to buttons to make them stand out. A call to action can animate all the time, but for anything less shouty trigger="hover" is usually the better manner: the button is quiet until the pointer arrives, and then the border draws itself.

Always on

radius=256

trigger="hover"

variant="glow"

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Button, Flex, Stack, Text } from '@mantine/core';

function Demo() {
  return (
    <Flex gap="xl" align="flex-start" wrap="wrap" justify="center">
      {/* Always running: a call to action that asks to be noticed */}
      <Stack align="center" gap="xs">
        <BorderAnimate radius="md" size="sm">
          <Button>Click me</Button>
        </BorderAnimate>
        <Text size="xs" c="dimmed">Always on</Text>
      </Stack>

      {/* Pill shaped: the ring follows any radius, including a fully rounded one */}
      <Stack align="center" gap="xs">
        <BorderAnimate radius={256} size="sm">
          <Button radius={256} variant="default">
            Rounded
          </Button>
        </BorderAnimate>
        <Text size="xs" c="dimmed">radius={256}</Text>
      </Stack>

      {/* Quiet until the pointer arrives */}
      <Stack align="center" gap="xs">
        <BorderAnimate variant="draw" trigger="hover" radius="md" borderWidth="sm">
          <Button variant="default">Hover me</Button>
        </BorderAnimate>
        <Text size="xs" c="dimmed">trigger="hover"</Text>
      </Stack>

      {/* A halo instead of a ring */}
      <Stack align="center" gap="xs">
        <BorderAnimate radius="md" variant="glow" blur="xs">
          <Button variant="light" color="violet">
            Glow
          </Button>
        </BorderAnimate>
        <Text size="xs" c="dimmed">variant="glow"</Text>
      </Stack>
    </Flex>
  );
}

Input Fields

trigger="focus-within" is the answer here: the wrapper follows the focus of the control inside it, so the border lights up exactly when the field is active — no state to wire up, and it keeps working with the keyboard. Combine it with variant="draw" and the border writes itself around the field on focus. A field that needs attention before it is touched is the one case where an always-running animation is right.

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Stack, TextInput } from '@mantine/core';

function Demo() {
  return (
    <Stack w={300}>
      {/* focus-within follows the focus of anything inside the wrapper */}
      <BorderAnimate trigger="focus-within" radius="sm" borderWidth="sm">
        <TextInput w="100%" label="Email" placeholder="you@example.com" />
      </BorderAnimate>

      {/* The draw variant turns that into a border that writes itself */}
      <BorderAnimate
        variant="draw"
        trigger="focus-within"
        radius="sm"
        borderWidth="sm"
        colorFrom="teal.4"
        colorTo="teal.6"
      >
        <TextInput w="100%" label="Full name" placeholder="Click into the field" />
      </BorderAnimate>

      {/* Always on, for a field that needs attention right now */}
      <BorderAnimate variant="pulse" radius="sm" duration={2} colorFrom="orange.6" colorTo="red.6">
        <TextInput w="100%" label="Verification code" placeholder="6 digits" />
      </BorderAnimate>
    </Stack>
  );
}

Alerts and Notifications

Make alerts more noticeable with animated borders. Use different variants and colors to convey urgency levels.

import { IconInfoCircle, IconAlertTriangle } from '@tabler/icons-react';
import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Alert, Flex } from '@mantine/core';

function Demo() {
  return (
    <Flex direction="column" align="center" gap="xl" py={64}>
      <BorderAnimate size="lg" radius="md" duration={10}>
        <Alert
          variant="light"
          color="blue"
          title="Information"
          icon={<IconInfoCircle />}
        >
          This is an informational alert with an animated border effect.
        </Alert>
      </BorderAnimate>

      <BorderAnimate
        radius="md"
        duration={2}
        colorFrom="red"
        colorTo="orange"
        variant="glow"
      >
        <Alert
          variant="light"
          color="red"
          title="Warning"
          icon={<IconAlertTriangle />}
        >
          This is a warning alert with a glowing border effect.
        </Alert>
      </BorderAnimate>
    </Flex>
  );
}

Cards

Create premium-looking cards with animated borders. Great for featured content, pricing cards, or special offers.

Norway

Premium Package

On Sale

Get access to all premium features with our special animated border effect that highlights this exclusive offer.

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Card, Image, Text, Badge, Button, Group } from '@mantine/core';

function Demo() {
  return (
    <BorderAnimate variant="glow" blur="md" radius="md" duration={3}>
      <Card shadow="sm" padding="lg" radius="md" withBorder w={340}>
        <Card.Section>
          <Image
            src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-8.png"
            height={160}
            alt="Norway"
          />
        </Card.Section>

        <Group justify="space-between" mt="md" mb="xs">
          <Text fw={500}>Premium Package</Text>
          <Badge color="pink">On Sale</Badge>
        </Group>

        <Text size="sm" c="dimmed">
          Get access to all premium features with our special animated
          border effect that highlights this exclusive offer.
        </Text>

        <Button color="blue" fullWidth mt="md" radius="md">
          Get Started
        </Button>
      </Card>
    </BorderAnimate>
  );
}

Chips and Badges

Even small elements like chips can benefit from animated borders to indicate selection or special status.

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Chip, Flex } from '@mantine/core';

function Demo() {
  return (
    <Flex gap="md" align="center">
      <BorderAnimate size="xs" radius="xl">
        <Chip defaultChecked>Selected</Chip>
      </BorderAnimate>

      <BorderAnimate radius="xl" variant="pulse" colorTo="white" p={3}>
        <Chip defaultChecked color="green">Active</Chip>
      </BorderAnimate>

      <BorderAnimate radius="xl" variant="pulse" duration={2}>
        <Chip defaultChecked color="violet">Premium</Chip>
      </BorderAnimate>
    </Flex>
  );
}

Accordion

Wrap accordion components with animated borders to create visually engaging expandable sections. The border effect adds a modern touch to FAQ sections or collapsible content areas.

Crisp and refreshing fruit. Apples are known for their versatility and nutritional benefits.
import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Accordion } from '@mantine/core';

const data = [
  {
    emoji: '🍎',
    value: 'Apples',
    description:
      'Crisp and refreshing fruit. Apples are known for their versatility and nutritional benefits.',
  },
  {
    emoji: '🍌',
    value: 'Bananas',
    description:
      'Naturally sweet and potassium-rich fruit. Bananas are a popular choice for energy-boosting.',
  },
  {
    emoji: '🥦',
    value: 'Broccoli',
    description:
      'Nutrient-packed green vegetable. Broccoli is packed with vitamins, minerals, and fiber.',
  },
];

function Demo() {
  const items = data.map((item) => (
    <Accordion.Item key={item.value} value={item.value}>
      <Accordion.Control icon={item.emoji}>{item.value}</Accordion.Control>
      <Accordion.Panel>{item.description}</Accordion.Panel>
    </Accordion.Item>
  ));

  return (
    <BorderAnimate size="lg" radius="md">
      <Accordion variant="contained" defaultValue="Apples">
        {items}
      </Accordion>
    </BorderAnimate>
  );
}

Multiple Borders

Nest multiple BorderAnimate components to create complex, layered effects. Combine different variants, speeds, directions and colors — reverse and phase are what keep the layers from moving in lockstep. This technique is perfect for hero sections or premium UI elements that need extra visual impact.

Multiple Animated Borders

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Box, Flex, Text } from '@mantine/core';

function Content({ children }: { children: React.ReactNode }) {
  return (
    <Box
      w="100%"
      h="100%"
      p="md"
      style={{
        backgroundColor: 'var(--mantine-color-default)',
        borderRadius: 'var(--mantine-radius-md)',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
      }}
    >
      {children}
    </Box>
  );
}

function Demo() {
  return (
    <BorderAnimate w={400} h={250}>
      <BorderAnimate w={400} h={250} duration={55} reverse borderWidth={1} size="xl" colorFrom="#ff6b6b" colorTo="#2b00ff">
        <BorderAnimate w={400} h={250} duration={23} phase={8} withMask={false} size="xl" borderOpacity={0.2} blur={14} zIndex={-1}>
          <BorderAnimate w={400} h={250} variant="glow" blur={4}>
            <Content>
              <Text fw={500}>Multiple Animated Borders</Text>
            </Content>
          </BorderAnimate>
        </BorderAnimate>
      </BorderAnimate>
    </BorderAnimate>
  );
}

Circular Elements

BorderAnimate works perfectly with circular shapes. Set radius="100%" to create animated borders around avatars, profile pictures, or any circular UI element, and the effect follows the circle instead of cutting corners. It also gives you a ring progress for free: variant="draw" on a square element with radius="100%" measures the circumference exactly.

Circle

Simple circle

Avatar

Avatar

Pulse

Pulse variant

70%

draw + radius="100%"

import { BorderAnimate } from '@gfazioli/mantine-border-animate';
import { Avatar, Box, Flex, Stack, Text } from '@mantine/core';

function CircleContent({ children }: { children: React.ReactNode }) {
  return (
    <Box
      w="100%"
      h="100%"
      style={{
        backgroundColor: 'var(--mantine-color-default)',
        borderRadius: '100%',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
      }}
    >
      {children}
    </Box>
  );
}

function Demo() {
  return (
    <Flex gap="xl" align="center" wrap="wrap" justify="center">
      {/* Simple circle */}
      <Stack align="center" gap="xs">
        <BorderAnimate w={100} h={100} radius="100%">
          <CircleContent>
            <Text size="xs">Circle</Text>
          </CircleContent>
        </BorderAnimate>
        <Text size="xs" c="dimmed">Simple circle</Text>
      </Stack>

      {/* Avatar with glow */}
      <Stack align="center" gap="xs">
        <BorderAnimate w={80} h={80} variant="glow" radius="100%" colorFrom="green" colorTo="cyan">
          <Avatar
            src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-1.png"
            alt="Avatar"
            radius="100%"
            size={80}
          />
        </BorderAnimate>
        <Text size="xs" c="dimmed">Avatar</Text>
      </Stack>

      {/* Larger circle with the pulse variant */}
      <Stack align="center" gap="xs">
        <BorderAnimate w={120} h={120} radius="100%" variant="pulse" duration={3}>
          <CircleContent>
            <Text size="xs">Pulse</Text>
          </CircleContent>
        </BorderAnimate>
        <Text size="xs" c="dimmed">Pulse variant</Text>
      </Stack>

      {/* A circular progress: the draw variant follows the ring exactly */}
      <Stack align="center" gap="xs">
        <BorderAnimate
          w={100}
          h={100}
          radius="100%"
          variant="draw"
          progress={70}
          borderWidth="md"
          withTrack
          colorFrom="blue.4"
          colorTo="cyan.4"
        >
          <CircleContent>
            <Text size="xs">70%</Text>
          </CircleContent>
        </BorderAnimate>
        <Text size="xs" c="dimmed">draw + radius="100%"</Text>
      </Stack>
    </Flex>
  );
}

Accessibility

BorderAnimate honors the prefers-reduced-motion media query through the Mantine theme. When a user has enabled "Reduce motion" in their operating system settings, every animation and transition is disabled — including the ones on pseudo-elements, which the rule shipped by Mantine core cannot reach on its own.

Because the guard is scoped to theme.respectReducedMotion, an application that deliberately turns that setting off keeps its animations, exactly like every other Mantine component.

The animated ring is decorative and carries aria-hidden, so it is never announced by assistive technology.