Buttons✨ Premium

Border Draw

On hover, four 2 px strokes draw an indigo frame around the button: top and bottom in 300 ms, then both sides in another 300 ms. Two pseudo-elements, two spans, not a line of JavaScript.

CSSHoverBorder

Updated

This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Buttons has 27 effects, including 2 free. Explore the category →

3 usage examples

Subscribe — newsletter block in a footer — Border Draw example 1

① Subscribe — newsletter block in a footer

WhenDark footer: a heading, an email field and the submit button, in one column.
WhyThe transparent button doesn't compete with the field; the frame drawn on hover confirms the target without adding a solid color to an already dense area. The use case was already listed on the previous page.
SettingsColor #818cf8 (6.6:1 on dark), border-radius: 0 for a closed frame, .25s per phase.
Watch the demo — secondary action in a hero — Border Draw example 2

② Watch the demo — secondary action in a hero

WhenProduct hero with two buttons: a filled one to get started, a transparent one for the demo.
WhyThe primary / secondary hierarchy holds on its own: the filled button catches the first glance, the outline button only gains presence on hover. The open-cornered frame echoes the rounded shape of the filled neighbor.
Settings1 px strokes (height: 1px / width: 1px), currentColor to inherit the text color, 400 ms per phase.
Try 14 days — non-highlighted pricing card — Border Draw example 3

③ Try 14 days — non-highlighted pricing card

WhenPricing grid: the recommended plan has a filled button, the others an outline button.
WhyThe secondary plan keeps a visible call to action without stealing the show; on hover the frame draws across the full width of the card and gives the plan a border — the gesture says “this plan is a choice too”.
Settingswidth: 100%, border-radius: 8px, side delay lowered to .2s with .2s phases (400 ms total).

How it works

The .btn-border-draw button is transparent (background: 0 0), with indigo #6366f1 text and a border: 2px solid transparent that reserves 2 px all around. The four sides of the frame are four separate boxes: ::before (top, top: 0; left: 0) and ::after (bottom, bottom: 0; right: 0) are 2 px tall and 0 wide; the two <span>s in the HTML, .border-left (left: 0; bottom: 0) and .border-right (right: 0; top: 0), are 2 px wide and 0 tall. A button only has two pseudo-elements: the spans supply the two missing boxes.

On :hover, the pseudo-elements go to width: 100% and the spans to height: 100%. The sequencing comes from the transitions: transition: width .3s on the horizontal strokes, transition: height .3s .3s on the vertical ones — same duration, but a 300 ms delay equal to the first phase. The top draws left to right and the bottom right to left (0 → 300 ms), then the right side top to bottom and the left side bottom to top (300 → 600 ms): the frame is drawn clockwise, two opposite sides at a time. Default ease curve.

Measured in Chromium on the shipped button (164 × 54 px): the horizontal strokes reach 160 px and the vertical ones 50 px — the padding box. All four boxes are positioned against it, so they sit 2 px inside the outer edge, within the transparent border. And since .demo-btn sets border-radius: 12px and overflow: hidden, the rounded clip (inner radius 10 px) trims the ends of the straight strokes: they never meet, all four corners stay open by about 6 px. That is how the shipped version renders; with border-radius: 0 the frame closes.

When the cursor leaves, the same transitions replay in reverse with their delays: the horizontal strokes retract at once, the vertical ones 300 ms later — the frame erases in the same order it was drawn, not backwards. Nothing else changes: no background, no shadow, no text color, no :active. The motion runs on width and height, two layout properties recalculated every frame — harmless for four 2 px boxes, but not GPU-composited the way transform: scaleX() would be.

Accessibility

  • Insufficient contrast at rest: the #6366f1 text on the demo background #0a0a0f measures 4.42:1, below the 4.5:1 AA threshold for 16 px — and 4.47:1 on white, just as short. The button background is transparent: your page decides. On a dark background use #818cf8 (6.6:1); on a light one, #4f46e5 (6.3:1). The strokes reuse the same color: above 3:1 in both cases, the frame stays perceivable (WCAG 1.4.11).
  • prefers-reduced-motion not handled: add @media (prefers-reduced-motion: reduce) { .btn-border-draw::before, .btn-border-draw::after, .btn-border-draw .border-left, .btn-border-draw .border-right { transition: none } } — the frame appears in one go on hover, the information is kept.
  • Keyboard: no :hover when tabbing. Duplicate the two hover rules onto :focus-visible (.btn-border-draw:focus-visible::before, .btn-border-draw:focus-visible::after { width: 100% } and the height equivalent for the spans): the frame being drawn becomes a focus indicator, on top of the native outline the code doesn't remove.
  • Screen readers: the two <span>s only contain line breaks, no text — the accessible name stays the label. To be safe, add aria-hidden="true" to each so no assistive technology stops on empty boxes.
  • Touch: no hover on mobile; the frame draws on tap and stays (iOS) — use @media (hover: hover) to avoid it. A transparent button at rest reads like a link: keep it for secondary actions. Target 54 px tall (16 px padding, 18 px text, 2 × 2 px border), above the 44 px minimum, and no layout shift on hover thanks to the transparent border already reserved.

