From CSS to SVG animations for GitHub README

GitHub recently added support for Profile READMEs, allowing one to display a public README.md using their special named repository (eg. CER10TY/CER10TY). I wanted to display a simple animation that links to this website using my logo and some text.

GitHub Flavored Markdown is the markdown spec used by GitHub (based on CommonMark). Simple GIFs can be placed using standard markdown syntax: ![Alt Text](Image Link), and it even allows for some HTML tags. Crucially, <video> is not allowed, but <svg> is. In practice, we can embed GIFs and SVGs but we cannot embed videos (eg. .mp4 files).

Animating and converting to GIF/MP4#

The animation idea was fairly straightforward: Using two SVGs, I shift my logo and a separate box shadow away from each other, popping out a text from “in between” and shifting the text to the right.

The required HTML and CSS is absolutely minimal, since I already have the SVGs prepped:

<div class="container">
    <object data="SJLogo.svg" type="image/svg+xml" width="128" height="128" class="svg-style"></object>
    <object data="SJLogoBackdrop.svg" type="image/svg+xml" width="128" height="128" class="svg-style-prop"></object>
    <h2 class="text-backdrop">soeren.codes</h2>
</div>
.text-backdrop {
    width: 0;
    overflow: hidden;
    z-index: -1;
    position: absolute;
    animation: anim-text .45s ease-in forwards .6s;
    text-transform: uppercase;
    font-size: 2.5rem;
    font-family: 'Share Tech Mono', monospace;
}

.svg-style {
    z-index: 1;
    position: absolute;
    animation: anim-logo .45s ease-out forwards, anim-move-logo .45s ease-out forwards .55s;
}

.svg-style-prop {
    z-index: -2;
    position: absolute;
    animation: anim-boxshadow .45s ease-out forwards, anim-move-box .45s ease-out forwards .55s;
}

@keyframes anim-boxshadow {
    0% {
        transform: translate(0, 0);
    }
    100% {
        opacity: 100%;
        transform: translate(5px, 5px);
    }
}

@keyframes anim-logo {
    0% {
        transform: translate(0, 0);
    }
    100% {
        transform: translate(-5px, -5px);
    }
}

@keyframes anim-text {
    0% {
        overflow: hidden;
        transform: translateX(0);
    }
    100% {
        overflow: visible;
        transform: translateX(20px);
    }
}

@keyframes anim-move-box {
    100% {
        transform: translate(-60px, 5px);
    }
}

@keyframes anim-move-logo {
    100% {
        transform: translate(-70px, -5px);
    }
}

Initially, I simply wanted to record a GIF and then use that in the README. For that, I used LICEcap to record a GIF at 30 FPS, the result being a 160KB GIF:

GIF Logo

The GIF is not cropped, so the animation replays a few times and has a weird bump at the end. Either way, I was unsatisfied with the frame rate, the forced white background and file size. As an alternative, I recorded an MP4 using my Mac:

Much smoother!

Unfortunately, GitHub does not support embedding video tags (as mentioned previously), though I only found out after I made the video.

Since the GIF is quite large, not smooth enough and video is not supported, we’ll have to use SVG animations.

Combining everything into one SVG#

At this point, I use two SVGs, one for the logo and one for the box shadow. The text is completely separate. To be able to animate all of the elements together, we’ll first have to combine everything into a single SVG.

