Navigation
LinkSocial
Card-style link to external social profiles
Preview
size
icon
children
Code
import css from "./LinkSocial.module.css";
type Size = "sm" | "md" | "lg";
interface ResponsiveSize {
base: Size;
mobile?: Size;
tablet?: Size;
desktop?: Size;
wide?: Size;
}
interface LinkSocialProps {
size: Size | ResponsiveSize;
icon: string;
href: string;
target?: string;
alt?: string;
children: React.ReactNode;
}
function getSizeClasses(size: Size | ResponsiveSize): string {
if (typeof size === "string") {
return css[size];
}
const classes = [css[`${size.base}_base`]];
if (size.mobile) classes.push(css[`${size.mobile}_mobile`]);
if (size.tablet) classes.push(css[`${size.tablet}_tablet`]);
if (size.desktop) classes.push(css[`${size.desktop}_desktop`]);
if (size.wide) classes.push(css[`${size.wide}_wide`]);
return classes.join(" ");
}
export default function LinkSocial({
size,
icon,
href,
target,
alt,
children,
}: LinkSocialProps) {
return (
<a
className={`${css.link} ${getSizeClasses(size)}`}
href={href}
target={target}
rel={target === "_blank" ? "noopener noreferrer" : undefined}
>
<img src={icon} alt={alt ?? ""} />
<span>{children}</span>
</a>
);
}
.link {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: var(--space-12);
width: var(--link-size);
height: var(--link-size);
border-radius: var(--radius-8);
background: var(--color-surface);
text-decoration: none;
text-align: center;
transition: background 0.2s ease;
-webkit-tap-highlight-color: transparent;
/* Off the control ladder: 96 and 124 have no token, so the steps stay literal. */
&.sm {
--link-size: 96px;
--link-icon: 28px;
font-size: var(--text-14);
}
&.md {
--link-size: 112px;
--link-icon: 32px;
font-size: var(--text-14);
}
&.lg {
--link-size: 124px;
--link-icon: 40px;
font-size: var(--text-16);
}
&.sm_base {
--link-size: 96px;
--link-icon: 28px;
font-size: var(--text-14);
}
&.md_base {
--link-size: 112px;
--link-icon: 32px;
font-size: var(--text-14);
}
&.lg_base {
--link-size: 124px;
--link-icon: 40px;
font-size: var(--text-16);
}
@media (min-width: 480px) {
&.sm_mobile {
--link-size: 96px;
--link-icon: 28px;
font-size: var(--text-14);
}
&.md_mobile {
--link-size: 112px;
--link-icon: 32px;
font-size: var(--text-14);
}
&.lg_mobile {
--link-size: 124px;
--link-icon: 40px;
font-size: var(--text-16);
}
}
@media (min-width: 768px) {
&.sm_tablet {
--link-size: 96px;
--link-icon: 28px;
font-size: var(--text-14);
}
&.md_tablet {
--link-size: 112px;
--link-icon: 32px;
font-size: var(--text-14);
}
&.lg_tablet {
--link-size: 124px;
--link-icon: 40px;
font-size: var(--text-16);
}
}
@media (min-width: 1024px) {
&.sm_desktop {
--link-size: 96px;
--link-icon: 28px;
font-size: var(--text-14);
}
&.md_desktop {
--link-size: 112px;
--link-icon: 32px;
font-size: var(--text-14);
}
&.lg_desktop {
--link-size: 124px;
--link-icon: 40px;
font-size: var(--text-16);
}
}
@media (min-width: 1440px) {
&.sm_wide {
--link-size: 96px;
--link-icon: 28px;
font-size: var(--text-14);
}
&.md_wide {
--link-size: 112px;
--link-icon: 32px;
font-size: var(--text-14);
}
&.lg_wide {
--link-size: 124px;
--link-icon: 40px;
font-size: var(--text-16);
}
}
img {
width: var(--link-icon);
height: var(--link-icon);
object-fit: contain;
}
span {
font-family: var(--font-body);
font-weight: var(--weight-500);
color: var(--color-text);
}
&:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
@media (hover: hover) {
&:hover {
background: color-mix(in srgb, var(--color-surface), var(--color-text) 8%);
}
}
&:active {
background: color-mix(in srgb, var(--color-surface), var(--color-text) 15%);
}
}
<script setup lang="ts">
import { computed } from "vue";
type Size = "sm" | "md" | "lg";
interface ResponsiveSize {
base: Size;
mobile?: Size;
tablet?: Size;
desktop?: Size;
wide?: Size;
}
interface LinkSocialProps {
size: Size | ResponsiveSize;
icon: string;
href: string;
target?: string;
alt?: string;
}
const { size, target } = defineProps<LinkSocialProps>();
const sizeClasses = computed(() => {
if (typeof size === "string") {
return size;
}
const classes = [`${size.base}_base`];
if (size.mobile) classes.push(`${size.mobile}_mobile`);
if (size.tablet) classes.push(`${size.tablet}_tablet`);
if (size.desktop) classes.push(`${size.desktop}_desktop`);
if (size.wide) classes.push(`${size.wide}_wide`);
return classes.join(" ");
});
const rel = computed(() =>
target === "_blank" ? "noopener noreferrer" : undefined,
);
</script>
<template>
<a
class="link"
:class="sizeClasses"
:href="href"
:target="target"
:rel="rel"
>
<img :src="icon" :alt="alt ?? ''" />
<span><slot></slot></span>
</a>
</template>
<style scoped>
.link {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: var(--space-12);
width: var(--link-size);
height: var(--link-size);
border-radius: var(--radius-8);
background: var(--color-surface);
text-decoration: none;
text-align: center;
transition: background 0.2s ease;
-webkit-tap-highlight-color: transparent;
/* Off the control ladder: 96 and 124 have no token, so the steps stay literal. */
&.sm {
--link-size: 96px;
--link-icon: 28px;
font-size: var(--text-14);
}
&.md {
--link-size: 112px;
--link-icon: 32px;
font-size: var(--text-14);
}
&.lg {
--link-size: 124px;
--link-icon: 40px;
font-size: var(--text-16);
}
&.sm_base {
--link-size: 96px;
--link-icon: 28px;
font-size: var(--text-14);
}
&.md_base {
--link-size: 112px;
--link-icon: 32px;
font-size: var(--text-14);
}
&.lg_base {
--link-size: 124px;
--link-icon: 40px;
font-size: var(--text-16);
}
@media (min-width: 480px) {
&.sm_mobile {
--link-size: 96px;
--link-icon: 28px;
font-size: var(--text-14);
}
&.md_mobile {
--link-size: 112px;
--link-icon: 32px;
font-size: var(--text-14);
}
&.lg_mobile {
--link-size: 124px;
--link-icon: 40px;
font-size: var(--text-16);
}
}
@media (min-width: 768px) {
&.sm_tablet {
--link-size: 96px;
--link-icon: 28px;
font-size: var(--text-14);
}
&.md_tablet {
--link-size: 112px;
--link-icon: 32px;
font-size: var(--text-14);
}
&.lg_tablet {
--link-size: 124px;
--link-icon: 40px;
font-size: var(--text-16);
}
}
@media (min-width: 1024px) {
&.sm_desktop {
--link-size: 96px;
--link-icon: 28px;
font-size: var(--text-14);
}
&.md_desktop {
--link-size: 112px;
--link-icon: 32px;
font-size: var(--text-14);
}
&.lg_desktop {
--link-size: 124px;
--link-icon: 40px;
font-size: var(--text-16);
}
}
@media (min-width: 1440px) {
&.sm_wide {
--link-size: 96px;
--link-icon: 28px;
font-size: var(--text-14);
}
&.md_wide {
--link-size: 112px;
--link-icon: 32px;
font-size: var(--text-14);
}
&.lg_wide {
--link-size: 124px;
--link-icon: 40px;
font-size: var(--text-16);
}
}
img {
width: var(--link-icon);
height: var(--link-icon);
object-fit: contain;
}
span {
font-family: var(--font-body);
font-weight: var(--weight-500);
color: var(--color-text);
}
&:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
@media (hover: hover) {
&:hover {
background: color-mix(in srgb, var(--color-surface), var(--color-text) 8%);
}
}
&:active {
background: color-mix(in srgb, var(--color-surface), var(--color-text) 15%);
}
}
</style>
Props
| Prop | Type | Required | Description |
|---|---|---|---|
| size | "sm" | "md" | "lg" | ResponsiveSize | required | Card size (icon and text). Fixed or responsive. |
| icon | string | required | Path of the icon image. |
| href | string | required | Link destination. |
| target | string | — | Where it opens. With "_blank" it adds rel="noopener noreferrer". |
| alt | string | — | Alt text of the icon. Omitting it marks it decorative (alt=""), which is correct when the link already carries visible text. |
| children | React.ReactNode | required | Link content (network name). |