Chalkboard Number Practice
A chalkboard number scene where handwritten math marks appear stroke by stroke like real chalk.
← you, in 45 minutes.
what you'll build
Here's the plan
A cute classroom chalkboard in SVG — wooden frame, green board, chalk tray, eraser, resting chalk, and handwritten numbers that appear stroke by stroke as if a tiny chalk stick is writing them. All motion respects prefers-reduced-motion.
You’ll learn
- Building a framed prop scene with layered rectangles and a small color palette
- Drawing handwritten numbers as SVG paths with thick rounded strokes
- Revealing a path with stroke-dasharray and stroke-dashoffset
- Moving an element along a path with offset-path and offset-distance
- Staggering CSS animations so a sequence reads like real writing
- Skill level
- Advanced
- Time
- ~45 min
- Tools
- Just a browser
- Code type
- SVG + CSS
- Lines
- ~85
the steps
Build it, one change at a time
Build the blackboard base
Start with a soft cream wall, a wooden frame rectangle, and a dark green board inset. The rounded corners and thick navy outline give it the same sticker feel as the rest of CoDoodle.
<svg class="chalkboard-number-practice" viewBox="0 0 230 210" width="230" height="210" role="img" aria-label="Chalkboard number practice board"> <rect x="0" y="0" width="230" height="210" fill="#FFF8EF" stroke="none"/> <rect x="16" y="16" width="198" height="178" rx="10" fill="#C9A06C" stroke="#2B3A55" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/> <rect x="28" y="28" width="174" height="142" rx="4" fill="#2F5A44" stroke="none"/> </svg>a framed chalkboard waiting on a soft classroom wall.
Add chalk tray, eraser, and chalk details
A wooden tray across the bottom, a small pink felt eraser, and a resting white chalk stick make the board feel used. Group transforms let you place the eraser and chalk without recalculating every coordinate.
<!-- chalk tray --> <rect x="22" y="164" width="186" height="20" rx="4" fill="#B8905C" stroke="#2B3A55" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/> <rect x="30" y="168" width="170" height="8" rx="2" fill="#9E7B4F" stroke="none"/> <!-- small felt eraser --> <g transform="translate(44, 158)"> <rect x="0" y="0" width="22" height="12" rx="3" fill="#FF8E8E" stroke="#2B3A55" stroke-width="3.5" stroke-linejoin="round"/> <rect x="0" y="10" width="22" height="4" rx="2" fill="#8B6F4E" stroke="#2B3A55" stroke-width="3.5" stroke-linejoin="round"/> </g> <!-- resting chalk stick --> <g transform="translate(154, 162) rotate(-12)"> <rect x="0" y="0" width="30" height="8" rx="4" fill="#FFFDF7" stroke="#2B3A55" stroke-width="3" stroke-linejoin="round"/> </g>the board now has a tray, eraser, and a piece of chalk at rest.
Draw number strokes as SVG paths
Handwritten numbers are just paths with a chalk-colored stroke and rounded line caps. Keep the paths simple and a little wobbly — perfect circles look like printed text, not chalk. We draw 1 2 3 on top and 4 + 5 = 9 below.
<!-- top row: 1 2 3 --> <g fill="none" stroke="#FFFDF7" stroke-width="4.2" stroke-linecap="round" stroke-linejoin="round" opacity="0.95"> <path d="M 58 64 L 64 60 L 64 92"/> <path d="M 90 66 C 108 60 114 74 98 82 C 112 86 116 94 94 94"/> <path d="M 132 66 C 152 60 148 76 142 80 C 150 84 156 96 132 94"/> </g> <!-- bottom row: 4 + 5 = 9 --> <g fill="none" stroke="#FFFDF7" stroke-width="4.2" stroke-linecap="round" stroke-linejoin="round" opacity="0.95"> <path d="M 52 100 L 42 126 L 60 126 M 48 116 L 64 116"/> <path d="M 92 112 L 108 112 M 100 104 L 100 120"/> <path d="M 120 102 L 140 102 M 122 104 L 122 118 C 122 130 142 126 142 120"/> <path d="M 150 108 L 166 108 M 150 120 L 166 120"/> <path d="M 180 104 C 198 104 198 118 180 118 C 168 118 168 104 180 104 M 190 116 C 190 138 166 138 166 138"/> </g>static chalk writing — the numbers are drawn, but they don't move yet.
Animate chalk writing
The classic trick: set stroke-dasharray to a value larger than the path length and animate stroke-dashoffset from that value down to 0. The stroke seems to draw itself. Wrap the animation in prefers-reduced-motion so the board stays readable for anyone who needs it.
.chalkboard-number-practice .write { stroke-dasharray: 200; stroke-dashoffset: 0; } @media (prefers-reduced-motion: no-preference) { .chalkboard-number-practice .write { stroke-dashoffset: 200; animation: chalk-write 1.3s ease-in-out forwards; } } @keyframes chalk-write { to { stroke-dashoffset: 0; } }every number draws itself at the same time — a busy first lesson.
Add chalk dust details
Tiny white circles near stroke endpoints fade in and out while the chalk is writing. They live in the same animation layer as the strokes, but with a later delay so they puff only after the tip arrives.
.chalkboard-number-practice .dust { opacity: 0; } @media (prefers-reduced-motion: no-preference) { .chalkboard-number-practice .dust { animation: chalk-dust 2.2s ease-in-out infinite; } } @keyframes chalk-dust { 0%, 100% { opacity: 0; transform: scale(0.4); } 50% { opacity: 0.55; transform: scale(1); } }small chalk puffs now appear at the ends of the strokes.
Coordinate the writing timing
Give each number its own animation-delay so the strokes write in order, not all at once. A small chalk rectangle follows each path using offset-path and offset-distance. The chalk fades out at the end of each stroke so only the writing remains.
/* sequential delays: 1 2 3, pause, then 4 + 5 = 9 */ .chalkboard-number-practice .num-1, .chalkboard-number-practice .c-1 { animation-delay: 0.2s; } .chalkboard-number-practice .num-2, .chalkboard-number-practice .c-2 { animation-delay: 1.7s; } .chalkboard-number-practice .num-3, .chalkboard-number-practice .c-3 { animation-delay: 3.2s; } .chalkboard-number-practice .num-4, .chalkboard-number-practice .c-4 { animation-delay: 5.2s; } .chalkboard-number-practice .plus, .chalkboard-number-practice .c-plus { animation-delay: 6.7s; } .chalkboard-number-practice .num-5, .chalkboard-number-practice .c-5 { animation-delay: 8.2s; } .chalkboard-number-practice .equals, .chalkboard-number-practice .c-equals { animation-delay: 9.7s; } .chalkboard-number-practice .num-9, .chalkboard-number-practice .c-9 { animation-delay: 11.2s; } /* chalk pieces ride the same paths as the strokes */ .chalkboard-number-practice .c-1 { offset-path: path('M 58 64 L 64 60 L 64 92'); } .chalkboard-number-practice .c-2 { offset-path: path('M 90 66 C 108 60 114 74 98 82 C 112 86 116 94 94 94'); } .chalkboard-number-practice .c-3 { offset-path: path('M 132 66 C 152 60 148 76 142 80 C 150 84 156 96 132 94'); } .chalkboard-number-practice .c-4 { offset-path: path('M 52 100 L 42 126 L 60 126 M 48 116 L 64 116'); } .chalkboard-number-practice .c-plus { offset-path: path('M 92 112 L 108 112 M 100 104 L 100 120'); } .chalkboard-number-practice .c-5 { offset-path: path('M 120 102 L 140 102 M 122 104 L 122 118 C 122 130 142 126 142 120'); } .chalkboard-number-practice .c-equals { offset-path: path('M 150 108 L 166 108 M 150 120 L 166 120'); } .chalkboard-number-practice .c-9 { offset-path: path('M 180 104 C 198 104 198 118 180 118 C 168 118 168 104 180 104 M 190 116 C 190 138 166 138 166 138'); }the chalk now writes each number in order, one tiny piece at a time.
Remix ideas
The finished doodle adds tiny chalk stars and a smiley accent that sparkle softly. From here you can change the sum, add a clock above the board, draw letters instead of numbers, or make the eraser wiggle like it's about to clean the board.
/* tiny chalk accent sparkle */ @media (prefers-reduced-motion: no-preference) { .chalkboard-number-practice path[stroke-width="2.8"] { animation: accent-sparkle 2.8s ease-in-out infinite; } } @keyframes accent-sparkle { 0%, 100% { opacity: 0.55; } 50% { opacity: 0.9; } }the finished chalkboard — numbers, math, dust, and a little sparkle.
the complete code
Everything, in one place
Skipped straight to the end? Welcome. Copy the whole thing, download it, or open it in the Playground to start remixing.
<svg class="chalkboard-number-practice" viewBox="0 0 230 210" width="230" height="210" role="img" aria-label="Animated chalkboard number practice">
<!-- soft classroom wall -->
<rect x="0" y="0" width="230" height="210" fill="#FFF8EF" stroke="none"/>
<!-- wooden frame -->
<rect x="16" y="16" width="198" height="178" rx="10" fill="#C9A06C" stroke="#2B3A55" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<!-- dark green chalkboard surface -->
<rect x="28" y="28" width="174" height="142" rx="4" fill="#2F5A44" stroke="none"/>
<!-- subtle inner chalkboard sheen -->
<rect x="34" y="34" width="162" height="130" rx="2" fill="#FFFFFF" opacity="0.04" stroke="none"/>
<!-- chalk tray -->
<rect x="22" y="164" width="186" height="20" rx="4" fill="#B8905C" stroke="#2B3A55" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="30" y="168" width="170" height="8" rx="2" fill="#9E7B4F" stroke="none"/>
<!-- small felt eraser -->
<g transform="translate(44, 158)">
<rect x="0" y="0" width="22" height="12" rx="3" fill="#FF8E8E" stroke="#2B3A55" stroke-width="3.5" stroke-linejoin="round"/>
<rect x="0" y="10" width="22" height="4" rx="2" fill="#8B6F4E" stroke="#2B3A55" stroke-width="3.5" stroke-linejoin="round"/>
</g>
<!-- resting chalk stick -->
<g transform="translate(154, 162) rotate(-12)">
<rect x="0" y="0" width="30" height="8" rx="4" fill="#FFFDF7" stroke="#2B3A55" stroke-width="3" stroke-linejoin="round"/>
<rect x="2" y="2" width="4" height="4" rx="2" fill="#2B3A55" opacity="0.12" stroke="none"/>
</g>
<!-- top row: 1 2 3 -->
<g class="numbers" stroke="#FFFDF7" stroke-width="4.2" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke-dasharray="200" stroke-dashoffset="0">
<path class="write num-1" d="M 58 64 L 64 60 L 64 92"/>
<path class="write num-2" d="M 90 66 C 108 60 114 74 98 82 C 112 86 116 94 94 94"/>
<path class="write num-3" d="M 132 66 C 152 60 148 76 142 80 C 150 84 156 96 132 94"/>
</g>
<!-- bottom row: 4 + 5 = 9 -->
<g class="numbers" stroke="#FFFDF7" stroke-width="4.2" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke-dasharray="200" stroke-dashoffset="0">
<path class="write num-4" d="M 52 100 L 42 126 L 60 126 M 48 116 L 64 116"/>
<path class="write plus" d="M 92 112 L 108 112 M 100 104 L 100 120"/>
<path class="write num-5" d="M 120 102 L 140 102 M 122 104 L 122 118 C 122 130 142 126 142 120"/>
<path class="write equals" d="M 150 108 L 166 108 M 150 120 L 166 120"/>
<path class="write num-9" d="M 180 104 C 198 104 198 118 180 118 C 168 118 168 104 180 104 M 190 116 C 190 138 166 138 166 138"/>
</g>
<!-- tiny chalk dust puffs -->
<g class="dust-group" fill="#FFFDF7" stroke="none">
<circle class="dust d-1" cx="64" cy="92" r="2" opacity="0"/>
<circle class="dust d-2" cx="94" cy="94" r="2.2" opacity="0"/>
<circle class="dust d-3" cx="132" cy="94" r="2" opacity="0"/>
<circle class="dust d-4" cx="60" cy="126" r="2.2" opacity="0"/>
<circle class="dust d-5" cx="108" cy="120" r="2" opacity="0"/>
<circle class="dust d-9" cx="166" cy="138" r="2.2" opacity="0"/>
</g>
<!-- tiny doodle accents drawn in chalk -->
<g fill="none" stroke="#FFFDF7" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round" opacity="0.7">
<!-- little star top-left -->
<path d="M 44 46 L 46 52 L 52 54 L 46 56 L 44 62 L 42 56 L 36 54 L 42 52 Z"/>
<!-- tiny smiley bottom-right -->
<circle cx="182" cy="144" r="7" opacity="0.65"/>
<path d="M 179 143 Q 182 145 185 143" opacity="0.65"/>
</g>
<!-- moving chalk pieces that trace each number -->
<g class="writing-chalks" fill="#FFFDF7" stroke="#2B3A55" stroke-width="2.2" stroke-linejoin="round" opacity="0">
<rect class="chalk c-1" x="-5" y="-3" width="12" height="7" rx="3.5"/>
<rect class="chalk c-2" x="-5" y="-3" width="12" height="7" rx="3.5"/>
<rect class="chalk c-3" x="-5" y="-3" width="12" height="7" rx="3.5"/>
<rect class="chalk c-4" x="-5" y="-3" width="12" height="7" rx="3.5"/>
<rect class="chalk c-plus" x="-5" y="-3" width="12" height="7" rx="3.5"/>
<rect class="chalk c-5" x="-5" y="-3" width="12" height="7" rx="3.5"/>
<rect class="chalk c-equals" x="-5" y="-3" width="12" height="7" rx="3.5"/>
<rect class="chalk c-9" x="-5" y="-3" width="12" height="7" rx="3.5"/>
</g>
</svg>
tips & gotchas
Mistakes we actually made
If a stroke doesn't fully disappear before writing, raise the stroke-dasharray value until it covers the whole path length.
offset-path works best with simple paths; if the chalk drifts, check the path string matches the stroke path exactly.
Keep the board strokes white or cream (#FFFDF7) with a slight opacity so they look like chalk, not paint.
Use prefers-reduced-motion: no-preference around every decorative motion so the board stays still for anyone who prefers that.
make it yours
Remix it
- New lessoneasy
Replace the numbers with A B C or a short word like 'hello' using the same path technique.
- Different boardeasy
Swap the green board for a black slate (#1A2332) and use a lighter chalk color.
- Eraser wigglemedium
Add a tiny keyframe that rotates the eraser a few degrees, as if someone just picked it up.
- Spelling beemedium
Animate a full word path by chaining several letters with staggered animation-delay values.
challenge extension
Same chalkboard, new subject: animate the word 'CODE' being written in chalk, with each letter using its own stroke-dashoffset reveal.