For reference, here’s the Logo:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
	<defs>
		<image width="512" height="512" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIAAQMAAADOtka5AAAAAXNSR0IB2cksfwAAAANQTFRFGDBZeL4QsQAAADZJREFUeJztwQEBAAAAgiD/r25IQAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfBuCAAAB0niJ8AAAAABJRU5ErkJggg=="/>
	</defs>
	<style>
		tspan { white-space:pre }
	</style>
	<use id="Background" href="#img1" x="0" y="0" />
	<path id="SJ " fill="#ffffff" d="M258.59 177.68L256.77 215.21Q251.8 213.04 245.14 210.86Q238.61 208.55 231.34 206.74Q224.2 204.92 216.81 203.83Q209.42 202.62 202.88 202.62Q191.5 202.62 185.08 205.53Q178.66 208.43 178.66 215.34Q178.66 218.61 180.84 221.15Q183.14 223.57 187.26 225.75Q191.38 227.81 197.07 229.87Q202.88 231.8 209.91 233.98L218.75 236.77Q229.89 240.16 238.73 245Q247.57 249.73 253.62 256.27Q259.8 262.68 263.07 271.04Q266.34 279.39 266.34 290.05Q266.34 302.52 262.34 312.82Q258.46 322.99 250.11 330.25Q241.75 337.52 228.68 341.52Q215.72 345.51 197.43 345.51Q181.21 345.51 165.1 342.85Q149.12 340.18 132.77 333.52L132.77 293.56Q148.39 301.55 163.04 305.31Q177.82 308.94 192.11 308.94Q203.85 308.94 209.18 305.19Q214.63 301.43 214.63 294.77Q214.63 290.78 212.57 287.99Q210.63 285.09 206.15 282.66Q201.79 280.24 194.65 277.82Q187.5 275.28 177.33 272.13Q167.16 269.1 158.44 264.5Q149.84 259.9 143.55 253.48Q137.25 246.94 133.62 238.34Q130.11 229.75 130.11 218.73Q130.11 207.34 134.1 197.9Q138.1 188.45 146.33 181.79Q154.57 175.01 167.16 171.26Q179.88 167.5 197.31 167.5Q204.21 167.5 211.96 168.11Q219.71 168.71 227.71 169.93Q235.82 171.14 243.69 173.07Q251.56 175.01 258.59 177.68ZM324.34 345.51Q313.32 345.51 303.03 343.94Q292.86 342.36 285.59 337.76L285.59 303.25Q293.7 305.79 300.61 306.52Q307.63 307.13 312.71 307.13Q318.16 307.13 321.19 304.95Q324.34 302.64 325.91 299.13Q327.49 295.62 327.85 291.26Q328.21 286.78 328.21 282.42L328.21 170.77L382.46 170.77L382.46 290.05Q382.46 304.58 377.38 315.12Q372.29 325.53 364.06 332.31Q355.95 339.09 345.41 342.36Q335 345.51 324.34 345.51Z" />
</svg>

… and the box shadow:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
	<rect width="512" height="512" style="fill:#000000"></rect>
</svg>

