Aduok Code

Animated Pencil Loader with SVG & CSS

Introduction

In this article we build a fully animated pencil loader from scratch — no JavaScript, no libraries. Just a hand-crafted SVG and a handful of CSS keyframe animations. The result is a spinning pencil that draws a circular trail, wobbles its eraser, and narrows to a sharp graphite tip. Perfect as a page loader or an idle state indicator.

The loader is built around one SVG element containing five animated layers: a trail arc, three body rings, an eraser cap with a skew wobble, and a wood-and-graphite tip. Each layer has its own @keyframes block. All class names follow a flat pencil-* naming convention — no double underscores, no extra nesting.

What you will learn

How to animate SVG stroke-dashoffset to draw a path — how to spin an SVG group with CSS transforms — how to use clipPath to clip a rect inside a rounded rect — how to layer multiple circles for a thick body ring with depth — how to build the eraser cap with a skewX wobble — how to construct a sharp pencil tip with polygon shapes — how to use real pencil colors for an authentic look.

01
Part One
The SVG Structure

Overview of the SVG Layers

The entire loader lives inside a single <svg class="pencil"> element with a 200×200 viewBox. Everything is drawn relative to the centre point (100, 100). There are four top-level groups inside the SVG, each mapped to one animation.

Layer order matters. The trail arc sits behind everything. The spinning group contains all the pencil parts and rotates as one unit.

Step 1 — ClipPath for the Eraser

We define a <clipPath> in <defs> so we can clip the dark side stripe of the eraser to the rounded eraser shape. Without it the stripe would bleed outside the rounded corners. The id is namespaced as pencil-cap-clip to keep it consistent with the rest of the class naming.

SVG
<svg class="pencil" viewBox="0 0 200 200"
     xmlns="http://www.w3.org/2000/svg"
     role="img" aria-label="Loading">

  <defs>
    <clipPath id="pencil-cap-clip">
      <rect rx="5" ry="5" width="30" height="30" />
    </clipPath>
  </defs>

  <!-- layers go here -->
</svg>

Step 2 — The Trail Arc

The trail is a single <circle> with a dashed stroke. By animating stroke-dashoffset from the full circumference down to a partial value, we make the arc appear to draw itself. stroke-linecap="round" gives it a natural tapered end.

The circle has r="70", so its circumference is 2π × 70 ≈ 439.82. We set both stroke-dasharray and initial stroke-dashoffset to that value — making the stroke invisible at rest — then animate the offset downward to reveal it. The stroke color is #5a5a5a — a graphite grey that mimics a real pencil mark.

SVG
<circle
  class="pencil-trail"
  r="70"
  fill="none"
  stroke="#5a5a5a"
  stroke-width="2"
  stroke-dasharray="439.82 439.82"
  stroke-dashoffset="439.82"
  stroke-linecap="round"
  transform="rotate(-113,100,100)"
/>
Why rotate(-113)?

The initial rotation offsets the start of the arc so it aligns with the pencil tip position when the animation begins. Without it the trail would start at the wrong angle relative to the pencil body.

02
Part Two
Pencil Body Rings

Step 3 — Three Layered Body Circles

The thick yellow pencil body is made from three concentric circles inside a <g class="pencil-spin"> group that rotates the whole pencil. The three circles have different radii and stroke widths to give the body depth: a wide middle ring, a thin bright outer ring, and a thin dark inner ring.

SVG
<g class="pencil-spin" transform="translate(100,100)">
  <g fill="none">

    <!-- Main body — thick, mid-yellow -->
    <circle class="pencil-shell1" r="64"
      stroke="#F9C22E" stroke-width="30"
      stroke-dasharray="402.12 402.12"
      stroke-dashoffset="402"
      transform="rotate(-90)" />

    <!-- Outer highlight — thin, light yellow -->
    <circle class="pencil-shell2" r="74"
      stroke="#FBDB6A" stroke-width="10"
      stroke-dasharray="464.96 464.96"
      stroke-dashoffset="465"
      transform="rotate(-90)" />

    <!-- Inner shadow — thin, dark yellow -->
    <circle class="pencil-shell3" r="54"
      stroke="#D4981E" stroke-width="10"
      stroke-dasharray="339.29 339.29"
      stroke-dashoffset="339"
      transform="rotate(-90)" />

  </g>
  <!-- cap and tip go here -->
</g>

Each circle's stroke-dasharray equals its own circumference (2π × r). They all start with stroke-dashoffset near that value (mostly hidden) and animate to a shorter offset, revealing the arc segment that forms the visible body.

03
Part Three
The Eraser Cap

Step 4 — Building the Eraser & Metal Ferrule

The eraser sits at one end of the pencil body. It is built from stacked <rect> elements inside two nested groups: pencil-cap handles position & rotation, pencil-cap-tilt handles the skewX wobble animation.

