Cat in a box

No dependencies
Code format
React + Tailwind
import { useState } from 'react';

const css = `
  .cb-catbox { --fur: light-dark(#50535b, #a7abb2); --mark: light-dark(#373a40, #393c41); --white: light-dark(#fbfcfd, #f1f2f3); --pupil: light-dark(#373a40, #121417); --board: light-dark(#cbced3, #7d8087); --flap: light-dark(#dbdde1, #393c41); --inside: light-dark(#5f626a, #232529); --tape: light-dark(#ebedf0, #c6c8cd); --print: light-dark(#373a40, #121417); flex: none; display: flex; flex-direction: column; align-items: center; gap: 6px; color: light-dark(#42454c, #e5e6e9); font-size: 14px; font-weight: 500; cursor: pointer; -webkit-user-select: none; user-select: none; }
  .cb-catbox__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
  .cb-catbox__art { display: block; width: 256px; height: 190px; overflow: visible; border-radius: 12px; }
  .cb-catbox__input:focus-visible + .cb-catbox__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 2px; }
  .cb-catbox__floor { stroke: light-dark(#cbced3, #393c41); stroke-width: 1.5; stroke-linecap: round; }
  .cb-catbox__cat { transform: translate(64px, 176px); }
  .cb-catbox__input:checked + .cb-catbox__art .cb-catbox__cat { transform: translate(184px, 148px); }
  .cb-catbox[data-armed] .cb-catbox__input:checked + .cb-catbox__art .cb-catbox__cat { animation: cb-catbox-in 0.8s linear; }
  .cb-catbox[data-armed] .cb-catbox__input:not(:checked) + .cb-catbox__art .cb-catbox__cat { animation: cb-catbox-out 0.7s linear; }
  @keyframes cb-catbox-in {
    0% { transform: translate(64px, 176px); }
    16% { transform: translate(60px, 176px) scale(1.16, 0.74); animation-timing-function: ease-out; }
    26% { transform: translate(78px, 150px) scale(0.86, 1.16) rotate(8deg); animation-timing-function: cubic-bezier(0.33, 0.67, 0.67, 1); }
    54% { transform: translate(140px, 92px) rotate(16deg); animation-timing-function: cubic-bezier(0.33, 0, 0.67, 0.33); }
    80% { transform: translate(184px, 110px) scale(0.94, 1.08) rotate(6deg); }
    90% { transform: translate(184px, 160px) scale(1.12, 0.84); animation-timing-function: ease-out; }
    100% { transform: translate(184px, 148px); }
  }
  @keyframes cb-catbox-out {
    0% { transform: translate(184px, 148px); }
    14% { transform: translate(184px, 158px) scale(1.1, 0.86); animation-timing-function: cubic-bezier(0.33, 0.67, 0.67, 1); }
    50% { transform: translate(122px, 94px) rotate(-16deg); animation-timing-function: cubic-bezier(0.33, 0, 0.67, 0.33); }
    86% { transform: translate(64px, 176px) scale(1.16, 0.76); animation-timing-function: ease-out; }
    100% { transform: translate(64px, 176px); }
  }
  .cb-catbox__head { transform-box: fill-box; transform-origin: 50% 90%; transition: transform 0.3s cubic-bezier(0.3, 1.5, 0.5, 1); transform: rotate(5deg); }
  .cb-catbox:hover .cb-catbox__head { transform: rotate(11deg); }
  .cb-catbox__input:checked + .cb-catbox__art .cb-catbox__head { transform: rotate(-4deg); }
  .cb-catbox__pupil { transform: translate(2.4px, 0.6px); transition: transform 0.25s; }
  .cb-catbox__input:checked + .cb-catbox__art .cb-catbox__pupil { transform: translate(-1.4px, 0.8px); }
  .cb-catbox__eye { transform-box: fill-box; transform-origin: center; animation: cb-catbox-blink 4.6s linear infinite; }
  @keyframes cb-catbox-blink {
    0%, 95%, 100% { transform: none; }
    97.5% { transform: scaleY(0.1); }
  }
  .cb-catbox__tail { fill: none; stroke: var(--fur); stroke-width: 8; stroke-linecap: round; transform-box: fill-box; transform-origin: 0 100%; animation: cb-catbox-tail 2.8s ease-in-out infinite; }
  @keyframes cb-catbox-tail {
    0%, 100% { transform: rotate(-3deg); }
    50% { transform: rotate(9deg); }
  }
  .cb-catbox__body { fill: var(--fur); }
  .cb-catbox__chest { fill: var(--white); }
  .cb-catbox__stripe { fill: none; stroke: var(--mark); stroke-width: 2.4; stroke-linecap: round; }
  .cb-catbox__whisker { fill: none; stroke: var(--white); stroke-width: 0.9; stroke-linecap: round; }
  .cb-catbox__whites { fill: var(--white); }
  .cb-catbox__dark { fill: var(--pupil); }
  .cb-catbox__carton { transform-box: fill-box; transform-origin: 50% 100%; }
  .cb-catbox[data-armed] .cb-catbox__input:checked + .cb-catbox__art .cb-catbox__carton { animation: cb-catbox-thump 0.4s cubic-bezier(0.3, 1.5, 0.5, 1) 0.68s; }
  @keyframes cb-catbox-thump {
    0% { transform: scale(1.03, 0.94); }
    100% { transform: none; }
  }
  .cb-catbox__inside { fill: var(--inside); }
  .cb-catbox__flapb { fill: var(--flap); }
  .cb-catbox__front { fill: var(--board); }
  .cb-catbox__side { fill: var(--flap); transform-box: fill-box; transition: transform 0.3s cubic-bezier(0.3, 2, 0.5, 1) 0.7s; }
  .cb-catbox__side--l { transform-origin: 100% 100%; }
  .cb-catbox__side--r { transform-origin: 0 100%; }
  .cb-catbox__input:checked + .cb-catbox__art .cb-catbox__side--l { transform: rotate(-8deg); }
  .cb-catbox__input:checked + .cb-catbox__art .cb-catbox__side--r { transform: rotate(8deg); }
  .cb-catbox__tape { fill: var(--tape); }
  .cb-catbox__square { fill: none; stroke: var(--print); stroke-width: 2.4; }
  .cb-catbox__small { fill: var(--print); font: 700 7.5px system-ui, sans-serif; letter-spacing: 0.08em; text-anchor: middle; }
  .cb-catbox__tick { fill: none; stroke: var(--print); stroke-width: 5; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset 0.12s; }
  .cb-catbox__input:checked + .cb-catbox__art .cb-catbox__tick { stroke-dashoffset: 0; transition: stroke-dashoffset 0.3s ease-out 0.82s; }
  @media (prefers-reduced-motion: reduce) {
    .cb-catbox *, .cb-catbox[data-armed] .cb-catbox__input + .cb-catbox__art * { animation: none !important; transition: none !important; }
  }
`;

