GSAP in Webflow · Recipes

Draw a border on hover with a conic gradient

There is no border — a slightly larger wrapper behind the element leaves a visible ring, filled by a conic gradient whose fill percentage is registered with @property so it can actually transition; GSAP's only job is toggling one class on the hovered instance's wrapper.

  • advanced
  • Last verified: July 27, 2026
  • Verified against: cloneable

The rule

There is no border property anywhere in this. Wrap the element in a slightly larger circle; the inner element covers most of it, leaving a thin ring visible. Give the ring a conic gradient — conic because colour spreads radially around a circle — and animate how far it fills.

Three parts, and GSAP is the smallest of them:

PartJob
@property --fillmakes the fill percentage a typed value, so it can transition
The CSS transitiondoes the actual animation
GSAPa set action toggling one class on the right wrapper

The CSS

<!-- Embed on the body, display: none, dragged above the section so it loads first -->
<style>
  @property --fill {
    syntax: '<percentage>';
    inherits: true;
    initial-value: 0%;
  }

  [data-animate-link-border] {
    transition: --fill 0.8s cubic-bezier(.19, 1, .22, 1);
    background: conic-gradient(greenyellow var(--fill), transparent var(--fill));

    &.on-hover {
      --fill: 100%;
    }
  }
</style>

@property is the load-bearing line. You cannot smoothly transition an ordinary CSS custom property — the browser treats it as an untyped string and swaps it instantly. Registering it with syntax: '<percentage>' tells the browser it is a real percentage, and only then does the transition on --fill do anything. Remove the @property block and the border snaps from 0 to 100 with no animation, which is the most likely way this build fails.

Build it

1. The element. A link block: flex-centred, square, border-radius: 100%. Inside, an embed with your SVG (fill: currentColor). Put the colour on the link, not the embed, add a colour transition, and set the hover-state colour there.

2. The fake-border wrapper. Wrap it in a slightly larger square, flex-centred, border-radius: 100%. Add the attribute data-animate-link-border.

3. The CSS. Embed on the body, display: none, dragged above the section — the @property registration must be parsed before anything references --fill.

4. Register on-hover. A hidden div carrying the class, ideally in a style guide, or a cleanup deletes it — see A style cleanup deletes the classes your interactions toggle.

5. The interaction. Add data-animate-link to the link. Hover interaction, mouse-enter + mouse-leave, one set action that toggles the on-hover class:

ControlValue
TargetAttribute data-animate-link-border
FilterDirect parent of
ReferenceTrigger element

Why the filter is not optional

Without it, the action targets every element carrying data-animate-link-border and every link’s border fills at once. The filter resolves the target relative to the trigger, so only the hovered link’s own wrapper is affected.

This is what makes the build reusable: duplicate the link and it animates independently, with no interaction edits and regardless of class names. See Target filters — resolving a target relative to the trigger.

Why GSAP only toggles a class

The alternative — animating the gradient with an animate action — is not available: the interactions panel does not animate custom properties of this kind. The class-toggle plus CSS transition pattern is the standard bridge whenever the property you want is outside what the panel can tween, and it recurs across the corpus. See The interactions model — trigger, target, action, timeline.

It also means the motion is owned by one line of CSS you can retune without opening the interaction.

Verify

Publish, or enable custom code in preview@property and the gradient will not run in plain preview, so a “broken” build in the Designer proves nothing. See It works in the Designer but not live — or the reverse.

Then hover two different links in turn. If both rings fill, the filter is missing or set to the wrong reference. Hover and leave quickly: the fill should reverse from wherever it got to, because the CSS transition — not a timeline — is driving it.

Sources