Social links group
A compact list of links to an organisation's own social profiles: optically balanced monochrome marks on even square targets, with optional names, handles and a heading. Ships no URLs of its own.
cmp_footer_social_links_01 Preview
Fit to the available width. The frame follows the height of its content; previews taller than the maximum auto-height scroll inside it.
Give this component to your coding agent Copy a prompt that fetches this exact version and palette through the PageSugar MCP server.
Using the PageSugar MCP server, fetch component cmp_footer_social_links_01 version 1.0.0 with variant "inherit", first inspect its requirements and license status and confirm this project uses Svelte 5 and Tailwind CSS 4. Retrieve every manifest file, including binary assets and any manifest-only response files, preserving relative paths. Then integrate the source and follow its usage notes. Run project checks, review the browser result and report anything unverified. Do not substitute another version or invent missing files.Not connected yet? Set up the MCP server
Code
- Palette
- Inherited colour
- Version
- 1.0.0
- Digest
Full digest
sha256-af8b0add5e578e6a56ad3676fc86d656b298c25deaaa3e53fcb185e1357436dc
This component needs all 2 files. Download the ZIP
<script lang="ts" module>
import type { Snippet } from 'svelte';
import type { Platform } from './icons';
export type SocialPlatform = Platform;
export interface SocialProfile {
platform: Platform;
/** Your real profile URL. The component ships none of its own. */
href: string;
/** Accessible name, naming you and the platform: "Halcyon on LinkedIn". */
label: string;
/** Account name shown after the platform name in the labelled displays. */
handle?: string;
/** Visible platform name, for another language or an `other` profile. */
name?: string;
/** Your own mark, drawn in currentColor. Replaces the built-in one. */
icon?: Snippet;
}
</script>
<script lang="ts">
import { FALLBACK_MARK, MARKS, viewBoxFor } from './icons';
interface Props {
profiles: SocialProfile[];
heading?: string;
headingLevel?: 2 | 3 | 4 | 5 | 6;
display?: 'icons' | 'icons-with-labels' | 'labels';
size?: 'sm' | 'md';
orientation?: 'horizontal' | 'vertical';
openInNewTab?: boolean;
rel?: string;
listLabel?: string;
newTabLabel?: string;
}
let {
profiles,
heading,
headingLevel,
display = 'icons',
size = 'md',
orientation = 'horizontal',
openInNewTab = false,
rel = 'me noopener',
listLabel = 'Social media',
newTabLabel = '(opens in new tab)'
}: Props = $props();
const uid = $props.id();
/* A profile with no destination is dropped rather than rendered as a dead link. */
const items = $derived(profiles.filter((profile) => profile.href?.trim()));
const headingTag = $derived(headingLevel ? `h${headingLevel}` : 'p');
const showIcon = $derived(display !== 'labels');
const showText = $derived(display !== 'icons');
const visibleName = (profile: SocialProfile) =>
profile.name?.trim() ||
(profile.platform === 'other' ? profile.label : MARKS[profile.platform].name);
/*
* The label is read in place of the visible name only when it already contains it, so a
* voice command that speaks what is on screen always matches the link (WCAG 2.5.3).
*/
const nameInLabel = (profile: SocialProfile) =>
profile.label.toLocaleLowerCase().includes(visibleName(profile).toLocaleLowerCase());
/*
* Every link is a square target (icons) or a padded row (text) whose inset the list pulls
* back on both sides, so the first mark or word sits on the heading's edge, the last one
* ends flush when the group is aligned to the end, and the hover fill hangs into the gutter.
*/
const SIZES = {
md: {
list: '-mx-3',
square: 'size-11 justify-center rounded-lg',
row: 'min-h-11 min-w-11 gap-2 rounded-lg px-3 py-3 text-sm leading-5',
glyph: 'size-5',
glyphLine: 'h-5'
},
sm: {
list: '-mx-2',
square: 'size-8 justify-center rounded-md',
row: 'min-h-8 min-w-8 gap-2 rounded-md px-2 py-2 text-xs leading-4',
glyph: 'size-4',
glyphLine: 'h-4'
}
} as const;
const scale = $derived(SIZES[size]);
const linkClass =
'inline-flex max-w-full text-(--_muted) transition-[color,background-color,scale] duration-150 ease-[cubic-bezier(.2,0,0,1)] hover:bg-(--_hover) hover:text-(--_ink) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--_accent) active:scale-[.96] active:duration-[80ms] motion-reduce:active:scale-100';
</script>
{#snippet mark(profile: SocialProfile)}
{#if profile.icon}
<span class="block {scale.glyph} [&>svg]:size-full" aria-hidden="true">
{@render profile.icon()}
</span>
{:else if profile.platform === 'other'}
<svg
class="block {scale.glyph}"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.75"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
focusable="false"><path d={FALLBACK_MARK} /></svg
>
{:else}
{@const glyph = MARKS[profile.platform]}
<svg
class="block {scale.glyph}"
viewBox={viewBoxFor(glyph.scale)}
fill="currentColor"
aria-hidden="true"
focusable="false"><path d={glyph.path} /></svg
>
{/if}
{/snippet}
{#if items.length}
<div class="footer-social-links" data-display={display} data-size={size}>
{#if heading}
<svelte:element
this={headingTag}
id="{uid}-heading"
class="mb-2 text-sm leading-5 font-semibold text-balance text-(--_ink)"
>
{heading}
</svelte:element>
{/if}
<ul
role="list"
class={[
'm-0 flex list-none p-0',
scale.list,
orientation === 'vertical' ? 'flex-col items-start' : 'flex-wrap items-center gap-1'
]}
aria-labelledby={heading ? `${uid}-heading` : undefined}
aria-label={heading ? undefined : listLabel}
>
{#each items as profile, index (index)}
<li class="flex max-w-full min-w-0">
<a
href={profile.href}
rel={rel || undefined}
target={openInNewTab ? '_blank' : undefined}
class={[linkClass, showText ? ['items-start', scale.row] : scale.square]}
>
{#if showIcon}
<span class="flex shrink-0 items-center {showText ? scale.glyphLine : ''}"
>{@render mark(profile)}</span
>
{/if}
{#if showText}
<span class="min-w-0 [overflow-wrap:anywhere]">
<span class="sr-only">{profile.label}</span>
<span class="font-medium" aria-hidden={nameInLabel(profile) ? 'true' : undefined}
>{visibleName(profile)}</span
>
{#if profile.handle}
<bdi class="font-normal text-(--_subtle)">{profile.handle}</bdi>
{/if}
{#if openInNewTab}<span class="sr-only"> {newTabLabel}</span>{/if}
</span>
{:else}
<span class="sr-only"
>{profile.label}{#if openInNewTab} {newTabLabel}{/if}</span
>
{/if}
</a>
</li>
{/each}
</ul>
</div>
{/if}
<style>
/*
* Public tokens. Every colour defaults to the surrounding text colour, so the group takes the
* footer, author box or dark band it sits in without any change.
*/
.footer-social-links {
--_accent: var(--footer-social-links-accent, currentColor);
--_ink: var(--footer-social-links-ink, currentColor);
--_muted: var(--footer-social-links-muted, currentColor);
--_subtle: var(--footer-social-links-subtle, currentColor);
--_hover: var(--footer-social-links-hover, color-mix(in srgb, currentColor 8%, transparent));
}
</style>
Usage#
On this pagePresentational only: renders the profiles you pass, in the order you pass them. It has no default or example URLs, does not check that a profile exists, fetch follower counts, embed feeds or offer share-this-page buttons, and does not offer brand colours. No runtime packages beyond Svelte and Tailwind CSS.
- Suggested location
src/lib/components/footer-social-links-01- Required props
profiles
Limitations
- Profiles with a blank href are skipped; with none left, nothing renders.
- The built-in marks are trademarks of their owners. Use them in one colour to link to your own profiles; the component offers no brand-colour option.
- The LinkedIn mark comes from Simple Icons 13.0.0, the last release that carried it.
- Visible platform names are English; pass name on each profile to translate them.
- Light appearance by default. It inherits the surrounding text colour, so it works on dark surfaces without a dark mode being declared.
Example
<script lang="ts">
import SocialLinks, {
type SocialProfile
} from '$lib/components/footer-social-links-01/SocialLinks.svelte';
const profiles: SocialProfile[] = [
{ platform: 'mastodon', href: 'https://mastodon.example/@yourname', label: 'Example Co on Mastodon' },
{ platform: 'linkedin', href: 'https://linkedin.example/company/yourname', label: 'Example Co on LinkedIn' },
{ platform: 'github', href: 'https://github.example/yourname', label: 'Example Co on GitHub' }
];
</script>
<SocialLinks {profiles} heading="Follow us" />Social links group#
Pass your real profiles. The component has no default URLs; the examples here and in the
preview use reserved .example domains, so replace every href with your own account.
<script lang="ts">
import SocialLinks, {
type SocialProfile
} from '$lib/components/footer-social-links-01/SocialLinks.svelte';
const profiles: SocialProfile[] = [
{
platform: 'mastodon',
href: 'https://mastodon.example/@yourname',
label: 'Example Co on Mastodon',
handle: '@yourname@mastodon.example'
},
{
platform: 'linkedin',
href: 'https://linkedin.example/company/yourname',
label: 'Example Co on LinkedIn'
},
{
platform: 'github',
href: 'https://github.example/yourname',
label: 'Example Co on GitHub',
handle: 'yourname'
}
];
</script>
<div
class="text-zinc-600 [--footer-social-links-ink:#09090b] [--footer-social-links-subtle:#71717a]"
>
<SocialLinks {profiles} heading="Follow us" />
</div>In a footer#
Footers take the group through a snippet rather than importing it. With
footer-multi-column-01:
{#snippet social()}
<SocialLinks {profiles} heading="Follow us" headingLevel={3} size="sm" />
{/snippet}
<FooterMultiColumn {brand} {columns} {copyright} {social} />Colour#
Everything defaults to currentColor, so the group takes the text colour of whatever it sits
in. The recommended light setup is a zinc-600 container with ink at #09090b and subtle at
#71717a. On a zinc-950 band, use text-zinc-400 with ink at #fafafa and subtle at
#a1a1aa. The hover fill is 8% of the link colour; set --footer-social-links-hover: transparent for a tone-only hover.
Your own mark#
Use platform: 'other' and pass icon, a snippet that draws an SVG in currentColor on a
24 px grid. It is sized for you and wrapped in an aria-hidden span; give the SVG aria-hidden="true" and
focusable="false" too, and nothing inside it that takes focus. Without icon, an
other profile shows a globe. Set name for the visible text, and write a label that contains it ("Example Co forum"
for a visible "Forum"), so a voice command that says what is on screen matches the link. When the
label does not contain the name, the name is read after it.
Trademarks#
The built-in marks come from Simple Icons under CC0. That covers the drawing, not the brand: each mark is a trademark of its owner. Use them to link to your own profiles, in one colour and at their shipped proportions. LinkedIn asked Simple Icons to remove its mark in 14.0.0; the path here is from 13.0.0. Replace it with the mark from LinkedIn's own brand kit if you prefer.
Mastodon verification#
rel defaults to me noopener. Keep me, then add your site's URL to your Mastodon profile's
metadata; Mastodon checks that the page links back with rel="me" and shows a verified tick.
Props and content inputs#
On this page| Name | Type | Required | Default | Description |
|---|---|---|---|---|
profiles | SocialProfile[] | Yes | None | { platform, href, label, handle?, name?, icon? }[]; platform is one of the eleven built-in marks or 'other'. label is the accessible name ("Example Co on LinkedIn"), handle follows the name in the labelled displays, name replaces the visible platform name, icon is a snippet drawing your own mark in currentColor. No defaults. |
heading | string | No | None | Visible label above the list, which also names it. Omitted, the list is named by listLabel. |
headingLevel | 2 | 3 | 4 | 5 | 6 | No | None | Renders the heading as h2 to h6. Omitted, it is a paragraph. |
display | 'icons' | 'icons-with-labels' | 'labels' | No | 'icons' | Marks only, marks with the platform name and handle, or names only. |
size | 'sm' | 'md' | No | 'md' | md: 20 px marks on 44 px targets, 14 px text. sm: 16 px marks on 32 px targets, 12 px text. |
orientation | 'horizontal' | 'vertical' | No | 'horizontal' | A wrapping row, or a column of rows. |
openInNewTab | boolean | No | false | Opens each profile in a new tab and appends newTabLabel to each link's name. |
rel | string | No | 'me noopener' | rel on every link. 'me' lets Mastodon and similar services verify the profile links back to your site. |
listLabel | string | No | 'Social media' | Accessible name of the list when there is no heading. |
newTabLabel | string | No | '(opens in new tab)' | Visually hidden text after each name when openInNewTab is set; translate it with the page. |
Customization#
On this pageChange content through props. Every colour defaults to the surrounding text colour; five --footer-social-links-* variables (accent, ink, muted, subtle, hover) retone the focus ring, hover, rest, handle and hover fill. Edit the SIZES map for other sizes.
- Colour: put the group inside your footer's text colour (for example
text-zinc-600on white) and the marks, names and focus ring follow. Set--footer-social-links-inkto your strongest text colour so the heading and hovered links step up to it. - Handles: set
--footer-social-links-subtleto your tertiary text colour (#71717aon white) so handles sit a step below the names; keep it at 4.5:1. - Dark or tinted page: nothing to change if the container sets its own text colour. On a
zinc-950band usetext-zinc-400with ink#fafafaand subtle#a1a1aa. - Hover fill:
--footer-social-links-hoverdefaults to 8% of the link colour; set transparent for a tone-only hover. - Focus:
--footer-social-links-accentcolours the focus ring; keep it at 3:1 against the surface. - Marks:
icons.tsholds each path and its optical scale. Add a platform by adding an entry and its name to the Platform type, or pass icon on an 'other' profile. - Sizes: the SIZES map at the top of the script holds each size's target, padding, glyph and text classes as complete Tailwind strings.
- Mastodon verification: keep 'me' in rel, and add your site's URL to the profile's metadata on the Mastodon side.
Public CSS variables
| Variable | Token |
|---|---|
--footer-social-links-accent | accent |
--footer-social-links-ink | ink |
--footer-social-links-muted | muted |
--footer-social-links-subtle | subtle |
--footer-social-links-hover | hover |
Accessibility#
On this page- A ul with role="list" (so Safari keeps list semantics without markers), named by the heading through
aria-labelledby, or bylistLabel("Social media") when there is no heading. - Each link's accessible name is its label, such as "Halcyon on LinkedIn". In the labelled displays the visible platform name is hidden from assistive technology only when the label already contains it; otherwise it is read after the label, so the name always contains what is on screen (WCAG 2.5.3). Handles follow in a bdi element so they keep their own direction.
- Marks are
aria-hiddenwith focusable="false"; a custom icon snippet is wrapped in anaria-hiddenspan, and its SVG should carryaria-hidden="true" and focusable="false" as well. - No title tooltips: the name is in the markup, not on hover.
- md targets are at least 44 by 44 px; sm targets are at least 32 px, above the 24 px WCAG 2.2 minimum, for dense small-print rows.
- Focus shows a two-pixel outline in the accent, offset by two pixels, following the target's radius, on :focus-visible only.
- With
openInNewTab, each link opens a new tab and its name ends withnewTabLabel. - Logical properties only; the list mirrors under dir="rtl" and the marks keep their shapes.
- Server-rendered with no client JavaScript; ids come from
$props.id().
Known limitations
- The label is yours to write: include your organisation and the platform, and translate it with the page.
- When the group inherits a pale text colour, check the marks at 3:1 and the names at 4.5:1 against the surface.
Release details#
On this page- Integration
- Presentational
- Works without client-side JavaScript
- Server-side rendering supported
- Dependencies
- No additional runtime packages beyond Svelte and Tailwind CSS
- License
MIT. Default license approval is pending; see the license status before adopting the source.
- Version history
- 1.0.0 (Published) Current release · 30 September 2026
Only the current release is available. Keep downloaded source and its receipt if you need to use it again later.