Skip to content

Sweep Button

A label box and an arrow box that merge while a fill sweeps across the label.

$npx shadcn@latest add https://framebrick.co/r/sweep-button.json

From the Ciketo template, see it in context.

Installation

$ npx shadcn@latest add https://framebrick.co/r/sweep-button.json

Usage

import { SweepButton } from "@/components/ui/sweep-button";

<SweepButton href="/contact">Book a viewing</SweepButton>
<SweepButton href="/work" arrow={false}>Our work</SweepButton>
<SweepButton type="submit" size="lg">Start a project</SweepButton>

Props

PropTypeDefaultDescription
size"sm" | "default" | "lg""default"Height 32 / 36 / 44 px; the arrow box is a square of the same size.
arrowbooleantrueThe arrow box beside the label. Without it the button is the label box alone, with the sweep.
hrefstring—Render a Next.js link instead of a button. http(s) links open in a new tab.
…propsbutton / anchor props—Passed to the element (onClick, disabled, aria-*).
  • Plain CSS: no animation library and no JavaScript, so it works as it is in a server component and without scripts.
  • Framer's spring (bounce 0, 0.4 s) is approximated by cubic-bezier(0.22, 1, 0.36, 1) over 400 ms, the curve Ciketo's own Next.js edition uses for its width changes.
  • The light label is a second copy clipped by the same edge as the fill, so the text changes colour exactly where the sweep passes. Ciketo fades the colour, which leaves light text on a light box half way. The copy is aria-hidden, so the label is read once.
  • The sweep also runs on keyboard focus. With reduced motion the state simply changes, with no sweep. On a touch screen there is no hover, so only the resting look shows.
  • The label is rendered twice, so pass text or inline content, not a component with state of its own.

AI prompt

Paste into v0, Lovable, Bolt or Claude to rebuild this component in your own project.

Create a React + Tailwind CSS component called SweepButton using Next.js Link — plain CSS, no animation library.
- Two boxes in an inline-flex row, items-stretch, 2px apart (ml-0.5 on the second): a label box and a square arrow box
  of the same height (sizes: h-8 px-4 text-xs, h-9 px-5 text-sm, h-11 px-7 text-base). Both bg-secondary, rounded-md,
  text-secondary-foreground; the label is font-medium. The root has the class "group".
- Inside the label box (relative, overflow-hidden) put a second copy of the label in an absolutely positioned layer
  (inset-0, flex, centred) with bg-primary text-primary-foreground and aria-hidden. At rest it has clip-path
  inset(0 100% 0 0); on group-hover and group-focus-visible it becomes inset(0), transitioning clip-path over 400ms with
  cubic-bezier(0.22,1,0.36,1). The fill and the light label are the same layer, so they share one edge.
- On group-hover and group-focus-visible: the label box loses its right corners (rounded-r-none), the arrow box loses
  its margin (ml-0) and its left corners (rounded-l-none), so the two become one block — transitioning border-radius and
  margin with the same duration and curve; and the arrow (an up-right arrow, 16px) rotates 45°.
- motion-reduce:transition-none on everything that transitions.
- An arrow={false} prop drops the arrow box. If an href is given render a Link, external http(s) links open in a new tab
  with rel noopener noreferrer; otherwise a button. Visible focus ring (ring-[3px] ring-ring/50).

Be the first to know

Get new templates and components first

One short email when we release a template or a free component. No spam — unsubscribe anytime.

We use your email only for Framebrick updates. Privacy policy