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.
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.
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 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.
<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)"
/>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.
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.
<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.
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.
<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>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.
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.
<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>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.
*, *::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.
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.
.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.
@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.
@keyframes pencilSpin {
from { transform: translate(100px, 100px) rotate(0deg); }
to { transform: translate(100px, 100px) rotate(720deg); }
}Tip Keyframe
@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.
@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.
@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); }
}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.
| Part | Color | Hex |
|---|---|---|
| Body (main ring) | Classic pencil yellow | #F9C22E |
| Body (outer ring) | Light yellow highlight | #FBDB6A |
| Body (inner ring) | Dark yellow shadow | #D4981E |
| Eraser body | Soft pink | #F4A0B0 |
| Eraser shadow side | Deep pink | #f07a90 |
| Ferrule light | Silver | #d8d8d8 |
| Ferrule mid | Mid silver | #b0b0b0 |
| Ferrule dark | Dim silver | #c8c8c8 |
| Wood (light side) | Warm cedar | #C8855A |
| Wood (shadow side) | Dark cedar | #A0613A |
| Graphite tip | Near black | #2e2e2e |
| Trail arc | Graphite grey | #5a5a5a |
| Background | Cream / paper | #fdf6e3 |
Tuning Reference
All the values you can adjust to customise the loader:
| Property | Where | Effect |
|---|---|---|
| animation-duration: 3.5s | Shared CSS block | Increase for a slower, heavier feel. Decrease for a snappier spin |
| width / height: 9em | .pencil | Controls the overall size. Scales with the root font-size formula |
| stroke-width: 30 | .pencil-shell1 | Thickness of the main pencil body ring. Increase for a chunkier pencil |
| r="64 / 74 / 54" | SVG circles | Radii of the three body rings. Keep the gaps proportional when changing |
| stroke-dashoffset at 50% | @keyframes pencilShell1 | Controls how much of the arc is visible at the halfway point of the loop |
| skewX(±15deg) | @keyframes pencilCapTilt | Max wobble angle of the eraser. Increase for a more frantic erase motion |
| rotate(720deg) | @keyframes pencilSpin | Two full rotations per cycle. Change to 360deg for a single slower orbit |
| drop-shadow filter | .pencil | Remove 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.
// Full source available in the Aduok GitHub repository
// https://github.com/aduok/aduok-code-snippets/blob/main/blog/pencil-loader.htmlA great loader is one users enjoy seeing — make it worth the wait.