<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kaomojikan</title>
    <description>The latest articles on DEV Community by Kaomojikan (@kaomojikan).</description>
    <link>https://dev.to/kaomojikan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3986912%2F91cc57c7-5eac-4659-8e7d-2effe18e11d8.jpg</url>
      <title>DEV Community: Kaomojikan</title>
      <link>https://dev.to/kaomojikan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kaomojikan"/>
    <language>en</language>
    <item>
      <title>I gave my side project a mascot: she follows your cursor, blinks, and breathes (plain SVG, no libraries)</title>
      <dc:creator>Kaomojikan</dc:creator>
      <pubDate>Tue, 16 Jun 2026 08:45:08 +0000</pubDate>
      <link>https://dev.to/kaomojikan/i-gave-my-side-project-a-mascot-she-follows-your-cursor-blinks-and-breathes-plain-svg-no-3ec6</link>
      <guid>https://dev.to/kaomojikan/i-gave-my-side-project-a-mascot-she-follows-your-cursor-blinks-and-breathes-plain-svg-no-3ec6</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally posted on &lt;a href="https://kaomojikan.com/en/blog/eye-tracking-mascot-vanilla-svg" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I've loved Japanese kaomoji for years. The little upright faces like &lt;code&gt;(｡•ᴗ•｡)&lt;/code&gt; and &lt;code&gt;(っ´ω`c)&lt;/code&gt;, not the sideways &lt;code&gt;:-)&lt;/code&gt;. They warm up a message just a little, and that always stuck with me.&lt;/p&gt;

&lt;p&gt;A while back I went looking for a good place to grab them, and honestly I couldn't find one I liked. There are plenty of sites, but most of them feel old. Cramped pages, tiny text, ads everywhere, a stray link sitting right where you meant to tap. The kaomoji were cute. Using the sites was not.&lt;/p&gt;

&lt;p&gt;So I figured I'd just build my own. But a straight copy of what already existed felt pointless. Same list, fewer ads, who cares about that.&lt;/p&gt;

&lt;p&gt;Then I thought about the small shops I used to wander into in Japan. The quiet, warm ones, each with its own little personality. You step inside and your shoulders drop, and somehow half an hour goes by. That was the feeling I wanted. Not a database, but a little shop you can walk into, with someone actually in it.&lt;/p&gt;

&lt;p&gt;So I put someone in it. A small 看板娘 (&lt;em&gt;kanban-musume&lt;/em&gt;), the kind of shop girl who says hi when you come in. I call her Nyamoji. She watches your cursor, blinks when she feels like it, and even sort of breathes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feeveejd7sor7pst48mxp.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feeveejd7sor7pst48mxp.gif" alt="kaomojikan-showcase" width="720" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the thing, though: none of it is hard. It's plain SVG, one &lt;code&gt;pointermove&lt;/code&gt;, and a bit of CSS. The tricky part was getting her to stop looking dead. Let me walk you through the bits that actually mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  One face, swapped parts
&lt;/h2&gt;

&lt;p&gt;She has sixteen expressions, and they change depending on the page. But her outline and her cheeks never change. So I draw the base once, and only swap out the eyes and the mouth. An expression is really just data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Each face only carries eyes + mouth. The base SVG is shared.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FACES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;eyes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="na"&gt;mouth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SMILE&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;cry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;eyes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TEARY_EYES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mouth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WAVER&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;sing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;eyes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SING_EYES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;mouth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NOTE&lt;/span&gt;  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...16 total&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want a new expression? Add an eyes path and a mouth path. I never redraw the whole face. It's the cheapest decision in the whole project, and it kept paying off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Following the cursor
&lt;/h2&gt;

&lt;p&gt;This is the part I cared about most.&lt;/p&gt;

&lt;p&gt;The page listens to a single &lt;code&gt;pointermove&lt;/code&gt;. Every time the cursor moves, I work out the direction from the center of her face to the pointer, and nudge just her eyes that way:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcn4ljjpupixo28zc7cwe.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcn4ljjpupixo28zc7cwe.gif" alt="The mascot's eyes following the cursor as it moves around her, with an occasional blink" width="799" height="511"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;follow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hypot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;eyes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transform&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`translate(&lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;MAX&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things I got wrong the first time.&lt;/p&gt;

