OpenSourceUIOpenSourceUIOpensource UI
GitHub—
  1. Home
  2. Components
  3. Buttons
  4. Follow Toggle
Back to ButtonsComponents / Buttons

Follow Toggle

Free Buttons React component — FollowButton. MIT licensed, copy-paste ready for Next.js and Tailwind CSS.

Social follow toggle — dark Follow pill becomes a quiet Following state with a user-check icon. Tap again to unfollow.

Preview

Setup

How to use

Free for personal and commercial use. No UI attribution required. Include the MIT copyright notice when copying component source into your project. Read the MIT license.

  1. 1

    Run in your terminal:

    $npm install clsx tailwind-merge lucide-react
  2. 2

    Copy lib/cn.ts below. Skip if you already have cn().

  3. 3

    Copy the code below and create components/buttons/follow-button.tsx in your project.

  4. 4

    Import and render:

    Example

    import { FollowButton } from "@/components/buttons/follow-button";

    <FollowButton label="Follow" followingLabel="Following" />

lib/cn.ts

1
2
3
4
5
6
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
 
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

components/buttons/follow-button.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
"use client";
 
import { forwardRef, useState, type ComponentPropsWithoutRef } from "react";
 
import { cn } from "@/lib/cn";
import { UserCheck, UserPlus } from "lucide-react";
 
export type FollowButtonProps = Readonly<
{
label?: string;
followingLabel?: string;
defaultFollowing?: boolean;
} & ComponentPropsWithoutRef<"button">
>;
 
// Both labels stack in the same grid cell so the button width never jumps.
// The outgoing label fades out first; the incoming one fades in after a
// short delay, so the swap reads as fade-out, then fade-in.
const LABEL_LAYER =
"col-start-1 row-start-1 flex items-center justify-center gap-2 transition-opacity ease-out motion-reduce:transition-none";
 
// Follow toggle — raised dark key becomes a quiet light "Following" key.
export const FollowButton = forwardRef<HTMLButtonElement, FollowButtonProps>(
(
{
className,
label = "Follow",
followingLabel = "Following",
defaultFollowing = false,
onClick,
...props
},
ref,
) => {
const [following, setFollowing] = useState(defaultFollowing);
 
return (
<button
ref={ref}
type="button"
aria-pressed={following}
data-slot="follow-button"
onClick={(event) => {
setFollowing((prev) => !prev);
onClick?.(event);
}}
className={cn(
"grid h-10 cursor-pointer place-items-stretch rounded-full px-4 font-sans text-sm font-semibold outline-none select-none",
"transition-[background-color,box-shadow,color] duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none",
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-900",
following
? cn(
// Light raised keycap — soft blurred highlight, not a hard rim.
"bg-neutral-50 text-neutral-600",
"shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_1px_rgba(0,0,0,0.1),0_2px_4px_rgba(0,0,0,0.08),inset_0_1px_2px_rgba(255,255,255,0.4),inset_0_-2px_4px_rgba(0,0,0,0.08)]",
"active:bg-neutral-100 active:shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_1px_rgba(0,0,0,0.06),inset_0_1px_2px_rgba(0,0,0,0.08),inset_0_2px_4px_rgba(0,0,0,0.04),inset_0_-1px_2px_rgba(0,0,0,0.05)]",
)
: cn(
// Dark 3D key: soft top sheen + deep lift + bottom recess.
"bg-neutral-900 text-white hover:bg-neutral-800",
"shadow-[0_1px_1px_rgba(0,0,0,0.35),0_3px_6px_rgba(0,0,0,0.28),0_8px_16px_rgba(0,0,0,0.22),inset_0_1px_2px_rgba(255,255,255,0.14),inset_0_-3px_6px_rgba(0,0,0,0.55)]",
"active:bg-neutral-950 active:shadow-[0_1px_2px_rgba(0,0,0,0.25),inset_0_2px_6px_rgba(0,0,0,0.55),inset_0_-1px_1px_rgba(255,255,255,0.06)]",
),
className,
)}
{...props}
>
<span
aria-hidden={following}
className={cn(
LABEL_LAYER,
following
? "opacity-0 duration-200"
: "opacity-100 delay-200 duration-300",
)}
>
<UserPlus size={15} strokeWidth={2} aria-hidden />
{label}
</span>
 
<span
aria-hidden={!following}
className={cn(
LABEL_LAYER,
following
? "opacity-100 delay-200 duration-300"
: "opacity-0 duration-200",
)}
>
<UserCheck size={15} strokeWidth={2} aria-hidden />
{followingLabel}
</span>
</button>
);
},
);
 
FollowButton.displayName = "FollowButton";
PreviousNext

On this

page

Jump to a section on this page.

  • lib/cn.ts
  • components/buttons/follow-button.tsx
Platinum slotAvailable

Feature your product here.

Sticky card next to every component — highest-intent placement.

Claim this card