/Navigation
Navigation • CSS
Social Icon Buttons
Circular social links that trade their icon for a brand-colored arrow and glow on hover.
Installation
Requires lucide-react for its icons. The shadcn CLI installs it for you; copying the code by hand means installing it yourself.
Terminal
npx shadcn@latest add https://microkit.co/r/social-icon-buttons.jsonCode
TypeScript component
1"use client";23import { useId, useState } from "react";4import { ArrowRight } from "lucide-react";56import "./social-icon-buttons.css";78const socials = [9{ label: "Henrique Barone on LinkedIn", href: "https://www.linkedin.com/in/hbarone/", brand: "#0a66c2", icon: "linkedin" },10{ label: "Henrique Barone on GitHub", href: "https://github.com/henriquegpb", brand: "#f0f0f0", icon: "github" },11{ label: "Henrique Barone on Instagram", href: "https://www.instagram.com/henrique_barone/", brand: "#e4405f", icon: "instagram" },12];1314function SocialIcon({ name = "linkedin" }) {15if (name === "linkedin") {16return <svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M5.1 3.5a2.1 2.1 0 1 0 0 4.2 2.1 2.1 0 0 0 0-4.2ZM3.3 8.9h3.6V20H3.3V8.9Zm5.8 0h3.4v1.5h.1c.5-.9 1.6-1.9 3.5-1.9 3.7 0 4.4 2.4 4.4 5.6V20h-3.6v-5.1c0-1.2 0-2.8-1.7-2.8s-2 1.3-2 2.7V20H9.1V8.9Z" /></svg>;17}1819if (name === "github") {20return <svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M12 2C6.48 2 2 6.58 2 12.23c0 4.52 2.87 8.35 6.84 9.7.5.1.68-.22.68-.5v-1.9c-2.78.62-3.36-1.2-3.36-1.2-.46-1.19-1.11-1.5-1.11-1.5-.91-.64.07-.63.07-.63 1 .08 1.53 1.06 1.53 1.06.9 1.57 2.35 1.12 2.92.86.09-.67.35-1.12.64-1.38-2.22-.26-4.56-1.14-4.56-5.06 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.31.1-2.74 0 0 .84-.28 2.75 1.05A9.36 9.36 0 0 1 12 6.84c.85 0 1.7.12 2.5.34 1.91-1.33 2.75-1.05 2.75-1.05.55 1.43.2 2.48.1 2.74.64.72 1.03 1.63 1.03 2.75 0 3.93-2.34 4.8-4.57 5.06.36.32.68.94.68 1.89v2.8c0 .28.18.6.69.5A10.25 10.25 0 0 0 22 12.23C22 6.58 17.52 2 12 2Z" /></svg>;21}2223return <svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="5" stroke="currentColor" strokeWidth="2" /><circle cx="12" cy="12" r="4" stroke="currentColor" strokeWidth="2" /><circle cx="17.4" cy="6.6" r="1.2" fill="currentColor" /></svg>;24}2526function SocialButtonBorder({ side = "", brand = "transparent" }) {27const gradientId = useId().replaceAll(":", "");28const fillGradientId = gradientId + "-fill";29const path = side === "left"30? "M10.6 2.14A17.5 17.5 0 0 0 10.6 33.86"31: "M25.4 2.14A17.5 17.5 0 0 1 25.4 33.86";32const fillCenter = side === "left" ? "0" : "36";3334return (35<svg className="social-button-border" data-side={side || undefined} viewBox="0 0 36 36" aria-hidden="true" style={{ color: brand }}>36<defs>37<linearGradient id={gradientId} x1="0" y1="2.14" x2="0" y2="33.86" gradientUnits="userSpaceOnUse">38<stop offset="0" stopColor="currentColor" stopOpacity="0" />39<stop offset=".28" stopColor="currentColor" stopOpacity=".2" />40<stop offset=".5" stopColor="currentColor" stopOpacity=".86" />41<stop offset=".72" stopColor="currentColor" stopOpacity=".2" />42<stop offset="1" stopColor="currentColor" stopOpacity="0" />43</linearGradient>44<radialGradient id={fillGradientId} cx={fillCenter} cy="18" r="25" gradientUnits="userSpaceOnUse">45<stop offset="0" stopColor="currentColor" stopOpacity=".14" />46<stop offset=".42" stopColor="currentColor" stopOpacity=".06" />47<stop offset=".82" stopColor="currentColor" stopOpacity="0" />48</radialGradient>49</defs>50<circle className="social-button-fill-edge" cx="18" cy="18" r="17" fill={"url(#" + fillGradientId + ")"} />51<circle className="social-button-border-base" cx="18" cy="18" r="17.5" />52<path className="social-button-border-edge" d={path} stroke={"url(#" + gradientId + ")"} />53</svg>54);55}5657export function SocialIconButtons() {58const [activeIndex, setActiveIndex] = useState(-1);59const activeBrand = activeIndex < 0 ? "transparent" : socials[activeIndex].brand;6061return (62<div className="social-icon-buttons" aria-label="Social links" onMouseLeave={() => setActiveIndex(-1)}>63{socials.map((social, index) => {64const adjacentSide = activeIndex < 0 || Math.abs(index - activeIndex) !== 165? ""66: index < activeIndex ? "right" : "left";6768return (69<a70className="social-icon-button"71href={social.href}72key={social.href}73target="_blank"74rel="noreferrer"75title={social.label}76aria-label={social.label}77data-adjacent-side={adjacentSide || undefined}78style={{ color: social.brand }}79onMouseEnter={() => setActiveIndex(index)}80onFocus={() => setActiveIndex(index)}81onBlur={() => setActiveIndex(-1)}82>83<SocialButtonBorder side={adjacentSide} brand={activeBrand} />84<span className="social-icon-button-swap"><SocialIcon name={social.icon} /></span>85<span className="social-icon-button-swap social-icon-button-arrow"><ArrowRight size={17} strokeWidth={1.9} /></span>86</a>87);88})}89</div>90);91}
CSS
1.social-icon-buttons {2display: flex;3align-items: center;4gap: 10px;5}6.social-icon-button {7position: relative;8display: grid;9width: 42px;10height: 42px;11place-items: center;12overflow: hidden;13border: 0;14border-radius: 50%;15background: #101015;16text-decoration: none;17}18.social-icon-button-swap {19display: grid;20place-items: center;21transition: transform .32s cubic-bezier(.22, 1, .36, 1);22}23.social-icon-button-swap:not(.social-icon-button-arrow) {24color: #e8e8ec;25}26.social-icon-button-swap svg {27width: 18px;28height: 18px;29}30.social-icon-button-arrow {31position: absolute;32transform: translateY(260%);33}34.social-button-border {35position: absolute;36inset: 0;37width: 100%;38height: 100%;39overflow: visible;40pointer-events: none;41}42.social-button-border-base,43.social-button-border-edge {44fill: none;45stroke-width: 1;46}47.social-button-border-base {48stroke: #2b2b34;49transition: stroke .2s ease;50}51.social-button-border-edge,52.social-button-fill-edge {53opacity: 0;54}55.social-button-border-edge {56stroke-linecap: round;57}58.social-button-border[data-side] .social-button-border-edge,59.social-button-border[data-side] .social-button-fill-edge {60opacity: 1;61animation: social-neighbor-in .16s ease-out;62}63.social-icon-button:hover .social-icon-button-swap:not(.social-icon-button-arrow),64.social-icon-button:focus-visible .social-icon-button-swap:not(.social-icon-button-arrow) {65transform: translateY(-260%);66}67.social-icon-button:hover .social-icon-button-arrow,68.social-icon-button:focus-visible .social-icon-button-arrow {69transform: translateY(0);70}71.social-icon-button:hover .social-button-border-base,72.social-icon-button:focus-visible .social-button-border-base {73stroke: currentColor;74}75.social-icon-button:hover,76.social-icon-button:focus-visible {77outline: none;78box-shadow: 0 0 18px color-mix(in srgb, currentColor 55%, transparent);79}80.social-icon-button:focus-visible {81outline: 2px solid currentColor;82outline-offset: 4px;83}84@keyframes social-neighbor-in {85from {86opacity: 0;87}88}89@media (prefers-reduced-motion: reduce) {90.social-icon-button,91.social-icon-button-swap {92transition: none;93}94.social-button-border-edge,95.social-button-fill-edge {96animation: none;97}98}