Reading from bottom to top: a pink rounded rect is the eraser body, a dark stripe clipped by #pencil-cap-clip is the shadow side, then three grey rects form the silver metal ferrule, with two thin dark rects as the ferrule grooves.

SVG
<g class="pencil-cap" transform="rotate(-90) translate(49,0)">
  <g class="pencil-cap-tilt">

    <!-- Pink eraser body -->
    <rect fill="#F4A0B0" rx="5" ry="5" width="30" height="30" />

    <!-- Dark side stripe, clipped to rounded shape -->
    <rect fill="#f07a90" width="5" height="30"
          clip-path="url(#pencil-cap-clip)" />

    <!-- Silver ferrule — three tones for depth -->
    <rect fill="#d8d8d8" width="30" height="20" />
    <rect fill="#b0b0b0" width="15" height="20" />
    <rect fill="#c8c8c8" width="5"  height="20" />

    <!-- Ferrule grooves -->
    <rect fill="rgba(0,0,0,0.15)" y="6"  width="30" height="2" />
    <rect fill="rgba(0,0,0,0.15)" y="13" width="30" height="2" />

  </g>
</g>
Why two nested groups?

The outer group pencil-cap moves the entire eraser to the correct position and rotates with the pencil. The inner group pencil-cap-tilt only handles the skewX wobble — splitting the two transforms keeps the animations clean and independent.

04
Part Four
The Pencil Tip

Step 5 — Wood & Graphite Tip

The tip is three <polygon> shapes stacked inside pencil-tip. The first wide polygon is the cedar wood shaving. The second narrower polygon adds a shadow side. The tiny top polygon is the dark graphite core.

SVG
<g class="pencil-tip" transform="rotate(-90) translate(49,-30)">

  <!-- Wood — warm cedar, full triangle -->
  <polygon fill="#C8855A" points="15 0,30 30,0 30" />

  <!-- Shadow side of wood -->
  <polygon fill="#A0613A" points="15 0,6 30,0 30" />

  <!-- Graphite tip — tiny dark triangle at the point -->
  <polygon fill="#2e2e2e" points="15 0,20 10,10 10" />

</g>
05
Part Five
CSS Base Setup

Step 6 — CSS Reset, Variables & Layout

We use CSS custom properties to hold all colors. The body is a flex container that centres the loader. A warm cream #fdf6e3 background mimics notebook paper — perfect for a pencil theme.