I made the Logo using Photopea, which evidently just exports an image instead of a <rect> tag for the background. Substituting the existing <image> and <use> tags with a simple <rect> tag, as seen in the box shadow implementation, works just as well. This simplifies the logo tremendously. After some experimenting, I ended up with the following logo:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 527" height="527" width="1400">
    <defs>
	<style type="text/css">
        @font-face {
            src: url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&amp;display=swap');
        }
	</style>
    </defs>
	<rect width="512" height="512" style="fill:#000000" x="20" y="40"></rect>
	<rect width="512" height="512" style="fill:#fd0054"></rect>
	<path fill="#ffffff" d="M258.59 177.68L256.77 215.21Q251.8 213.04 245.14 210.86Q238.61 208.55 231.34 206.74Q224.2 204.92 216.81 203.83Q209.42 202.62 202.88 202.62Q191.5 202.62 185.08 205.53Q178.66 208.43 178.66 215.34Q178.66 218.61 180.84 221.15Q183.14 223.57 187.26 225.75Q191.38 227.81 197.07 229.87Q202.88 231.8 209.91 233.98L218.75 236.77Q229.89 240.16 238.73 245Q247.57 249.73 253.62 256.27Q259.8 262.68 263.07 271.04Q266.34 279.39 266.34 290.05Q266.34 302.52 262.34 312.82Q258.46 322.99 250.11 330.25Q241.75 337.52 228.68 341.52Q215.72 345.51 197.43 345.51Q181.21 345.51 165.1 342.85Q149.12 340.18 132.77 333.52L132.77 293.56Q148.39 301.55 163.04 305.31Q177.82 308.94 192.11 308.94Q203.85 308.94 209.18 305.19Q214.63 301.43 214.63 294.77Q214.63 290.78 212.57 287.99Q210.63 285.09 206.15 282.66Q201.79 280.24 194.65 277.82Q187.5 275.28 177.33 272.13Q167.16 269.1 158.44 264.5Q149.84 259.9 143.55 253.48Q137.25 246.94 133.62 238.34Q130.11 229.75 130.11 218.73Q130.11 207.34 134.1 197.9Q138.1 188.45 146.33 181.79Q154.57 175.01 167.16 171.26Q179.88 167.5 197.31 167.5Q204.21 167.5 211.96 168.11Q219.71 168.71 227.71 169.93Q235.82 171.14 243.69 173.07Q251.56 175.01 258.59 177.68ZM324.34 345.51Q313.32 345.51 303.03 343.94Q292.86 342.36 285.59 337.76L285.59 303.25Q293.7 305.79 300.61 306.52Q307.63 307.13 312.71 307.13Q318.16 307.13 321.19 304.95Q324.34 302.64 325.91 299.13Q327.49 295.62 327.85 291.26Q328.21 286.78 328.21 282.42L328.21 170.77L382.46 170.77L382.46 290.05Q382.46 304.58 377.38 315.12Q372.29 325.53 364.06 332.31Q355.95 339.09 345.41 342.36Q335 345.51 324.34 345.51Z" id="SJ"></path>
	<text id="text18" style="font-size:90px; font-family: 'Share Tech Mono', monospace;"><tspan y="256" x="600"><tspan style="text-transform:uppercase">soeren.codes</tspan></tspan></text>
</svg>

I used Inkscape to create the header text, cutting down significantly on the pre-generated SVG, just leaving the <text> attribute. The viewBox attribute is very important! It tells the SVG the aspect ratio so you are able to scale it when using it with any tag by specifying width and height.

I haven’t found any good SVG editor with live reload yet, so what I ended up doing was using Firefox to create the new SVG from scratch. Fire up a blank tab, open the developer console and get to editing the HTML. This gives you live reload, but it’s not very helpful when starting to animate (since you lose the SVG when reloading the page, so keep a backup!).

Animating the SVG#

When animating an SVG, there’s two key attributes: <animateMotion> and <animate>. The former is used to animate motion, similar to transform: translate() used in CSS animations. The latter is used to animate attributes such as opacity. A quick and dirty example for both attributes:

<animateMotion dur="0.25s" values="100,0; 100,30" fill="freeze"></animateMotion>
<animate attributeName="opacity" begin="0.35s" to="1" dur="0.15s" fill="freeze"></animate>

The fill="freeze" tells the SVG to keep the object at its new position after the animation is finished, which is exactly like the forwards value for animation-fill-mode in CSS. When animating a motion, we need to tell the SVG its X and Y values. We can add as many values as we like, but usually the interpolation is very good. Again, think about this like a regular CSS keyframe, where you’d specify 0% (starting position) and 100% (end position), with as many values in between as you need. When chaining animations, the begin attribute is used to specify an animation delay (so in the above case, the animation would start after 0.35s).

The logo and box shadow#

Animations equal experimentation. SVGs are no different. I started with trying out the “split” of the logo and the box shadow. There’s a few things to note:

  1. Both the logo and the box shadow will have to be moved to the left later, so they start slightly indented (X=100)
  2. The logo has to move to the top left, whereas the box shadow has move to the top right.

Keeping in mind the second point, we know that the logo will need a negative X value and a negative Y value during the split. On the other hand, the box shadow will get a positive X value and positive Y value during the animation, to move it downwards.

The animations roughly look like this:

<rect width="512" height="512" style="fill:#000000" x="100" y="0">
    <animateMotion dur="0.25s" values="100,0; 100,30" fill="freeze"></animateMotion>
