Arrow Capsule Button
A clear call to action with a circular arrow that slides into its next direction.
'use client';
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react';
import './arrow-capsule-button.css';
export interface ArrowCapsuleButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** A decorative icon. Defaults to a diagonal arrow. */
icon?: ReactNode;
}
function DiagonalArrow() {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" focusable="false">
<path
d="M6 18 18 6M6 6h12v12"
stroke="currentColor"
strokeWidth="1.7"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function makeDecorative(node: HTMLSpanElement | null) {
node?.setAttribute('inert', '');
}
/** A capsule action with a clipped, repeating arrow inside its circular end cap. */
export const ArrowCapsuleButton = forwardRef<HTMLButtonElement, ArrowCapsuleButtonProps>(
function ArrowCapsuleButton(
{ children, icon, className = '', type = 'button', disabled = false, ...props },
ref,
) {
const arrow = icon ?? <DiagonalArrow />;
return (
<button
{...props}
ref={ref}
type={type}
disabled={disabled}
className={`ac-arrow-capsule ${className}`.trim()}
>
<span className="ac-arrow-capsule__label">{children}</span>
<span
ref={makeDecorative}
className="ac-arrow-capsule__circle"
aria-hidden="true"
>
<span className="ac-arrow-capsule__icon ac-arrow-capsule__icon--first">
{arrow}
</span>
<span className="ac-arrow-capsule__icon ac-arrow-capsule__icon--next">
{arrow}
</span>
</span>
</button>
);
},
);
export default ArrowCapsuleButton;
Installation
Run this in your shadcn-configured React project. The component and stylesheet are added together.
npx shadcn@latest add http://localhost:5173/r/arrow-capsule-button.jsonThis command uses the current site address. For localhost, keep this preview running on the same computer. Files follow your configured components alias.
Usage
Import the component and make the details your own.
'use client';
import { useState } from 'react';
import { ArrowCapsuleButton } from '@/components/anticraft/ArrowCapsuleButton';
export function Example() {
const [count, setCount] = useState(0);
return (
<div>
<ArrowCapsuleButton onClick={() => setCount(value => value + 1)}>
Let’s work together
</ArrowCapsuleButton>
<p role="status">{count ? `Demo action activated ${count} times.` : 'Try the button.'}</p>
</div>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | Required | A meaningful button label. Keep children non-interactive; decorative icons should be hidden from assistive technology. |
icon | ReactNode | Diagonal arrow | An optional decorative replacement icon, repeated inside the circle. It is hidden from assistive technology and made inert; keep it non-interactive. |
type | 'button' | 'submit' | 'reset' | 'button' | The native button type. Set submit explicitly when the button should submit its associated form. |
disabled | boolean | false | Disables native activation and decorative hover/pressed effects. The live demo includes a checkbox to inspect this state. |
onClick | MouseEventHandler<HTMLButtonElement> | undefined | Your action handler. Native keyboard and touch activation use the same handler. Buttons have no built-in navigation, payment, or form-submission action. |
className / style | string / CSSProperties | '' / undefined | Additional classes and native inline styles. Theme with --ac-arrow-bg, --ac-arrow-color, --ac-arrow-circle-bg, --ac-arrow-circle-color, --ac-arrow-hover-bg, and --ac-arrow-focus. |
Native button props / ref | ButtonHTMLAttributes<HTMLButtonElement> / Ref<HTMLButtonElement> | undefined | Native form, name, value, aria-*, data-*, event, and other button attributes pass through. A forwarded ref points to the actual button. Native disabled behavior is preserved. |
Motion & accessibility
ArrowCapsuleButton accepts an optional decorative icon. The default arrow repeats inside a clipped circle on fine-pointer hover or visible keyboard focus. Its hit area stays still. Motion is finite and removed for prefers-reduced-motion. Every variant keeps a visible focus outline, a native button hit area of at least 44px, and a disabled state. In the demo, activation increments a clearly labeled demo counter; connect your own action in your app.
Use semantic content and meaningful labels in your composition. Check contrast and keyboard behavior with your own styles and content.