Browser compatibility

Requires CSS transitions on width/height, the ::before/::after pseudo-elements and absolute positioning — every browser since 2012. No JavaScript, zero dependencies.

Chrome 26+✓ Full
Firefox 16+✓ Full
Safari 9+✓ Full
Edge 12+✓ Full
Mobile iOS✓ Full (frame drawn on tap, stays)
Android Chrome✓ Full (on tap)

Without transitions, the frame appears and disappears at once on hover; without pseudo-elements (prehistoric browsers), only the two sides carried by the spans are drawn.

The code

HTML structure to paste into your page (the CSS + JS come with Premium):

index.html — structure
<button class="demo-btn btn-border-draw">
  Border Draw
  <span class="border-left"></span>
  <span class="border-right"></span>
</button>
Unlock the full code — $9.90/mo incl. tax

The full HTML + CSS + JS of Border Draw, ready to paste — plus all 840 effects. Or $299 once, lifetime access.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Color #6366f1 (CSS — color + background of the 4 strokes) #6366f1 Five occurrences to change together. Set background: currentColor on the four strokes: the text color drives everything, and one declaration handles branding or contrast (#818cf8 on dark, #4f46e5 on light).
transition: width .3s (CSS — ::before, ::after) .3s Duration of the first phase (top and bottom). If you change it, align the sides' delay with it, otherwise the phases overlap or leave a gap.
transition: height .3s .3s (CSS — .border-left, .border-right) .3s duration, .3s delay The delay is the sequencer. 0s: all four sides draw at once (300 ms total). .15s: the sides start halfway through the horizontal strokes, smoother.
Thickness height: 2px / width: 2px (CSS — the 4 strokes) 2 px 1 px: hairline; 3 to 4 px: bold frame. Set the button's transparent border to the same value so the stroke keeps the same distance from the edge.
border-radius (CSS — .demo-btn) 12px Decides the corners. 0: the strokes meet, closed frame. Above 12 px the open corners widen — a style in itself (a “cut-corner” frame).
left/right and top/bottom anchors (CSS — position of each stroke) top: left 0 · bottom: right 0 · left: bottom 0 · right: top 0 The anchor sets the drawing direction. Put right: 0 on ::before and left: 0 on ::after, top: 0 on .border-left and bottom: 0 on .border-right: the frame draws counter-clockwise.
border: 2px solid transparent (CSS — .btn-border-draw) transparent Replace transparent with rgba(99, 102, 241, .25): a ghost frame is visible at rest, which the strokes then underline on hover — easier to read as a button for a transparent element.

FAQ

Because the four strokes are straight and overflow: hidden clips them along the button's border-radius: 12px (10 px inside the border). At the corners the arc moves more than 2 px away from the edges: the stroke is trimmed at an angle over about 6 px and never meets its neighbor. Three ways out: border-radius: 0 for a closed frame, own the open corners as a signature, or switch to an <svg> with a <rect rx="12"> animated through stroke-dashoffset if you want a stroke that truly follows the curve.
Yes, with the two-pseudo-element “L” technique: ::before carries border-top and border-right, ::after carries border-bottom and border-left, each going from width: 0; height: 0 to 100%. Two boxes are then enough, at the cost of drawing two symmetric Ls rather than four sequenced sides. The shipped code keeps the spans to control each side separately.
Give each side its own window: ::before with transition: width .15s, .border-right with height .15s .15s, ::after with width .15s .3s, .border-left with height .15s .45s. The stroke chains top → right → bottom → left in 600 ms, like a pen that never lifts — the shipped anchors already fit: the bottom starts from the right (right: 0) and the left side starts from the bottom (bottom: 0).