export function CatboxCheckbox() {
  // The leaps only play when someone ticks it, not on load.
  const [armed, setArmed] = useState(false);
  return (
    <>
      <style>
        {css}
      </style>
      <label className="cb-catbox" data-armed={armed || undefined}>
        <input className="cb-catbox__input" type="checkbox" onChange={() => setArmed(true)} />
        <svg className="cb-catbox__art" viewBox="0 0 256 190" aria-hidden="true">
          <path className="cb-catbox__floor" d="M6 176H250" />
          <g className="cb-catbox__carton">
            <path className="cb-catbox__inside" d="M134 100L148 84H220L234 100Z" />
            <path className="cb-catbox__flapb" d="M148 84L156 66H212L220 84Z" />
          </g>
          {/* The cat is drawn between the back and the front of the carton, so climbing in hides its body. */}
          <g className="cb-catbox__cat">
            <path className="cb-catbox__tail" d="M18 -5C40 -3 46 -20 38 -34" />
            <path className="cb-catbox__body" d="M-21 0C-27 -18 -21 -46 0 -48C21 -46 27 -18 21 0Z" />
            <ellipse className="cb-catbox__chest" cx="0" cy="-20" rx="10" ry="15" />
            <ellipse className="cb-catbox__body" cx="-9" cy="-2" rx="7" ry="4" />
            <ellipse className="cb-catbox__body" cx="9" cy="-2" rx="7" ry="4" />
            <g className="cb-catbox__head">
              <path className="cb-catbox__body" d="M-22 -72L-24 -96L-5 -82ZM22 -72L24 -96L5 -82Z" />
              <path className="cb-catbox__chest" d="M-19 -76L-20 -89L-10 -81ZM19 -76L20 -89L10 -81Z" />
              <path className="cb-catbox__body" d="M-24 -62C-24 -78 -13 -84 0 -84C13 -84 24 -78 24 -62C24 -49 13 -43 0 -43C-13 -43 -24 -49 -24 -62Z" />
              <path className="cb-catbox__stripe" d="M0 -83V-74M-7 -82V-76M7 -82V-76" />
              <g className="cb-catbox__eye">
                <ellipse className="cb-catbox__whites" cx="-10" cy="-64" rx="6" ry="6.8" />
                <ellipse className="cb-catbox__dark cb-catbox__pupil" cx="-10" cy="-64" rx="3.1" ry="4.6" />
              </g>
              <g className="cb-catbox__eye">
                <ellipse className="cb-catbox__whites" cx="10" cy="-64" rx="6" ry="6.8" />
                <ellipse className="cb-catbox__dark cb-catbox__pupil" cx="10" cy="-64" rx="3.1" ry="4.6" />
              </g>
              <path className="cb-catbox__dark" d="M-2.6 -55H2.6L0 -52Z" />
              <path className="cb-catbox__whisker" d="M-14 -54L-30 -57M-14 -51L-30 -49M14 -54L30 -57M14 -51L30 -49" />
            </g>
          </g>
          <g className="cb-catbox__carton">
            <path className="cb-catbox__side cb-catbox__side--l" d="M134 100L114 84L118 112L134 112Z" />
            <path className="cb-catbox__side cb-catbox__side--r" d="M234 100L254 84L250 112L234 112Z" />
            <path className="cb-catbox__front" d="M134 100H234V176H134Z" />
            <rect className="cb-catbox__tape" x="134" y="100" width="100" height="9" />
            <rect className="cb-catbox__square" x="166" y="121" width="36" height="36" rx="3" />
            <path className="cb-catbox__tick" pathLength={1} d="M173 139L181 148L196 128" />
            <text className="cb-catbox__small" x="184" y="170">
              THIS WAY UP
            </text>
          </g>
        </svg>
        Add a gift box
      </label>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/checkbox-catbox
Source on GitHub

About this component

A real pun on check box. A cat sits watching an empty carton; tick it and the cat crouches, leaps in with a thump and peeks over the rim while a tick prints itself on the carton.

Dependencies
None
Difficulty
Intermediate
Triggers
click, state change, keyframe, draw, ambient
Techniques
keyframes, svg, stroke dashoffset, css transition, css variables
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

A native checkbox inside a <label>, so "Add a gift box" is its name. The drawing is aria-hidden and keyboard focus outlines it. Under prefers-reduced-motion the cat is simply in or out of the box and the tick appears at once.

More checkboxes

See all
Checkmate
Cheque
Door hanger
Hatch