Skip to content
CoDdleCoDoodle home
Beginner ProjectsAdvancedSVG + CSS

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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>
Open in Playground

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.