Dynamic Social Share Images

Published Oct 7, 2021

Table of Contents

Why Use Social Share Images

Using a social share image is important to make your content stand out in a feed when you link it on social media because it draws your attention and has a large actionable area.

Example of a social share image

In this post I’m going to show you a simple way how to automate that process, so that you’re able to art direct and quickly export one for your post using what you already know as a developer without involving a headless browser, or serverless.

Open Graph Meta Tags

If you’re unfamiliar with the open graph protocol it’s just metadata that represents the content of your site first created by Facebook. Some of the common <meta> tags include og:title, og:type, og:image, and og:url that are used to display information inside social cards.

<meta content="https://joyofcode.xyz/og-image.png" property="og:image" />

Here is a Twitter example that displays the large card for Twitter Cards. Other services such as Discord can decide to also use these.

<meta content="summary_large_image" name="twitter:card" />
<meta content="https://joyofcode.xyz/og-image.png" name="twitter:image" />

Project Setup

We’re going to design the social share image with plain HTML and CSS, then take an optimized image of it that we can save.

Finished project showing a social share card and a save button

We’re going to use Vite to initialize the project with vanilla TypeScript which is optional, so you can select vanilla if you prefer JavaScript and ignore types.

# initialize project
npm init vite@latest

# install dependencies
npm i

# start development server
npm run dev


Let’s add some markup in index.html.

<div class="social" data-social>
  <span class="social-site" contenteditable>Joy of Code</span>
  <h1 class="social-title" contenteditable>Post Title</h1>

<button data-save>Save</button>

Thanks to the contenteditable attribute we can directly edit the text inside the element. We have to disable :focus because in Chrome it displays an outline which is going to end up in the final image.

For the styles we have to define a width and height of 1200x630 recommended by Facebook for social share images. We’re going to add some simple styles and add a design element for interest that can be a SVG, or a simple shape using CSS.

Social share image

Feel free to experiment with the design of your card. You can find a lot of cool patterns from places such as Hero Patterns and unDraw, or use any SVG illustration site you like.

@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap');

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;

:root {
  --font-sans: 'Inter', sans-serif;

  --gray-10: hsl(220 20% 10%);
  --gray-14: hsl(220 20% 14%);
  --white-98: hsl(0 0% 98%);
  --blue-50: hsl(240 100% 50%);
  --purple-25: hsl(300 100% 25%);

  --spacing-1: 1rem;
  --spacing-2: 2rem;
  --radius: 4px;

body {
  height: 100%;

body {
  display: grid;
  place-content: center;
  justify-items: start;
  font-family: var(--font-sans);
  color: var(--white-98);
  background-color: var(--gray-14);

button {
  position: relative;
  padding: var(--spacing-1) var(--spacing-2);
  margin: var(--spacing-2) 0;
  font-size: 1.4rem;
  font-weight: bold;
  color: var(--white-98);
  background-color: var(--gray-10);
  border: none;
  border-radius: var(--radius);
  cursor: pointer;

button::before {
  content: '';
  position: absolute;
  inset: 0;
  transform: translateY(4px) translateX(4px);
  background-image: linear-gradient(to right, var(--blue-50), var(--purple-25));
  z-index: -10;

.social {
  width: 1200px;
  height: 630px;
  display: flex;
  position: relative;
  flex-direction: column;
  align-items: flex-start;
  justify-content: space-between;
  padding: var(--spacing-2);
  background-color: var(--gray-10);
  overflow: hidden;
  z-index: 10;

.social::after {
  content: '';
  position: absolute;
  bottom: -40%;
  right: -20%;
  height: 800px;
  width: 800px;
  background-image: linear-gradient(
    to bottom left,
  border-radius: 50%;
  z-index: -10;

.social-site {
  padding: var(--spacing-1) var(--spacing-2);
  font-size: 1.2rem;
  font-weight: 700;
  color: var(--gray-10);
  background-color: var(--white-98);
  border-radius: var(--radius);

.social-title {
  max-width: 80%;
  font-size: 6rem;
  font-weight: 900;

.social-title:focus {
  outline: none;


Speaking of taking images 📸 we’re going to need html-to-image and browser-image-compression which is awesome because it works in the browser meaning we can deploy the site anywhere and use it as is.

npm i html-to-image browser-image-compression

Inside main.ts we’re going to import the packages and attach a click event listener on the button.

import * as htmlToImage from 'html-to-image'
import imageCompression from 'browser-image-compression'

const socialElement = document.querySelector<HTMLDivElement>('[data-social]')!
const saveElement = document.querySelector<HTMLButtonElement>('[data-save]')!

saveElement.addEventListener('click', () => saveImage(socialElement))

We’re going to create a saveImage function that takes a HTML node as a parameter and then creates a Blob which is a file-like object that can be read as text or binary data which is something we can pass to be compressed.

You could use other simple html-to-image methods such as toPng if you just want to get the image.

Once we create the compressed file we’re going to create a URL from it using the window.URL.createObjectURL method that creates a URL object from a File, Blob, or MediaSource that looks like blob:http://localhost:3000/7fe50800-234c-496b-a336-51cff55c4308.

Lastly we’re going to create the <a> link tag, give the download a name of og-image.png and assign it to the href attribute. After that is done we’re going to use click on it to start the download.

This is going to happen each time you press Save.

async function saveImage(node: HTMLElement) {
  try {
    // create Blob from node
    const imageOptions = { width: 1200, height: 630 }
    const imageBlob = (await htmlToImage.toBlob(node, imageOptions)) as File

    // pass Blob and the quality option to be compressed
    const compressionOptions = { initialQuality: 1 }
    const compressedFile = await imageCompression(imageBlob, compressionOptions)

    // create URL from Blob
    const imageUrl = window.URL.createObjectURL(compressedFile)

    // download image
    const linkElement = document.createElement('a')
    linkElement.download = 'og-image.png'
    linkElement.href = imageUrl
  } catch (error) {
    throw new Error(`Something went wrong: ${error}`)

Depending how complex your image is the size may vary. In this example before compression the size is around 76 kB and after compression it’s around 14 kB.

The design I use for Joy of Code has a size of 400 kB before compression and 100 kB after. You can have a look at what I use for the site to create posts that generates the image and post front matter. Here’s the source code to the repository.


If you want to preview how your card looks on different sites use Social Share Preview. You can deploy this to anywhere like Netlify by simply pointing to your repository since it’s just a static site.

Thanks for reading! 🏄‍♀️


If you want to support the content you're reading or watching on YouTube consider becoming a patreon starting low as 1$ per month.

Become a patreon
Subscribe For Updates
Found a mistake?

Every post is a Markdown file so contributing is simple as following the link below and pressing the pencil icon inside GitHub to edit it.

Edit on GitHub