Tufted Heart Rug
A plush coral heart rug with wiggly fuzzy edges, a soft inner ring, and a tiny tufting gun resting beside it. It is cozy craft-corner energy — all yarn snips, paper warmth, and gentle shimmer.
← you, in 25 minutes.
what you'll build
Here's the plan
A plump coral heart rug lying on warm paper, with a lighter inner heart ring, hand-drawn pile ticks, and fuzzy fringe doing the main-character work. A tiny tufting gun sits beside it — cream body, navy ink, a few yarn snips — so the whole thing feels like your craft corner got very cute on purpose.
You’ll learn
- Layering an SVG scene from floor props to rug silhouette to tiny craft tools
- Drawing a fuzzy edge with short repeated strokes that follow a larger shape
- Using a second heart path as an inner pile ring instead of trying to outline text or filters
- Animating textile details gently with transform and opacity only, wrapped in reduced-motion support
- Skill level
- Beginner
- Time
- ~25 min
- Tools
- Just a browser
- Code type
- SVG + CSS
- Lines
- ~81
the steps
Build it, one change at a time
Set the craft floor
Start with warm paper, one soft oval shadow where the rug will land, and a few yarn snips and sparkles around the edges. The shadow stays separate from the rug so the finished textile can shimmer while the floor stays calm.
<rect x="0" y="0" width="200" height="200" rx="18" fill="#FBF2E5" stroke="none"/> <ellipse class="tr-shadow" cx="101" cy="154" rx="58" ry="12" fill="#2B3A55" opacity="0.14" stroke="none"/> <!-- tiny craft scraps around the workspace --> <path class="tr-spark tr-spark1" d="M31 54 L35 62 L31 70 L27 62 Z" fill="#FFD84D" stroke="#2B3A55" stroke-width="2" stroke-linejoin="round"/> <path class="tr-spark tr-spark2" d="M164 52 L168 59 L164 66 L160 59 Z" fill="#FF8FA3" stroke="none"/> <path class="tr-snip tr-snip1" d="M37 136 l10 -4" fill="none" stroke="#FF6B5E" stroke-width="3" stroke-linecap="round"/> <path class="tr-snip tr-snip2" d="M44 146 l7 5" fill="none" stroke="#2B3A55" stroke-width="2.5" stroke-linecap="round" opacity="0.75"/> <circle class="tr-spark tr-dot1" cx="153" cy="36" r="3" fill="#FFD84D" stroke="none"/> <circle class="tr-spark tr-dot2" cx="171" cy="133" r="2.5" fill="#FF6B5E" stroke="none"/>paper down, shadow placed, yarn bits already escaping.
Block the heart rug
Draw one smooth heart silhouette first: flat coral fill, chunky navy outline, no fuzz yet. This gives you a clean base shape before the fringe and pile texture start making it look handmade.
<g class="tr-rug"> <!-- smooth first: the fuzzy edge comes in the next pass --> <path class="tr-heart" d="M100 157 C86 143 56 130 47 107 C38 82 57 68 76 76 C88 81 94 93 100 103 C106 93 112 81 124 76 C143 68 162 82 153 107 C144 130 114 143 100 157 Z" fill="#FF6B5E" stroke="#2B3A55" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/> </g>a bare coral heart rug — soft, but not fuzzy yet.
Tuft the fuzzy details
Now the rug earns its texture: short fringe strokes all around, a wobbly outline over the smooth one, a soft pink inner heart ring, and small pile ticks across the surface. The tufting gun is just an L-shaped cream tool with navy outlines — enough detail to read instantly without stealing the rug's spotlight.
<!-- fringe groups follow the outside edge so they can wiggle in staggered sets later --> <g class="tr-fringe tr-fringe-a" fill="none" stroke="#2B3A55" stroke-width="3" stroke-linecap="round"> <path d="M50 98 l-9 -4"/> <path d="M50 115 l-10 2"/> <path d="M58 130 l-8 6"/> <path d="M72 141 l-4 9"/> <path d="M91 151 l-1 10"/> <path d="M109 151 l1 10"/> </g> <g class="tr-fringe tr-fringe-b" fill="none" stroke="#2B3A55" stroke-width="3" stroke-linecap="round"> <path d="M128 141 l4 9"/> <path d="M142 130 l8 6"/> <path d="M150 115 l10 2"/> <path d="M150 98 l9 -4"/> <path d="M136 79 l6 -7"/> <path d="M118 79 l2 -10"/> </g> <g class="tr-fringe tr-fringe-c" fill="none" stroke="#2B3A55" stroke-width="2.6" stroke-linecap="round" opacity="0.86"> <path d="M82 79 l-2 -10"/> <path d="M64 79 l-6 -7"/> <path d="M43 105 l-10 -1"/> <path d="M157 105 l10 -1"/> <path d="M100 158 l0 11"/> </g> <path class="tr-wobble" d="M100 157 C86 144 57 131 48 108 C39 84 58 69 75 77 C88 82 94 94 100 103 C106 94 113 82 125 77 C142 69 161 84 152 108 C143 131 114 144 100 157 Z" fill="none" stroke="#2B3A55" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/> <path class="tr-inner-ring" d="M100 139 C91 130 72 122 67 108 C62 94 73 86 84 91 C92 94 96 103 100 109 C104 103 108 94 116 91 C127 86 138 94 133 108 C128 122 109 130 100 139 Z" fill="none" stroke="#FF8FA3" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" opacity="0.72"/> <!-- tiny pile ticks make the flat fill feel tufted --> <g class="tr-pile" fill="none" stroke-linecap="round"> <path d="M77 106 l9 -2" stroke="#FFFDF7" stroke-width="2.2" opacity="0.7"/> <path d="M92 93 l5 7" stroke="#FFFDF7" stroke-width="2" opacity="0.55"/> <path d="M118 105 l8 -4" stroke="#FFFDF7" stroke-width="2" opacity="0.55"/> <path d="M78 125 l8 4" stroke="#FFD84D" stroke-width="2" opacity="0.46"/> <path d="M111 130 l10 2" stroke="#FFFDF7" stroke-width="2.2" opacity="0.58"/> <path d="M103 119 l5 -4" stroke="#2B3A55" stroke-width="2" opacity="0.22"/> <path d="M62 111 l7 3" stroke="#FFFDF7" stroke-width="1.8" opacity="0.5"/> <path d="M133 119 l6 -5" stroke="#FFFDF7" stroke-width="1.8" opacity="0.5"/> </g> <!-- a little L-shaped tufting gun, parked beside the rug --> <g class="tr-tool" transform="rotate(-8 158 148)" stroke="#2B3A55" stroke-linecap="round" stroke-linejoin="round"> <path d="M143 128 H167 C170 128 172 130 172 133 V140 H158 V157 H148 V140 H143 C140 140 138 138 138 134 V133 C138 130 140 128 143 128 Z" fill="#FFFDF7" stroke-width="3"/> <path d="M158 140 h13" fill="none" stroke-width="3"/> <path d="M148 141 c4 4 5 9 3 14" fill="none" stroke-width="2.4"/> <path d="M171 133 l9 -2" fill="none" stroke-width="3"/> <circle cx="145" cy="134" r="2.5" fill="#FFD84D" stroke="none"/> </g> <g class="tr-extra-snips" fill="none" stroke-linecap="round"> <path class="tr-snip tr-snip3" d="M166 161 l8 4" stroke="#FF8FA3" stroke-width="3"/> <path class="tr-snip tr-snip4" d="M154 171 l7 -5" stroke="#2B3A55" stroke-width="2.5" opacity="0.7"/> <circle class="tr-spark tr-dot3" cx="178" cy="151" r="2.5" fill="#FFD84D" stroke="none"/> </g>fringe, pile, tool, snips — the craft table has entered the chat.
Make the pile shimmer
The motion stays textile-soft: the inner ring quietly scales and fades, fringe groups wiggle in tiny staggered loops, and the props twinkle with opacity and scale only. Everything lives inside @media (prefers-reduced-motion: no-preference), so the rug politely becomes a still illustration when motion is reduced.
@media (prefers-reduced-motion: no-preference) { .tr .tr-inner-ring { transform-box: fill-box; transform-origin: center; animation: tr-pile-shimmer 3.2s ease-in-out infinite; } .tr .tr-fringe { transform-box: fill-box; transform-origin: center; } .tr .tr-fringe-a { animation: tr-fringe-a 3.8s ease-in-out infinite; } .tr .tr-fringe-b { animation: tr-fringe-b 3.8s ease-in-out 0.45s infinite; } .tr .tr-fringe-c { animation: tr-fringe-c 3.8s ease-in-out 0.9s infinite; } .tr .tr-spark, .tr .tr-snip { transform-box: fill-box; transform-origin: center; } .tr .tr-spark1 { animation: tr-twinkle 2.8s ease-in-out infinite; } .tr .tr-spark2 { animation: tr-twinkle 2.8s ease-in-out 0.6s infinite; } .tr .tr-dot1 { animation: tr-twinkle 3.1s ease-in-out 1s infinite; } .tr .tr-dot2 { animation: tr-twinkle 3.1s ease-in-out 1.5s infinite; } .tr .tr-dot3 { animation: tr-twinkle 3.1s ease-in-out 2s infinite; } .tr .tr-snip1 { animation: tr-snip-glint 3.6s ease-in-out 0.2s infinite; } .tr .tr-snip3 { animation: tr-snip-glint 3.6s ease-in-out 1.2s infinite; } } @keyframes tr-pile-shimmer { 0%, 100% { opacity: 0.62; transform: scale(1); } 50% { opacity: 0.92; transform: scale(1.025); } } @keyframes tr-fringe-a { 0%, 100% { transform: rotate(0deg) translateY(0); } 50% { transform: rotate(-1.6deg) translateY(-1px); } } @keyframes tr-fringe-b { 0%, 100% { transform: rotate(0deg) translateY(0); } 50% { transform: rotate(1.4deg) translateY(1px); } } @keyframes tr-fringe-c { 0%, 100% { transform: rotate(0deg) translateX(0); } 50% { transform: rotate(-1deg) translateX(1px); } } @keyframes tr-twinkle { 0%, 100% { opacity: 0.35; transform: scale(0.85); } 45% { opacity: 1; transform: scale(1.12); } } @keyframes tr-snip-glint { 0%, 100% { opacity: 0.55; transform: scale(1); } 45% { opacity: 1; transform: scale(1.08); } }a fuzzy little rug doing its softest possible shimmer.
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="tr" viewBox="0 0 200 200" width="200" height="200" role="img" aria-label="A fuzzy coral heart-shaped tufted rug gently shimmering with wiggling fringe and twinkling yarn snips">
<rect x="0" y="0" width="200" height="200" rx="18" fill="#FBF2E5" stroke="none"/>
<ellipse class="tr-shadow" cx="101" cy="154" rx="58" ry="12" fill="#2B3A55" opacity="0.14" stroke="none"/>
<path class="tr-spark tr-spark1" d="M31 54 L35 62 L31 70 L27 62 Z" fill="#FFD84D" stroke="#2B3A55" stroke-width="2" stroke-linejoin="round"/>
<path class="tr-spark tr-spark2" d="M164 52 L168 59 L164 66 L160 59 Z" fill="#FF8FA3" stroke="none"/>
<path class="tr-snip tr-snip1" d="M37 136 l10 -4" fill="none" stroke="#FF6B5E" stroke-width="3" stroke-linecap="round"/>
<path class="tr-snip tr-snip2" d="M44 146 l7 5" fill="none" stroke="#2B3A55" stroke-width="2.5" stroke-linecap="round" opacity="0.75"/>
<circle class="tr-spark tr-dot1" cx="153" cy="36" r="3" fill="#FFD84D" stroke="none"/>
<circle class="tr-spark tr-dot2" cx="171" cy="133" r="2.5" fill="#FF6B5E" stroke="none"/>
<g class="tr-rug">
<path class="tr-heart" d="M100 157 C86 143 56 130 47 107 C38 82 57 68 76 76 C88 81 94 93 100 103 C106 93 112 81 124 76 C143 68 162 82 153 107 C144 130 114 143 100 157 Z" fill="#FF6B5E" stroke="#2B3A55" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<g class="tr-fringe tr-fringe-a" fill="none" stroke="#2B3A55" stroke-width="3" stroke-linecap="round">
<path d="M50 98 l-9 -4"/>
<path d="M50 115 l-10 2"/>
<path d="M58 130 l-8 6"/>
<path d="M72 141 l-4 9"/>
<path d="M91 151 l-1 10"/>
<path d="M109 151 l1 10"/>
</g>
<g class="tr-fringe tr-fringe-b" fill="none" stroke="#2B3A55" stroke-width="3" stroke-linecap="round">
<path d="M128 141 l4 9"/>
<path d="M142 130 l8 6"/>
<path d="M150 115 l10 2"/>
<path d="M150 98 l9 -4"/>
<path d="M136 79 l6 -7"/>
<path d="M118 79 l2 -10"/>
</g>
<g class="tr-fringe tr-fringe-c" fill="none" stroke="#2B3A55" stroke-width="2.6" stroke-linecap="round" opacity="0.86">
<path d="M82 79 l-2 -10"/>
<path d="M64 79 l-6 -7"/>
<path d="M43 105 l-10 -1"/>
<path d="M157 105 l10 -1"/>
<path d="M100 158 l0 11"/>
</g>
<path class="tr-wobble" d="M100 157 C86 144 57 131 48 108 C39 84 58 69 75 77 C88 82 94 94 100 103 C106 94 113 82 125 77 C142 69 161 84 152 108 C143 131 114 144 100 157 Z" fill="none" stroke="#2B3A55" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tr-inner-ring" d="M100 139 C91 130 72 122 67 108 C62 94 73 86 84 91 C92 94 96 103 100 109 C104 103 108 94 116 91 C127 86 138 94 133 108 C128 122 109 130 100 139 Z" fill="none" stroke="#FF8FA3" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" opacity="0.72"/>
<g class="tr-pile" fill="none" stroke-linecap="round">
<path d="M77 106 l9 -2" stroke="#FFFDF7" stroke-width="2.2" opacity="0.7"/>
<path d="M92 93 l5 7" stroke="#FFFDF7" stroke-width="2" opacity="0.55"/>
<path d="M118 105 l8 -4" stroke="#FFFDF7" stroke-width="2" opacity="0.55"/>
<path d="M78 125 l8 4" stroke="#FFD84D" stroke-width="2" opacity="0.46"/>
<path d="M111 130 l10 2" stroke="#FFFDF7" stroke-width="2.2" opacity="0.58"/>
<path d="M103 119 l5 -4" stroke="#2B3A55" stroke-width="2" opacity="0.22"/>
<path d="M62 111 l7 3" stroke="#FFFDF7" stroke-width="1.8" opacity="0.5"/>
<path d="M133 119 l6 -5" stroke="#FFFDF7" stroke-width="1.8" opacity="0.5"/>
</g>
<g class="tr-tool" transform="rotate(-8 158 148)" stroke="#2B3A55" stroke-linecap="round" stroke-linejoin="round">
<path d="M143 128 H167 C170 128 172 130 172 133 V140 H158 V157 H148 V140 H143 C140 140 138 138 138 134 V133 C138 130 140 128 143 128 Z" fill="#FFFDF7" stroke-width="3"/>
<path d="M158 140 h13" fill="none" stroke-width="3"/>
<path d="M148 141 c4 4 5 9 3 14" fill="none" stroke-width="2.4"/>
<path d="M171 133 l9 -2" fill="none" stroke-width="3"/>
<circle cx="145" cy="134" r="2.5" fill="#FFD84D" stroke="none"/>
</g>
<g class="tr-extra-snips" fill="none" stroke-linecap="round">
<path class="tr-snip tr-snip3" d="M166 161 l8 4" stroke="#FF8FA3" stroke-width="3"/>
<path class="tr-snip tr-snip4" d="M154 171 l7 -5" stroke="#2B3A55" stroke-width="2.5" opacity="0.7"/>
<circle class="tr-spark tr-dot3" cx="178" cy="151" r="2.5" fill="#FFD84D" stroke="none"/>
</g>
</g>
</svg>
tips & gotchas
Mistakes we actually made
I like drawing the smooth heart first, because it keeps the rug readable before the fringe gets delightfully messy.
I keep fringe strokes short and uneven — too many long strokes start looking like sun rays instead of yarn pile.
I put transform-box: fill-box on the animating details so each tiny wiggle feels anchored to its own little patch of fabric.
I make the tufting gun smaller than my first instinct; it is a prop, and the fuzzy edge is the star.
make it yours
Remix it
- Change the yarneasy
Swap the coral fill for sunshine, mint, or cream while keeping the navy outline and pink inner ring.
- Add a monogrameasy
Place one chunky SVG letter inside the heart and give it the same soft shimmer as the inner ring.
- Make it a flower rugmedium
Replace the heart silhouette with five rounded petals, then move the fringe strokes around the new outline.
- Thread trailmedium
Draw one loose yarn path from the tufting gun to the rug and animate its opacity like a slow highlight.
challenge extension
Turn the craft corner into a tiny rug wall: add two smaller tufted shapes behind the heart, then stagger their shimmer timings so the whole display feels handmade instead of synchronized.