/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 @microkit/social-icon-buttonsCode
TypeScript component
1"use client";23import { useId, useState } from "react";4import type { CSSProperties } from "react";5import { ArrowRight } from "lucide-react";67import "./social-icon-buttons.css";89const socials = [10{ label: "Henrique Barone on LinkedIn", href: "https://www.linkedin.com/in/hbarone/", brand: "#0a66c2", icon: "linkedin" },11{ label: "Henrique Barone on GitHub", href: "https://github.com/henriquegpb", brand: "light-dark(#14181e,#f0f0f0)", icon: "github" },12{ label: "Henrique Barone on Instagram", href: "https://www.instagram.com/henrique_barone/", brand: "#e4405f", icon: "instagram" },13];1415function SocialIcon({ name = "linkedin" }) {16if (name === "linkedin") {17return <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>;18}1920if (name === "github") {21return <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>;22}2324return <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>;25}2627function SocialButtonBorder({ side = "", brand = "transparent" }) {28const gradientId = useId().replaceAll(":", "");29const fillGradientId = gradientId + "-fill";30const path = side === "left"31? "M10.6 2.14A17.5 17.5 0 0 0 10.6 33.86"32: "M25.4 2.14A17.5 17.5 0 0 1 25.4 33.86";33const fillCenter = side === "left" ? "0" : "36";3435return (36<svg className="social-button-border" data-side={side || undefined} viewBox="0 0 36 36" aria-hidden="true" style={{ color: brand }}>37<defs>38<linearGradient id={gradientId} x1="0" y1="2.14" x2="0" y2="33.86" gradientUnits="userSpaceOnUse">39<stop offset="0" stopColor="currentColor" stopOpacity="0" />40<stop offset=".28" stopColor="currentColor" stopOpacity=".2" />41<stop offset=".5" stopColor="currentColor" stopOpacity=".86" />42<stop offset=".72" stopColor="currentColor" stopOpacity=".2" />43<stop offset="1" stopColor="currentColor" stopOpacity="0" />44</linearGradient>45<radialGradient id={fillGradientId} cx={fillCenter} cy="18" r="25" gradientUnits="userSpaceOnUse">46<stop offset="0" stopColor="currentColor" stopOpacity=".14" />47<stop offset=".42" stopColor="currentColor" stopOpacity=".06" />48<stop offset=".82" stopColor="currentColor" stopOpacity="0" />49</radialGradient>50</defs>51<circle className="social-button-fill-edge" cx="18" cy="18" r="17" fill={"url(#" + fillGradientId + ")"} />52<circle className="social-button-border-base" cx="18" cy="18" r="17.5" />53<path className="social-button-border-edge" d={path} stroke={"url(#" + gradientId + ")"} />54</svg>55);56}5758export function SocialIconButtons() {59const [activeIndex, setActiveIndex] = useState(-1);60const activeBrand = activeIndex < 0 ? "transparent" : socials[activeIndex].brand;6162return (63<div className="social-icon-buttons" aria-label="Social links" onMouseLeave={() => setActiveIndex(-1)}>64{socials.map((social, index) => {65const adjacentSide = activeIndex < 0 || Math.abs(index - activeIndex) !== 166? ""67: index < activeIndex ? "right" : "left";6869return (70<a71className="social-icon-button"72href={social.href}73key={social.href}74target="_blank"75rel="noreferrer"76title={social.label}77aria-label={social.label}78data-adjacent-side={adjacentSide || undefined}79style={{ color: social.brand, "--social-brand": social.brand } as CSSProperties}80onMouseEnter={() => setActiveIndex(index)}81onFocus={() => setActiveIndex(index)}82onBlur={() => setActiveIndex(-1)}83>84<SocialButtonBorder side={adjacentSide} brand={activeBrand} />85<span className="social-icon-button-swap"><SocialIcon name={social.icon} /></span>86<span className="social-icon-button-swap social-icon-button-arrow"><ArrowRight size={17} strokeWidth={1.9} /></span>87</a>88);89})}90</div>91);92}
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: light-dark(#ffffff,#101015);16text-decoration: none;17transition: box-shadow .45s cubic-bezier(.22, 1, .36, 1);18}19.social-icon-button-swap {20display: grid;21place-items: center;22transition: transform .45s cubic-bezier(.22, 1, .36, 1);23}24.social-icon-button-swap:not(.social-icon-button-arrow) {25color: light-dark(#262d38,#e8e8ec);26}27.social-icon-button-swap svg {28width: 18px;29height: 18px;30}31.social-icon-button-arrow {32position: absolute;33transform: translateY(260%);34}35.social-button-border {36position: absolute;37inset: 0;38width: 100%;39height: 100%;40overflow: visible;41pointer-events: none;42}43.social-button-border-base,44.social-button-border-edge {45fill: none;46stroke-width: 1;47}48.social-button-border-base {49stroke: light-dark(#dfe3ea,#2b2b34);50transition: stroke .4s cubic-bezier(.22, 1, .36, 1);51}52.social-button-border-edge,53.social-button-fill-edge {54opacity: 0;55}56.social-button-border-edge {57stroke-linecap: round;58}59.social-button-border[data-side] .social-button-border-edge,60.social-button-border[data-side] .social-button-fill-edge {61opacity: 1;62animation: social-neighbor-in .4s ease-out;63}64.social-icon-button:hover .social-icon-button-swap:not(.social-icon-button-arrow),65.social-icon-button:focus-visible .social-icon-button-swap:not(.social-icon-button-arrow) {66transform: translateY(-260%);67}68.social-icon-button:hover .social-icon-button-arrow,69.social-icon-button:focus-visible .social-icon-button-arrow {70transform: translateY(0);71}72.social-icon-button:hover .social-button-border-base,73.social-icon-button:focus-visible .social-button-border-base {74stroke: currentColor;75}76.social-icon-button:hover,77.social-icon-button:focus-visible {78outline: none;79box-shadow: 0 0 18px color-mix(in srgb, var(--social-brand) 55%, transparent);80}81.social-icon-button:focus-visible {82outline: 2px solid currentColor;83outline-offset: 4px;84}85@keyframes social-neighbor-in {86from {87opacity: 0;88}89}90@media (prefers-reduced-motion: reduce) {91.social-icon-button,92.social-icon-button-swap {93transition: none;94}95.social-button-border-edge,96.social-button-fill-edge {97animation: none;98}99}