</rect>
<rect width="512" height="512" style="fill:#fd0054" x="100" y="0">
    <animateMotion dur="0.25s" values="100,0; 70,-20" fill="freeze"></animateMotion>
</rect>
<path fill="#ffffff" d="M258.59 177.68L256.77 215.21Q251.8 213.04 245.14 210.86Q238.61 208.55 231.34 206.74Q224.2 204.92 216.81 203.83Q209.42 202.62 202.88 202.62Q191.5 202.62 185.08 205.53Q178.66 208.43 178.66 215.34Q178.66 218.61 180.84 221.15Q183.14 223.57 187.26 225.75Q191.38 227.81 197.07 229.87Q202.88 231.8 209.91 233.98L218.75 236.77Q229.89 240.16 238.73 245Q247.57 249.73 253.62 256.27Q259.8 262.68 263.07 271.04Q266.34 279.39 266.34 290.05Q266.34 302.52 262.34 312.82Q258.46 322.99 250.11 330.25Q241.75 337.52 228.68 341.52Q215.72 345.51 197.43 345.51Q181.21 345.51 165.1 342.85Q149.12 340.18 132.77 333.52L132.77 293.56Q148.39 301.55 163.04 305.31Q177.82 308.94 192.11 308.94Q203.85 308.94 209.18 305.19Q214.63 301.43 214.63 294.77Q214.63 290.78 212.57 287.99Q210.63 285.09 206.15 282.66Q201.79 280.24 194.65 277.82Q187.5 275.28 177.33 272.13Q167.16 269.1 158.44 264.5Q149.84 259.9 143.55 253.48Q137.25 246.94 133.62 238.34Q130.11 229.75 130.11 218.73Q130.11 207.34 134.1 197.9Q138.1 188.45 146.33 181.79Q154.57 175.01 167.16 171.26Q179.88 167.5 197.31 167.5Q204.21 167.5 211.96 168.11Q219.71 168.71 227.71 169.93Q235.82 171.14 243.69 173.07Q251.56 175.01 258.59 177.68ZM324.34 345.51Q313.32 345.51 303.03 343.94Q292.86 342.36 285.59 337.76L285.59 303.25Q293.7 305.79 300.61 306.52Q307.63 307.13 312.71 307.13Q318.16 307.13 321.19 304.95Q324.34 302.64 325.91 299.13Q327.49 295.62 327.85 291.26Q328.21 286.78 328.21 282.42L328.21 170.77L382.46 170.77L382.46 290.05Q382.46 304.58 377.38 315.12Q372.29 325.53 364.06 332.31Q355.95 339.09 345.41 342.36Q335 345.51 324.34 345.51Z" id="SJ">
	<animateMotion dur="0.25s" values="160,0; 140,-20" fill="freeze"></animateMotion>
</path>

I already included the animation for the bold text (“SJ”) in the snippet; the motion values are not exactly in line with the rectangles, but it’s the result of experimenting with different values.

Fading in the name#

At this stage, we’ll have to do two things: First, move the logo slightly to the left. Second, “pop out” the name of the website after the logo was moved.

To move the logo to the left is simply another <animateMotion> tag chained after the first one in all of the SVG elements. The only addition is the begin attribute within the motion, which in this case is set to 0.35s. The Y value of all three objects (logo, box shadow and text) stays the same. Just the X value for all objects is shifted 60px to the left - considering that the objects start at X 100px, this looks like a good enough shift:

<rect width="512" height="512" style="fill:#000000" x="100" y="0">
    <animateMotion dur="0.25s" values="100,0; 100,30" fill="freeze"></animateMotion>
    <animateMotion begin="0.35s" dur="0.15s" values="100,30;40,30" fill="freeze"></animateMotion>
</rect>
<rect width="512" height="512" style="fill:#fd0054" x="100" y="0">
    <animateMotion dur="0.25s" values="100,0; 70,-20" fill="freeze"></animateMotion>
    <animateMotion begin="0.35s" dur="0.15s" values="70,-20;10,-20" fill="freeze"></animateMotion>
