20 CSS Social Buttons12 / 20
Follow Button with Following Toggle
Follow becomes Following, and hovering Following reveals Unfollow in red — the three-state affordance real products ship. All three labels occupy the same grid cell, so the button never changes width as the state changes. A single data-state attribute drives every visual, and aria-pressed carries the same fact to assistive technology.
Published
The code
<div class="soc-12">
<div class="soc-12__stage">
<div class="soc-12__card">
<div class="soc-12__who">
<span class="soc-12__avatar" aria-hidden="true">PS</span>
<span class="soc-12__id">
<strong class="soc-12__name">Priya Sharma</strong>
<span class="soc-12__handle">@priya.builds</span>
</span>
</div>
<p class="soc-12__bio">Design engineer. Writes about CSS layout, motion and the parts of accessibility nobody tests.</p>
<div class="soc-12__row">
<button class="soc-12__btn" type="button" data-state="idle" aria-pressed="false">
<span class="soc-12__icons" aria-hidden="true">
<svg class="soc-12__i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 6v12"/><path d="M6 12h12"/></svg>
<svg class="soc-12__i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.6 4.4 4.4L19 7.4"/></svg>
<svg class="soc-12__i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round"><path d="M6 6l12 12"/><path d="M18 6 6 18"/></svg>
</span>
<span class="soc-12__labels">
<span class="soc-12__l-follow">Follow</span>
<span class="soc-12__l-following">Following</span>
<span class="soc-12__l-unfollow">Unfollow</span>
</span>
</button>
<p class="soc-12__count"><strong class="soc-12__num">18,204</strong> followers</p>
</div>
<p class="soc-12__live" role="status" aria-live="polite"></p>
</div>
</div>
</div><div class="soc-12">
<div class="soc-12__stage">
<div class="soc-12__card">
<div class="soc-12__who">
<span class="soc-12__avatar" aria-hidden="true">PS</span>
<span class="soc-12__id">
<strong class="soc-12__name">Priya Sharma</strong>
<span class="soc-12__handle">@priya.builds</span>
</span>
</div>
<p class="soc-12__bio">Design engineer. Writes about CSS layout, motion and the parts of accessibility nobody tests.</p>
<div class="soc-12__row">
<button class="soc-12__btn" type="button" data-state="idle" aria-pressed="false">
<span class="soc-12__icons" aria-hidden="true">
<svg class="soc-12__i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 6v12"/><path d="M6 12h12"/></svg>
<svg class="soc-12__i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.6 4.4 4.4L19 7.4"/></svg>
<svg class="soc-12__i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round"><path d="M6 6l12 12"/><path d="M18 6 6 18"/></svg>
</span>
<span class="soc-12__labels">
<span class="soc-12__l-follow">Follow</span>
<span class="soc-12__l-following">Following</span>
<span class="soc-12__l-unfollow">Unfollow</span>
</span>
</button>
<p class="soc-12__count"><strong class="soc-12__num">18,204</strong> followers</p>
</div>
<p class="soc-12__live" role="status" aria-live="polite"></p>
</div>
</div>
</div>@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap');
.soc-12 {
--bg: #f4f4f6;
--surface: #ffffff;
--fg: #0e0e12;
--muted: #6a6a76;
--line: #e5e5ea;
--ring: #3d63ff;
--brand: #111117;
--on-brand: #ffffff;
--danger: #c8382f;
--sans: "Geist",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--bg);
color: var(--fg);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
}
@supports (color: oklch(50% 0 0)) {
.soc-12 {
--bg: oklch(96.4% .003 285);
--surface: oklch(100% 0 0);
--fg: oklch(17% .012 285);
--muted: oklch(55% .012 285);
--line: oklch(91.5% .005 285);
--danger: oklch(54% .19 27);
}
}
@media (prefers-color-scheme: dark) {
.soc-12:not([data-theme="light"]) {
--bg: #0a0a0d;
--surface: #131318;
--fg: #f2f2f5;
--muted: #9a9aa6;
--line: #26262e;
--brand: #f2f2f5;
--on-brand: #0f0f14;
--danger: #ff6f61;
}
}
.soc-12[data-theme="dark"] {
--bg: #0a0a0d;
--surface: #131318;
--fg: #f2f2f5;
--muted: #9a9aa6;
--line: #26262e;
--brand: #f2f2f5;
--on-brand: #0f0f14;
--danger: #ff6f61;
}
.soc-12,
.soc-12 *,
.soc-12 *::before,
.soc-12 *::after {
box-sizing: border-box;
}
.soc-12__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1.25rem,5vw,3.5rem);
}
.soc-12__card {
inline-size: min(30rem,100%);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 1.35rem;
padding: clamp(1.5rem,4vw,2.25rem);
box-shadow: 0 1px 2px rgb(0 0 0/.04), 0 22px 48px -30px rgb(9 9 20/.45);
}
.soc-12__who {
display: flex;
align-items: center;
gap: .8rem;
margin-block-end: 1rem;
min-inline-size: 0;
}
.soc-12__avatar {
flex: none;
display: grid;
place-items: center;
inline-size: 3rem;
block-size: 3rem;
border-radius: 50%;
background: linear-gradient(140deg,#7c5cff,#2ec8b0);
color: #fff;
font-size: .9rem;
font-weight: 700;
letter-spacing: .02em;
}
.soc-12__id {
display: grid;
min-inline-size: 0;
}
.soc-12__name {
font-size: 1rem;
font-weight: 600;
letter-spacing: -.01em;
}
.soc-12__handle {
font-size: .85rem;
color: var(--muted);
overflow-wrap: anywhere;
}
.soc-12__bio {
margin: 0 0 1.5rem;
font-size: .9rem;
line-height: 1.6;
color: var(--muted);
text-wrap: pretty;
}
.soc-12__row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: .85rem;
}
.soc-12__btn {
display: inline-flex;
align-items: center;
gap: .5rem;
min-block-size: 2.875rem;
padding: 0 1.2rem;
border: 1px solid var(--brand);
border-radius: 999px;
background: var(--brand);
color: var(--on-brand);
font: inherit;
font-size: .92rem;
font-weight: 600;
cursor: pointer;
transition: background-color .2s ease, color .2s ease, border-color .2s ease, translate .18s cubic-bezier(.16,1,.3,1);
}
.soc-12__btn:active {
translate: 0 1px;
}
.soc-12__btn:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 3px;
}
.soc-12__icons {
display: grid;
inline-size: 1.05rem;
block-size: 1.05rem;
}
.soc-12__icons svg {
grid-area: 1/1;
inline-size: 1.05rem;
block-size: 1.05rem;
opacity: 0;
transition: opacity .16s ease;
}
.soc-12__labels {
display: grid;
}
.soc-12__labels span {
grid-area: 1/1;
opacity: 0;
white-space: nowrap;
transition: opacity .16s ease;
}
.soc-12__btn[data-state="idle"] .soc-12__l-follow,
.soc-12__btn[data-state="idle"] .soc-12__i-plus {
opacity: 1;
}
.soc-12__btn[data-state="following"] {
background: var(--surface);
color: var(--fg);
border-color: var(--line);
}
.soc-12__btn[data-state="following"] .soc-12__l-following,
.soc-12__btn[data-state="following"] .soc-12__i-check {
opacity: 1;
}
.soc-12__btn[data-state="following"]:hover,
.soc-12__btn[data-state="following"]:focus-visible {
background: color-mix(in oklab,var(--danger) 12%,var(--surface));
border-color: var(--danger);
color: var(--danger);
}
.soc-12__btn[data-state="following"]:hover .soc-12__l-following,
.soc-12__btn[data-state="following"]:focus-visible .soc-12__l-following,
.soc-12__btn[data-state="following"]:hover .soc-12__i-check,
.soc-12__btn[data-state="following"]:focus-visible .soc-12__i-check {
opacity: 0;
}
.soc-12__btn[data-state="following"]:hover .soc-12__l-unfollow,
.soc-12__btn[data-state="following"]:focus-visible .soc-12__l-unfollow,
.soc-12__btn[data-state="following"]:hover .soc-12__i-x,
.soc-12__btn[data-state="following"]:focus-visible .soc-12__i-x {
opacity: 1;
}
.soc-12__count {
margin: 0;
font-size: .88rem;
color: var(--muted);
}
.soc-12__num {
color: var(--fg);
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.soc-12__live {
margin: 1.1rem 0 0;
min-block-size: 1.1rem;
font-size: .8rem;
color: var(--muted);
}
@media (prefers-reduced-motion: reduce) {
.soc-12__btn,
.soc-12__labels span,
.soc-12__icons svg {
transition-duration: 1ms;
}
.soc-12__btn:active {
translate: none;
}
}
@media (forced-colors: active) {
.soc-12__btn {
border: 1px solid ButtonText;
background: ButtonFace;
color: ButtonText;
}
.soc-12__btn[data-state="following"]:hover {
background: Highlight;
color: HighlightText;
}
}@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap');
.soc-12 {
--bg: #f4f4f6;
--surface: #ffffff;
--fg: #0e0e12;
--muted: #6a6a76;
--line: #e5e5ea;
--ring: #3d63ff;
--brand: #111117;
--on-brand: #ffffff;
--danger: #c8382f;
--sans: "Geist",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--bg);
color: var(--fg);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
}
@supports (color: oklch(50% 0 0)) {
.soc-12 {
--bg: oklch(96.4% .003 285);
--surface: oklch(100% 0 0);
--fg: oklch(17% .012 285);
--muted: oklch(55% .012 285);
--line: oklch(91.5% .005 285);
--danger: oklch(54% .19 27);
}
}
@media (prefers-color-scheme: dark) {
.soc-12:not([data-theme="light"]) {
--bg: #0a0a0d;
--surface: #131318;
--fg: #f2f2f5;
--muted: #9a9aa6;
--line: #26262e;
--brand: #f2f2f5;
--on-brand: #0f0f14;
--danger: #ff6f61;
}
}
.soc-12[data-theme="dark"] {
--bg: #0a0a0d;
--surface: #131318;
--fg: #f2f2f5;
--muted: #9a9aa6;
--line: #26262e;
--brand: #f2f2f5;
--on-brand: #0f0f14;
--danger: #ff6f61;
}
.soc-12,
.soc-12 *,
.soc-12 *::before,
.soc-12 *::after {
box-sizing: border-box;
}
.soc-12__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1.25rem,5vw,3.5rem);
}
.soc-12__card {
inline-size: min(30rem,100%);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 1.35rem;
padding: clamp(1.5rem,4vw,2.25rem);
box-shadow: 0 1px 2px rgb(0 0 0/.04), 0 22px 48px -30px rgb(9 9 20/.45);
}
.soc-12__who {
display: flex;
align-items: center;
gap: .8rem;
margin-block-end: 1rem;
min-inline-size: 0;
}
.soc-12__avatar {
flex: none;
display: grid;
place-items: center;
inline-size: 3rem;
block-size: 3rem;
border-radius: 50%;
background: linear-gradient(140deg,#7c5cff,#2ec8b0);
color: #fff;
font-size: .9rem;
font-weight: 700;
letter-spacing: .02em;
}
.soc-12__id {
display: grid;
min-inline-size: 0;
}
.soc-12__name {
font-size: 1rem;
font-weight: 600;
letter-spacing: -.01em;
}
.soc-12__handle {
font-size: .85rem;
color: var(--muted);
overflow-wrap: anywhere;
}
.soc-12__bio {
margin: 0 0 1.5rem;
font-size: .9rem;
line-height: 1.6;
color: var(--muted);
text-wrap: pretty;
}
.soc-12__row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: .85rem;
}
.soc-12__btn {
display: inline-flex;
align-items: center;
gap: .5rem;
min-block-size: 2.875rem;
padding: 0 1.2rem;
border: 1px solid var(--brand);
border-radius: 999px;
background: var(--brand);
color: var(--on-brand);
font: inherit;
font-size: .92rem;
font-weight: 600;
cursor: pointer;
transition: background-color .2s ease, color .2s ease, border-color .2s ease, translate .18s cubic-bezier(.16,1,.3,1);
}
.soc-12__btn:active {
translate: 0 1px;
}
.soc-12__btn:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 3px;
}
.soc-12__icons {
display: grid;
inline-size: 1.05rem;
block-size: 1.05rem;
}
.soc-12__icons svg {
grid-area: 1/1;
inline-size: 1.05rem;
block-size: 1.05rem;
opacity: 0;
transition: opacity .16s ease;
}
.soc-12__labels {
display: grid;
}
.soc-12__labels span {
grid-area: 1/1;
opacity: 0;
white-space: nowrap;
transition: opacity .16s ease;
}
.soc-12__btn[data-state="idle"] .soc-12__l-follow,
.soc-12__btn[data-state="idle"] .soc-12__i-plus {
opacity: 1;
}
.soc-12__btn[data-state="following"] {
background: var(--surface);
color: var(--fg);
border-color: var(--line);
}
.soc-12__btn[data-state="following"] .soc-12__l-following,
.soc-12__btn[data-state="following"] .soc-12__i-check {
opacity: 1;
}
.soc-12__btn[data-state="following"]:hover,
.soc-12__btn[data-state="following"]:focus-visible {
background: color-mix(in oklab,var(--danger) 12%,var(--surface));
border-color: var(--danger);
color: var(--danger);
}
.soc-12__btn[data-state="following"]:hover .soc-12__l-following,
.soc-12__btn[data-state="following"]:focus-visible .soc-12__l-following,
.soc-12__btn[data-state="following"]:hover .soc-12__i-check,
.soc-12__btn[data-state="following"]:focus-visible .soc-12__i-check {
opacity: 0;
}
.soc-12__btn[data-state="following"]:hover .soc-12__l-unfollow,
.soc-12__btn[data-state="following"]:focus-visible .soc-12__l-unfollow,
.soc-12__btn[data-state="following"]:hover .soc-12__i-x,
.soc-12__btn[data-state="following"]:focus-visible .soc-12__i-x {
opacity: 1;
}
.soc-12__count {
margin: 0;
font-size: .88rem;
color: var(--muted);
}
.soc-12__num {
color: var(--fg);
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.soc-12__live {
margin: 1.1rem 0 0;
min-block-size: 1.1rem;
font-size: .8rem;
color: var(--muted);
}
@media (prefers-reduced-motion: reduce) {
.soc-12__btn,
.soc-12__labels span,
.soc-12__icons svg {
transition-duration: 1ms;
}
.soc-12__btn:active {
translate: none;
}
}
@media (forced-colors: active) {
.soc-12__btn {
border: 1px solid ButtonText;
background: ButtonFace;
color: ButtonText;
}
.soc-12__btn[data-state="following"]:hover {
background: Highlight;
color: HighlightText;
}
}(() => {
const root = document.querySelector('.soc-12');
if (!root) return;
const btn = root.querySelector('.soc-12__btn');
const num = root.querySelector('.soc-12__num');
const live = root.querySelector('.soc-12__live');
let count = 18204;
btn.addEventListener('click', () => {
const following = btn.dataset.state === 'following';
btn.dataset.state = following ? 'idle' : 'following';
btn.setAttribute('aria-pressed', String(!following));
count += following ? -1 : 1;
num.textContent = count.toLocaleString('en-US');
live.textContent = following ? 'Unfollowed Priya Sharma.' : 'Following Priya Sharma.';
});
})();(() => {
const root = document.querySelector('.soc-12');
if (!root) return;
const btn = root.querySelector('.soc-12__btn');
const num = root.querySelector('.soc-12__num');
const live = root.querySelector('.soc-12__live');
let count = 18204;
btn.addEventListener('click', () => {
const following = btn.dataset.state === 'following';
btn.dataset.state = following ? 'idle' : 'following';
btn.setAttribute('aria-pressed', String(!following));
count += following ? -1 : 1;
num.textContent = count.toLocaleString('en-US');
live.textContent = following ? 'Unfollowed Priya Sharma.' : 'Following Priya Sharma.';
});
})();Here's a working CSS Social Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Follow Button with Following Toggle
Source: https://codefronts.com/snippets/css-social-buttons/follow-button-with-following-toggle/
Follow becomes Following, and hovering Following reveals Unfollow in red — the three-state affordance real products ship. All three labels occupy the same grid cell, so the button never changes width as the state changes. A single data-state attribute drives every visual, and aria-pressed carries the same fact to assistive technology.
## HTML
```html
<div class="soc-12">
<div class="soc-12__stage">
<div class="soc-12__card">
<div class="soc-12__who">
<span class="soc-12__avatar" aria-hidden="true">PS</span>
<span class="soc-12__id">
<strong class="soc-12__name">Priya Sharma</strong>
<span class="soc-12__handle">@priya.builds</span>
</span>
</div>
<p class="soc-12__bio">Design engineer. Writes about CSS layout, motion and the parts of accessibility nobody tests.</p>
<div class="soc-12__row">
<button class="soc-12__btn" type="button" data-state="idle" aria-pressed="false">
<span class="soc-12__icons" aria-hidden="true">
<svg class="soc-12__i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 6v12"/><path d="M6 12h12"/></svg>
<svg class="soc-12__i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.6 4.4 4.4L19 7.4"/></svg>
<svg class="soc-12__i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round"><path d="M6 6l12 12"/><path d="M18 6 6 18"/></svg>
</span>
<span class="soc-12__labels">
<span class="soc-12__l-follow">Follow</span>
<span class="soc-12__l-following">Following</span>
<span class="soc-12__l-unfollow">Unfollow</span>
</span>
</button>
<p class="soc-12__count"><strong class="soc-12__num">18,204</strong> followers</p>
</div>
<p class="soc-12__live" role="status" aria-live="polite"></p>
</div>
</div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap');
.soc-12 {
--bg: #f4f4f6;
--surface: #ffffff;
--fg: #0e0e12;
--muted: #6a6a76;
--line: #e5e5ea;
--ring: #3d63ff;
--brand: #111117;
--on-brand: #ffffff;
--danger: #c8382f;
--sans: "Geist",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--bg);
color: var(--fg);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
}
@supports (color: oklch(50% 0 0)) {
.soc-12 {
--bg: oklch(96.4% .003 285);
--surface: oklch(100% 0 0);
--fg: oklch(17% .012 285);
--muted: oklch(55% .012 285);
--line: oklch(91.5% .005 285);
--danger: oklch(54% .19 27);
}
}
@media (prefers-color-scheme: dark) {
.soc-12:not([data-theme="light"]) {
--bg: #0a0a0d;
--surface: #131318;
--fg: #f2f2f5;
--muted: #9a9aa6;
--line: #26262e;
--brand: #f2f2f5;
--on-brand: #0f0f14;
--danger: #ff6f61;
}
}
.soc-12[data-theme="dark"] {
--bg: #0a0a0d;
--surface: #131318;
--fg: #f2f2f5;
--muted: #9a9aa6;
--line: #26262e;
--brand: #f2f2f5;
--on-brand: #0f0f14;
--danger: #ff6f61;
}
.soc-12,
.soc-12 *,
.soc-12 *::before,
.soc-12 *::after {
box-sizing: border-box;
}
.soc-12__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1.25rem,5vw,3.5rem);
}
.soc-12__card {
inline-size: min(30rem,100%);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 1.35rem;
padding: clamp(1.5rem,4vw,2.25rem);
box-shadow: 0 1px 2px rgb(0 0 0/.04), 0 22px 48px -30px rgb(9 9 20/.45);
}
.soc-12__who {
display: flex;
align-items: center;
gap: .8rem;
margin-block-end: 1rem;
min-inline-size: 0;
}
.soc-12__avatar {
flex: none;
display: grid;
place-items: center;
inline-size: 3rem;
block-size: 3rem;
border-radius: 50%;
background: linear-gradient(140deg,#7c5cff,#2ec8b0);
color: #fff;
font-size: .9rem;
font-weight: 700;
letter-spacing: .02em;
}
.soc-12__id {
display: grid;
min-inline-size: 0;
}
.soc-12__name {
font-size: 1rem;
font-weight: 600;
letter-spacing: -.01em;
}
.soc-12__handle {
font-size: .85rem;
color: var(--muted);
overflow-wrap: anywhere;
}
.soc-12__bio {
margin: 0 0 1.5rem;
font-size: .9rem;
line-height: 1.6;
color: var(--muted);
text-wrap: pretty;
}
.soc-12__row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: .85rem;
}
.soc-12__btn {
display: inline-flex;
align-items: center;
gap: .5rem;
min-block-size: 2.875rem;
padding: 0 1.2rem;
border: 1px solid var(--brand);
border-radius: 999px;
background: var(--brand);
color: var(--on-brand);
font: inherit;
font-size: .92rem;
font-weight: 600;
cursor: pointer;
transition: background-color .2s ease, color .2s ease, border-color .2s ease, translate .18s cubic-bezier(.16,1,.3,1);
}
.soc-12__btn:active {
translate: 0 1px;
}
.soc-12__btn:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 3px;
}
.soc-12__icons {
display: grid;
inline-size: 1.05rem;
block-size: 1.05rem;
}
.soc-12__icons svg {
grid-area: 1/1;
inline-size: 1.05rem;
block-size: 1.05rem;
opacity: 0;
transition: opacity .16s ease;
}
.soc-12__labels {
display: grid;
}
.soc-12__labels span {
grid-area: 1/1;
opacity: 0;
white-space: nowrap;
transition: opacity .16s ease;
}
.soc-12__btn[data-state="idle"] .soc-12__l-follow,
.soc-12__btn[data-state="idle"] .soc-12__i-plus {
opacity: 1;
}
.soc-12__btn[data-state="following"] {
background: var(--surface);
color: var(--fg);
border-color: var(--line);
}
.soc-12__btn[data-state="following"] .soc-12__l-following,
.soc-12__btn[data-state="following"] .soc-12__i-check {
opacity: 1;
}
.soc-12__btn[data-state="following"]:hover,
.soc-12__btn[data-state="following"]:focus-visible {
background: color-mix(in oklab,var(--danger) 12%,var(--surface));
border-color: var(--danger);
color: var(--danger);
}
.soc-12__btn[data-state="following"]:hover .soc-12__l-following,
.soc-12__btn[data-state="following"]:focus-visible .soc-12__l-following,
.soc-12__btn[data-state="following"]:hover .soc-12__i-check,
.soc-12__btn[data-state="following"]:focus-visible .soc-12__i-check {
opacity: 0;
}
.soc-12__btn[data-state="following"]:hover .soc-12__l-unfollow,
.soc-12__btn[data-state="following"]:focus-visible .soc-12__l-unfollow,
.soc-12__btn[data-state="following"]:hover .soc-12__i-x,
.soc-12__btn[data-state="following"]:focus-visible .soc-12__i-x {
opacity: 1;
}
.soc-12__count {
margin: 0;
font-size: .88rem;
color: var(--muted);
}
.soc-12__num {
color: var(--fg);
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.soc-12__live {
margin: 1.1rem 0 0;
min-block-size: 1.1rem;
font-size: .8rem;
color: var(--muted);
}
@media (prefers-reduced-motion: reduce) {
.soc-12__btn,
.soc-12__labels span,
.soc-12__icons svg {
transition-duration: 1ms;
}
.soc-12__btn:active {
translate: none;
}
}
@media (forced-colors: active) {
.soc-12__btn {
border: 1px solid ButtonText;
background: ButtonFace;
color: ButtonText;
}
.soc-12__btn[data-state="following"]:hover {
background: Highlight;
color: HighlightText;
}
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.soc-12');
if (!root) return;
const btn = root.querySelector('.soc-12__btn');
const num = root.querySelector('.soc-12__num');
const live = root.querySelector('.soc-12__live');
let count = 18204;
btn.addEventListener('click', () => {
const following = btn.dataset.state === 'following';
btn.dataset.state = following ? 'idle' : 'following';
btn.setAttribute('aria-pressed', String(!following));
count += following ? -1 : 1;
num.textContent = count.toLocaleString('en-US');
live.textContent = following ? 'Unfollowed Priya Sharma.' : 'Following Priya Sharma.';
});
})();
```Here's a working CSS Social Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Follow Button with Following Toggle
Source: https://codefronts.com/snippets/css-social-buttons/follow-button-with-following-toggle/
Follow becomes Following, and hovering Following reveals Unfollow in red — the three-state affordance real products ship. All three labels occupy the same grid cell, so the button never changes width as the state changes. A single data-state attribute drives every visual, and aria-pressed carries the same fact to assistive technology.
## HTML
```html
<div class="soc-12">
<div class="soc-12__stage">
<div class="soc-12__card">
<div class="soc-12__who">
<span class="soc-12__avatar" aria-hidden="true">PS</span>
<span class="soc-12__id">
<strong class="soc-12__name">Priya Sharma</strong>
<span class="soc-12__handle">@priya.builds</span>
</span>
</div>
<p class="soc-12__bio">Design engineer. Writes about CSS layout, motion and the parts of accessibility nobody tests.</p>
<div class="soc-12__row">
<button class="soc-12__btn" type="button" data-state="idle" aria-pressed="false">
<span class="soc-12__icons" aria-hidden="true">
<svg class="soc-12__i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 6v12"/><path d="M6 12h12"/></svg>
<svg class="soc-12__i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.6 4.4 4.4L19 7.4"/></svg>
<svg class="soc-12__i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round"><path d="M6 6l12 12"/><path d="M18 6 6 18"/></svg>
</span>
<span class="soc-12__labels">
<span class="soc-12__l-follow">Follow</span>
<span class="soc-12__l-following">Following</span>
<span class="soc-12__l-unfollow">Unfollow</span>
</span>
</button>
<p class="soc-12__count"><strong class="soc-12__num">18,204</strong> followers</p>
</div>
<p class="soc-12__live" role="status" aria-live="polite"></p>
</div>
</div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap');
.soc-12 {
--bg: #f4f4f6;
--surface: #ffffff;
--fg: #0e0e12;
--muted: #6a6a76;
--line: #e5e5ea;
--ring: #3d63ff;
--brand: #111117;
--on-brand: #ffffff;
--danger: #c8382f;
--sans: "Geist",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--bg);
color: var(--fg);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
}
@supports (color: oklch(50% 0 0)) {
.soc-12 {
--bg: oklch(96.4% .003 285);
--surface: oklch(100% 0 0);
--fg: oklch(17% .012 285);
--muted: oklch(55% .012 285);
--line: oklch(91.5% .005 285);
--danger: oklch(54% .19 27);
}
}
@media (prefers-color-scheme: dark) {
.soc-12:not([data-theme="light"]) {
--bg: #0a0a0d;
--surface: #131318;
--fg: #f2f2f5;
--muted: #9a9aa6;
--line: #26262e;
--brand: #f2f2f5;
--on-brand: #0f0f14;
--danger: #ff6f61;
}
}
.soc-12[data-theme="dark"] {
--bg: #0a0a0d;
--surface: #131318;
--fg: #f2f2f5;
--muted: #9a9aa6;
--line: #26262e;
--brand: #f2f2f5;
--on-brand: #0f0f14;
--danger: #ff6f61;
}
.soc-12,
.soc-12 *,
.soc-12 *::before,
.soc-12 *::after {
box-sizing: border-box;
}
.soc-12__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1.25rem,5vw,3.5rem);
}
.soc-12__card {
inline-size: min(30rem,100%);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 1.35rem;
padding: clamp(1.5rem,4vw,2.25rem);
box-shadow: 0 1px 2px rgb(0 0 0/.04), 0 22px 48px -30px rgb(9 9 20/.45);
}
.soc-12__who {
display: flex;
align-items: center;
gap: .8rem;
margin-block-end: 1rem;
min-inline-size: 0;
}
.soc-12__avatar {
flex: none;
display: grid;
place-items: center;
inline-size: 3rem;
block-size: 3rem;
border-radius: 50%;
background: linear-gradient(140deg,#7c5cff,#2ec8b0);
color: #fff;
font-size: .9rem;
font-weight: 700;
letter-spacing: .02em;
}
.soc-12__id {
display: grid;
min-inline-size: 0;
}
.soc-12__name {
font-size: 1rem;
font-weight: 600;
letter-spacing: -.01em;
}
.soc-12__handle {
font-size: .85rem;
color: var(--muted);
overflow-wrap: anywhere;
}
.soc-12__bio {
margin: 0 0 1.5rem;
font-size: .9rem;
line-height: 1.6;
color: var(--muted);
text-wrap: pretty;
}
.soc-12__row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: .85rem;
}
.soc-12__btn {
display: inline-flex;
align-items: center;
gap: .5rem;
min-block-size: 2.875rem;
padding: 0 1.2rem;
border: 1px solid var(--brand);
border-radius: 999px;
background: var(--brand);
color: var(--on-brand);
font: inherit;
font-size: .92rem;
font-weight: 600;
cursor: pointer;
transition: background-color .2s ease, color .2s ease, border-color .2s ease, translate .18s cubic-bezier(.16,1,.3,1);
}
.soc-12__btn:active {
translate: 0 1px;
}
.soc-12__btn:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 3px;
}
.soc-12__icons {
display: grid;
inline-size: 1.05rem;
block-size: 1.05rem;
}
.soc-12__icons svg {
grid-area: 1/1;
inline-size: 1.05rem;
block-size: 1.05rem;
opacity: 0;
transition: opacity .16s ease;
}
.soc-12__labels {
display: grid;
}
.soc-12__labels span {
grid-area: 1/1;
opacity: 0;
white-space: nowrap;
transition: opacity .16s ease;
}
.soc-12__btn[data-state="idle"] .soc-12__l-follow,
.soc-12__btn[data-state="idle"] .soc-12__i-plus {
opacity: 1;
}
.soc-12__btn[data-state="following"] {
background: var(--surface);
color: var(--fg);
border-color: var(--line);
}
.soc-12__btn[data-state="following"] .soc-12__l-following,
.soc-12__btn[data-state="following"] .soc-12__i-check {
opacity: 1;
}
.soc-12__btn[data-state="following"]:hover,
.soc-12__btn[data-state="following"]:focus-visible {
background: color-mix(in oklab,var(--danger) 12%,var(--surface));
border-color: var(--danger);
color: var(--danger);
}
.soc-12__btn[data-state="following"]:hover .soc-12__l-following,
.soc-12__btn[data-state="following"]:focus-visible .soc-12__l-following,
.soc-12__btn[data-state="following"]:hover .soc-12__i-check,
.soc-12__btn[data-state="following"]:focus-visible .soc-12__i-check {
opacity: 0;
}
.soc-12__btn[data-state="following"]:hover .soc-12__l-unfollow,
.soc-12__btn[data-state="following"]:focus-visible .soc-12__l-unfollow,
.soc-12__btn[data-state="following"]:hover .soc-12__i-x,
.soc-12__btn[data-state="following"]:focus-visible .soc-12__i-x {
opacity: 1;
}
.soc-12__count {
margin: 0;
font-size: .88rem;
color: var(--muted);
}
.soc-12__num {
color: var(--fg);
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.soc-12__live {
margin: 1.1rem 0 0;
min-block-size: 1.1rem;
font-size: .8rem;
color: var(--muted);
}
@media (prefers-reduced-motion: reduce) {
.soc-12__btn,
.soc-12__labels span,
.soc-12__icons svg {
transition-duration: 1ms;
}
.soc-12__btn:active {
translate: none;
}
}
@media (forced-colors: active) {
.soc-12__btn {
border: 1px solid ButtonText;
background: ButtonFace;
color: ButtonText;
}
.soc-12__btn[data-state="following"]:hover {
background: Highlight;
color: HighlightText;
}
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.soc-12');
if (!root) return;
const btn = root.querySelector('.soc-12__btn');
const num = root.querySelector('.soc-12__num');
const live = root.querySelector('.soc-12__live');
let count = 18204;
btn.addEventListener('click', () => {
const following = btn.dataset.state === 'following';
btn.dataset.state = following ? 'idle' : 'following';
btn.setAttribute('aria-pressed', String(!following));
count += following ? -1 : 1;
num.textContent = count.toLocaleString('en-US');
live.textContent = following ? 'Unfollowed Priya Sharma.' : 'Following Priya Sharma.';
});
})();
```How this works
Stack the labels, do not swap them. Replacing text content changes the button's intrinsic width, so the control jumps as you toggle and any row beside it reflows. All three labels live in the same grid area and only their opacity changes — the button is always as wide as its longest label.
.soc-12__labels{ display:grid; }
.soc-12__labels span{
grid-area:1/1; opacity:0;
transition:opacity .16s ease;
}
.soc-12__btn[data-state="idle"] .soc-12__l-follow{ opacity:1; }One attribute, every visual. Surface, border, icon and label all key off [data-state] on the button. There is no classList.toggle chain to fall out of sync, and reading the current state in the console is one attribute lookup rather than three class checks.
Destructive intent only where it applies. The red Unfollow label is scoped to [data-state="following"]:hover and :focus-visible, so it can never appear over an unfollowed account. The check mark also swaps to a cross, because colour alone must never be the signal.
The trap: aria-pressed versus a label change. Do both. aria-pressed exposes the toggle state, and the live region announces the outcome in words, so a screen reader user hears "Following Priya Sharma" rather than inferring it from a pressed attribute change they may not have focus on.
Make it yours
- Change the destructive colour with
--danger; keep it at 4.5:1 against the button surface. - Add a third label variant (Requested, Pending) by adding one span and one data-state value.
- Remove the follower count if your design shows it elsewhere — nothing else depends on it.
- Swap the check mark for a bell icon to build a notify-me control with the same machinery.
- Tune the label crossfade with the shared
.16sduration; longer reads as sluggish on a toggle. - Set
--brandto match your product accent for the unfollowed state.
Gotchas — read before shipping
- Swapping
textContentresizes the button and shoves neighbouring content — stack the labels instead. - A red Unfollow that appears on hover but not on
:focus-visibleis unreachable by keyboard, so the destructive action becomes invisible to those users. - Colour alone cannot carry the destructive state; change the icon and the word too for colour-blind and forced-colors users.
- Optimistic toggling without a rollback path leaves the UI claiming a follow that the server rejected — revert the attribute in the failure branch of your request.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by oklch(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 104+.
Grid stacking, data attributes and aria-pressed work everywhere; the floor is individual transform properties for the press travel. color-mix() tints fall back to the declared hex.