CSS
*, *::before, *::after {
  border: 0;
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

:root {
  --surface: #fdf6e3;
  --ink: #2c1a0e;
  font-size: calc(18px + (28 - 18) * (100vw - 320px) / (1280 - 320));
}

body {
  background: var(--surface);
  color: var(--ink);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.pencil {
  display: block;
  width: 9em;
  height: 9em;
  filter: drop-shadow(0 4px 18px rgba(220,160,0,0.18));
}

The drop-shadow filter on .pencil gives the whole SVG a warm golden glow that makes it feel lit from within — a small touch that dramatically lifts the visual quality.

06
Part Six
Keyframe Animations

Step 7 — Wiring Up the Animations

Every animated element shares the same animation-duration, timing-function, and iteration-count. We set these once on a shared selector block, then assign unique animation-name values to each class. All class names use the flat pencil-* prefix — single dash, no double underscores.

CSS
.pencil-shell1,
.pencil-shell2,
.pencil-shell3,
.pencil-cap,
.pencil-cap-tilt,
.pencil-tip,
.pencil-spin,
.pencil-trail {
  animation-duration: 3.5s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

.pencil-shell1 { animation-name: pencilShell1; }
.pencil-shell2 { animation-name: pencilShell2; }
.pencil-shell3 { animation-name: pencilShell3; }

.pencil-cap {
  animation-name: pencilCap;
  transform: rotate(-90deg) translate(49px, 0);
}
.pencil-cap-tilt {
  animation-name: pencilCapTilt;
  animation-timing-function: ease-in-out;
}
.pencil-tip {
  animation-name: pencilTip;
  transform: rotate(-90deg) translate(49px, -30px);
}
.pencil-spin  { animation-name: pencilSpin; }
.pencil-trail {
  animation-name: pencilTrail;
  transform: translate(100px, 100px) rotate(-113deg);
}

Body Ring Keyframes

Each shell animates its stroke-dashoffset (to reveal/hide the arc) and rotates from -90deg to -225deg at the midpoint — creating the feeling of the pencil rolling around the circle.

CSS
@keyframes pencilShell1 {
  from, to {
    stroke-dashoffset: 351.86;
    transform: rotate(-90deg);
  }
  50% {
    stroke-dashoffset: 150.8;
    transform: rotate(-225deg);
  }
}

@keyframes pencilShell2 {
  from, to {
    stroke-dashoffset: 406.84;
    transform: rotate(-90deg);
  }
  50% {
    stroke-dashoffset: 174.36;
    transform: rotate(-225deg);
  }
}

@keyframes pencilShell3 {
  from, to {
    stroke-dashoffset: 296.88;
    transform: rotate(-90deg);
  }
  50% {
    stroke-dashoffset: 127.23;
    transform: rotate(-225deg);
  }
}

Spin Keyframe — Full 720° Rotation

The pencilSpin keyframe rotates the entire pencil group two full turns. Two rotations instead of one keeps the motion feeling like a continuous orbit rather than a single snap.

CSS
@keyframes pencilSpin {
  from { transform: translate(100px, 100px) rotate(0deg); }
  to   { transform: translate(100px, 100px) rotate(720deg); }
}

Tip Keyframe

CSS
@keyframes pencilTip {
  from, to {
    transform: rotate(-90deg) translate(49px, -30px);
  }
  50% {
    transform: rotate(-225deg) translate(49px, -30px);
  }
}

Trail Arc Keyframe

The trail draws in from 0% to 50%, holds briefly, then erases as it sweeps around to its final angle. The rotation at 75%-100% snaps it back to the start position, creating a seamless loop.

CSS
@keyframes pencilTrail {
  from {
    stroke-dashoffset: 439.82;
    transform: translate(100px, 100px) rotate(-113deg);
  }
  50% {
    stroke-dashoffset: 164.93;
    transform: translate(100px, 100px) rotate(-113deg);
  }
  75%, to {
    stroke-dashoffset: 439.82;
    transform: translate(100px, 100px) rotate(112deg);
  }
}

Eraser Cap Keyframes

The cap rotates into position with pencilCap, while pencilCapTilt adds the satisfying skewX wobble — oscillating rapidly between −15deg and +15deg during the middle third of the loop, simulating an eraser being pressed across paper.

CSS
@keyframes pencilCap {
  from, to {
    transform: rotate(-45deg) translate(49px, 0);
  }
  50% {
    transform: rotate(0deg) translate(49px, 0);
  }
}

@keyframes pencilCapTilt {
  from, 32.5%, 67.5%, to     { transform: skewX(0); }
  35%, 65%                   { transform: skewX(-4deg); }
  37.5%, 62.5%               { transform: skewX(8deg); }
  40%, 45%, 50%, 55%, 60%    { transform: skewX(-15deg); }
  42.5%, 47.5%, 52.5%, 57.5% { transform: skewX(15deg); }
}
07
Part Seven
Real Pencil Colors

Step 8 — Choosing Authentic Colors

Using real pencil colors makes the animation immediately recognisable and far more satisfying to watch. The palette is grounded in what an actual No.2 pencil looks like — yellow body, pink eraser, silver ferrule, cedar wood tip, graphite point, and a cream paper background.

PartColorHex
Body (main ring)Classic pencil yellow#F9C22E
Body (outer ring)Light yellow highlight#FBDB6A
Body (inner ring)Dark yellow shadow#D4981E
Eraser bodySoft pink#F4A0B0
Eraser shadow sideDeep pink#f07a90
Ferrule lightSilver#d8d8d8
Ferrule midMid silver#b0b0b0
Ferrule darkDim silver#c8c8c8
Wood (light side)Warm cedar#C8855A
Wood (shadow side)Dark cedar#A0613A
Graphite tipNear black#2e2e2e
Trail arcGraphite grey#5a5a5a
BackgroundCream / paper#fdf6e3

Tuning Reference

All the values you can adjust to customise the loader:

PropertyWhereEffect
animation-duration: 3.5sShared CSS blockIncrease for a slower, heavier feel. Decrease for a snappier spin
width / height: 9em.pencilControls the overall size. Scales with the root font-size formula
stroke-width: 30.pencil-shell1Thickness of the main pencil body ring. Increase for a chunkier pencil
r="64 / 74 / 54"SVG circlesRadii of the three body rings. Keep the gaps proportional when changing
stroke-dashoffset at 50%@keyframes pencilShell1Controls how much of the arc is visible at the halfway point of the loop
skewX(±15deg)@keyframes pencilCapTiltMax wobble angle of the eraser. Increase for a more frantic erase motion
rotate(720deg)@keyframes pencilSpinTwo full rotations per cycle. Change to 360deg for a single slower orbit
drop-shadow filter.pencilRemove entirely for a flat look, or increase blur value for a stronger glow

Full Source Code

Save the following as pencil-loader.html and open it in any browser. Zero dependencies, zero build step.

HTML — pencil-loader.html (complete)
// Full source available in the Aduok GitHub repository
// https://github.com/aduok/aduok-code-snippets/blob/main/blog/pencil-loader.html

A great loader is one users enjoy seeing — make it worth the wait.

Want to see it in action?Watch the full build on YouTube