</rect>
<path fill="#ffffff" d="M258.59 177.68L256.77 215.21Q251.8 213.04 245.14 210.86Q238.61 208.55 231.34 206.74Q224.2 204.92 216.81 203.83Q209.42 202.62 202.88 202.62Q191.5 202.62 185.08 205.53Q178.66 208.43 178.66 215.34Q178.66 218.61 180.84 221.15Q183.14 223.57 187.26 225.75Q191.38 227.81 197.07 229.87Q202.88 231.8 209.91 233.98L218.75 236.77Q229.89 240.16 238.73 245Q247.57 249.73 253.62 256.27Q259.8 262.68 263.07 271.04Q266.34 279.39 266.34 290.05Q266.34 302.52 262.34 312.82Q258.46 322.99 250.11 330.25Q241.75 337.52 228.68 341.52Q215.72 345.51 197.43 345.51Q181.21 345.51 165.1 342.85Q149.12 340.18 132.77 333.52L132.77 293.56Q148.39 301.55 163.04 305.31Q177.82 308.94 192.11 308.94Q203.85 308.94 209.18 305.19Q214.63 301.43 214.63 294.77Q214.63 290.78 212.57 287.99Q210.63 285.09 206.15 282.66Q201.79 280.24 194.65 277.82Q187.5 275.28 177.33 272.13Q167.16 269.1 158.44 264.5Q149.84 259.9 143.55 253.48Q137.25 246.94 133.62 238.34Q130.11 229.75 130.11 218.73Q130.11 207.34 134.1 197.9Q138.1 188.45 146.33 181.79Q154.57 175.01 167.16 171.26Q179.88 167.5 197.31 167.5Q204.21 167.5 211.96 168.11Q219.71 168.71 227.71 169.93Q235.82 171.14 243.69 173.07Q251.56 175.01 258.59 177.68ZM324.34 345.51Q313.32 345.51 303.03 343.94Q292.86 342.36 285.59 337.76L285.59 303.25Q293.7 305.79 300.61 306.52Q307.63 307.13 312.71 307.13Q318.16 307.13 321.19 304.95Q324.34 302.64 325.91 299.13Q327.49 295.62 327.85 291.26Q328.21 286.78 328.21 282.42L328.21 170.77L382.46 170.77L382.46 290.05Q382.46 304.58 377.38 315.12Q372.29 325.53 364.06 332.31Q355.95 339.09 345.41 342.36Q335 345.51 324.34 345.51Z" id="SJ">
    <animateMotion dur="0.25s" values="160,0; 140,-20" fill="freeze"></animateMotion>
    <animateMotion begin="0.35s" dur="0.15s" values="140,-20;90,-20" fill="freeze"></animateMotion>
</path>

And finally, we add in the name of the website, including the animation (might have to reload the page to view proper animation):

<!-- rest of svg omitted -->
<text id="text18" style="font-size:100px; font-family: 'Share Tech Mono', monospace;" opacity="0">
	<animateMotion begin="0.35s" dur="0.15s" values="60,0;80,0" fill="freeze"></animateMotion>
	<animate attributeName="opacity" begin="0.35s" to="1" dur="0.15s" fill="freeze"></animate>
	<tspan y="256" x="600">
	    <tspan style="text-transform:uppercase">soeren.codes</tspan>
	</tspan>
</text>

Conclusion#

Creating a proper SVG with animations is not very complicated, and it allows for more flexibility than regular CSS animations or clunky formats such as GIFs.

To embed the final SVG into your GitHub README, simply upload the SVG to the repository and use an <img> tag inside the README.md to reference the newly uploaded SVG. It is best to specify a width inside the SVG (provided the viewBox attribute is set properly) and omitting the height so that it scales properly within the README.

