OpenSourceUIOpenSourceUIOpensource UI
GitHubTwitter/X
  1. Home
  2. Components
  3. Buttons
  4. Follow Button
Back to ButtonsComponents / Buttons

Follow Button

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
"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 duration-300 delay-200",
)}
>
<UserPlus size={15} strokeWidth={2} aria-hidden />
{label}
</span>
 
<span
aria-hidden={!following}
className={cn(
LABEL_LAYER,
following ? "opacity-100 duration-300 delay-200" : "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

Sponsor spot · demo preview

Logo

Your brand name

Your tagline or category

A short pitch about your product or service goes here. This is a preview of how sponsor cards will look in this sidebar.

Your call to action

This slot is available. Sponsor Opensource UI and reach developers browsing components every day.