&lt;p&gt;First, that &lt;code&gt;0.85&lt;/code&gt; on the vertical. If her eyes travel up and down as far as they go side to side, she looks like her eyes are rolling back into her head. It's honestly a little creepy. Pulling the vertical in a bit keeps her looking at you instead of through you.&lt;/p&gt;

&lt;p&gt;Second, the eyes group has &lt;code&gt;transition: transform .14s ease-out&lt;/code&gt; on it. Without that, the eyes jump from spot to spot and it feels robotic. Give them a seventh of a second to catch up and they glide, and suddenly it reads like she's actually watching you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blinking, never in sync
&lt;/h2&gt;

&lt;p&gt;Blinking is just CSS. I squash her eyes flat for a single instant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;m-blink&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%,&lt;/span&gt; &lt;span class="err"&gt;93&lt;/span&gt;&lt;span class="o"&gt;%,&lt;/span&gt; &lt;span class="err"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scaleY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;96&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;5&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;         &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scaleY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the part that surprised me. There's a mascot up in the header, and another one down in the page. The first time I shipped this, every one of them blinked on the exact same frame. The whole page sort of twitched at once. It looked like a bug, not a living thing.&lt;/p&gt;

&lt;p&gt;The fix is one line. Give each mascot a random delay when it gets drawn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;blink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animationDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;4.8&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;s&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now they each blink on their own little schedule, and she feels a lot more alive. Breathing works the same way: a slow scale from &lt;code&gt;1.0&lt;/code&gt; to &lt;code&gt;1.025&lt;/code&gt; over five seconds, also offset so they're never in lockstep.&lt;/p&gt;

&lt;h2&gt;
  
  
  The white dot
&lt;/h2&gt;

&lt;p&gt;There's a tiny white dot in each eye. A catchlight.&lt;/p&gt;

&lt;p&gt;That one dot is the whole difference between alive and dead. Take it away and her eyes look like buttons. Put it back and she's looking right at you. If I could keep only one detail from this whole post, I'd keep the dot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reduced motion
&lt;/h2&gt;

&lt;p&gt;Not everyone enjoys movement on a page, and that's fair. So when &lt;code&gt;prefers-reduced-motion&lt;/code&gt; is set to &lt;code&gt;reduce&lt;/code&gt;, I stop all of it. No blinking, no breathing, no cursor following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-reduced-motion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.m-blink&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.mascot&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-breathe&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nt"&gt;svg&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.m-eyes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cute movement is a bonus, not the point. I didn't want it to be a tax on anyone, so this went in early rather than as an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that cost me the most: an empty span shakes the page
&lt;/h2&gt;

&lt;p&gt;Let me end on the one that really got me.&lt;/p&gt;

&lt;p&gt;Her face gets drawn after the JS loads. At first I used an empty &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; as the placeholder. The moment the JS dropped her face in, that span popped open to full size and the whole page jumped &lt;code&gt;(╯°□°)╯︵ ┻━┻&lt;/code&gt;. On the header logo it was even worse: flicking between pages, you'd catch a glimpse of the empty shell.&lt;/p&gt;

&lt;p&gt;The fix was boring, but it worked. Write her face into the HTML from the start, and make it match what the JS draws, right down to the character. Then when the JS takes over, nothing moves at all.&lt;/p&gt;

&lt;p&gt;One warning. If you copy that placeholder by hand, it drifts out of sync sooner or later, and you get a little jump when the JS kicks in. So I just call the same function that builds her face, and paste its output straight into the HTML. Don't try to write it yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  That's it
&lt;/h2&gt;

&lt;p&gt;That's really all of it. Plain SVG, &lt;code&gt;pointermove&lt;/code&gt;, and some CSS. The 0.85 squash, the offset blinks, that little white dot. That's where she stops being a drawing and starts feeling like she's there.&lt;/p&gt;

&lt;p&gt;If you want to say hi, she's over at &lt;a href="https://kaomojikan.com/en/" rel="noopener noreferrer"&gt;Kaomojikan&lt;/a&gt;. She'll follow you around the page &lt;code&gt;(・ω・)&lt;/code&gt;. And if it's useful to you, the kaomoji data (readings, tags, and categories, all as JSON) is &lt;a href="https://github.com/kaomojikan/kaomoji-data" rel="noopener noreferrer"&gt;open under MIT&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>svg</category>
      <category>css</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