For reference, here is the full SVG:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 527" width="700">
    <defs>
        <style type="text/css">
            @font-face {
                src: url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&amp;display=swap');
            }
        </style>
    </defs>
	<rect width="512" height="512" style="fill:#000000" x="100" y="0">
        <animateMotion dur="0.25s" values="100,0; 100,30" fill="freeze"></animateMotion>
        <animateMotion begin="0.35s" dur="0.15s" values="100,30;40,30" fill="freeze"></animateMotion>
	</rect>
	<rect width="512" height="512" style="fill:#fd0054" x="100" y="0">
        <animateMotion dur="0.25s" values="100,0; 70,-20" fill="freeze"></animateMotion>
        <animateMotion begin="0.35s" dur="0.15s" values="70,-20;10,-20" fill="freeze"></animateMotion>
	</rect>
	<path fill="#ffffff" d="M258.59 177.68L256.77 215.21Q251.8 213.04 245.14 210.86Q238.61 208.55 231.34 206.74Q224.2 204.92 216.81 203.83Q209.42 202.62 202.88 202.62Q191.5 202.62 185.08 205.53Q178.66 208.43 178.66 215.34Q178.66 218.61 180.84 221.15Q183.14 223.57 187.26 225.75Q191.38 227.81 197.07 229.87Q202.88 231.8 209.91 233.98L218.75 236.77Q229.89 240.16 238.73 245Q247.57 249.73 253.62 256.27Q259.8 262.68 263.07 271.04Q266.34 279.39 266.34 290.05Q266.34 302.52 262.34 312.82Q258.46 322.99 250.11 330.25Q241.75 337.52 228.68 341.52Q215.72 345.51 197.43 345.51Q181.21 345.51 165.1 342.85Q149.12 340.18 132.77 333.52L132.77 293.56Q148.39 301.55 163.04 305.31Q177.82 308.94 192.11 308.94Q203.85 308.94 209.18 305.19Q214.63 301.43 214.63 294.77Q214.63 290.78 212.57 287.99Q210.63 285.09 206.15 282.66Q201.79 280.24 194.65 277.82Q187.5 275.28 177.33 272.13Q167.16 269.1 158.44 264.5Q149.84 259.9 143.55 253.48Q137.25 246.94 133.62 238.34Q130.11 229.75 130.11 218.73Q130.11 207.34 134.1 197.9Q138.1 188.45 146.33 181.79Q154.57 175.01 167.16 171.26Q179.88 167.5 197.31 167.5Q204.21 167.5 211.96 168.11Q219.71 168.71 227.71 169.93Q235.82 171.14 243.69 173.07Q251.56 175.01 258.59 177.68ZM324.34 345.51Q313.32 345.51 303.03 343.94Q292.86 342.36 285.59 337.76L285.59 303.25Q293.7 305.79 300.61 306.52Q307.63 307.13 312.71 307.13Q318.16 307.13 321.19 304.95Q324.34 302.64 325.91 299.13Q327.49 295.62 327.85 291.26Q328.21 286.78 328.21 282.42L328.21 170.77L382.46 170.77L382.46 290.05Q382.46 304.58 377.38 315.12Q372.29 325.53 364.06 332.31Q355.95 339.09 345.41 342.36Q335 345.51 324.34 345.51Z" id="SJ">
        <animateMotion dur="0.25s" values="160,0; 140,-20" fill="freeze"></animateMotion>
        <animateMotion begin="0.35s" dur="0.15s" values="140,-20;90,-20" fill="freeze"></animateMotion>
	</path>
	<text id="text18" style="font-size:100px; font-family: 'Share Tech Mono', monospace;" opacity="0">
        <animateMotion begin="0.35s" dur="0.15s" values="60,0;80,0" fill="freeze"></animateMotion>
        <animate attributeName="opacity" begin="0.35s" to="1" dur="0.15s" fill="freeze"></animate>
	<tspan y="256" x="600">
	    
	</tspan>
	</text>
</svg>

Down below, you can find the link to my GitHub profile, where you can see the SVG in action.

© 2020 Søren Johanson