Pencil

No dependencies
Code format
React + Tailwind
const T = 3.4;
const Y = 64; // the pencil's centre line
// The shaving: a ribbon curling out of the slot in a tightening spiral.
const SHAVING = (() => {
  const cx = 180;
  const cy = 20;
  const pts: string[] = [];
  for (let i = 0; i <= 64; i++) {
    const t = (i / 64) * 2.6 * Math.PI;
    const r = 22 - t * 2.2;
    const a = Math.PI * 0.62 - t;
    pts.push(`${i ? 'L' : 'M'}${(cx + r * Math.cos(a)).toFixed(1)} ${(cy + r * Math.sin(a)).toFixed(1)}`);
  }
  return pts.join('');
})();

const css = `
  .loader-pencil { --paint: light-dark(#5f626a, #7d8087); --paint-hi: color-mix(in oklab, var(--paint) 60%, light-dark(#fbfcfd, #121417)); --paint-lo: color-mix(in oklab, var(--paint) 70%, light-dark(#373a40, #f1f2f3)); --wood: light-dark(#dbdde1, #c6c8cd); --lead: light-dark(#373a40, #121417); --metal: light-dark(#cbced3, #7d8087); position: relative; width: 230px; height: 128px; }
  /* The pencil slides in and out as one piece; its painted faces roll past as it turns. */
  .loader-pencil__pencil { position: absolute; left: 4px; top: ${Y - 10}px; width: 180px; height: 20px; animation: loader-pencil-slide ${T}s infinite; }
  .loader-pencil__eraser { position: absolute; left: 0; top: 1px; width: 18px; height: 18px; border-radius: 5px 0 0 5px; background: linear-gradient(light-dark(#dbdde1, #a7abb2), light-dark(#6d7179, #393c41)); }
  .loader-pencil__ferrule { position: absolute; left: 16px; top: 0; width: 20px; height: 20px; background: repeating-linear-gradient(90deg, transparent 0 3px, color-mix(in oklab, light-dark(#fbfcfd, #121417) 30%, transparent) 3px 4px), linear-gradient(color-mix(in oklab, var(--metal) 50%, light-dark(#373a40, #f1f2f3)), var(--metal) 45%, color-mix(in oklab, var(--metal) 70%, light-dark(#fbfcfd, #121417))); }
  .loader-pencil__body { position: absolute; left: 36px; top: 0; width: 116px; height: 20px; overflow: hidden; background: repeating-linear-gradient(var(--paint-hi) 0 5px, var(--paint) 5px 11px, var(--paint-lo) 11px 14px, color-mix(in oklab, var(--paint-lo) 70%, light-dark(#fbfcfd, #121417)) 14px 14.6px, var(--paint-hi) 14.6px 15px); background-size: 100% 15px; animation: loader-pencil-turn ${T}s infinite; }
  .loader-pencil__body::after { content: ''; position: absolute; inset: 0; background: linear-gradient(color-mix(in oklab, light-dark(#fbfcfd, #121417) 22%, transparent), transparent 40%, color-mix(in oklab, light-dark(#373a40, #f1f2f3) 18%, transparent)); }
  /* The sharpened end: bare wood with a scalloped edge where the paint stops, and the lead. */
  .loader-pencil__cone { position: absolute; left: 151px; top: 0; width: 28px; height: 20px; background: linear-gradient(90deg, var(--wood) 74%, var(--lead) 74%); clip-path: polygon(0 0, 8% 12%, 0 25%, 8% 38%, 0 50%, 8% 62%, 0 75%, 8% 88%, 0 100%, 74% 62%, 100% 52%, 100% 48%, 74% 38%); }
  .loader-pencil__glint { position: absolute; left: 170px; top: 7.5px; width: 7px; height: 1.5px; border-radius: 1px; background: color-mix(in oklab, light-dark(#373a40, #f1f2f3) 70%, transparent); opacity: 0; animation: loader-pencil-glint ${T}s infinite; }
  /* The sharpener: a metal block with its blade screwed on top and a slot for the shaving. */
  .loader-pencil__block { position: absolute; left: 164px; top: ${Y - 24}px; width: 50px; height: 48px; border-radius: 5px 5px 4px 4px; background: linear-gradient(160deg, color-mix(in oklab, var(--metal) 55%, light-dark(#373a40, #f1f2f3)), var(--metal) 40%, color-mix(in oklab, var(--metal) 75%, light-dark(#fbfcfd, #121417))); box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 35%, transparent), 0 6px 10px -4px light-dark(rgba(0, 0, 0, 0.14), rgba(0, 0, 0, 0.45)); }
  .loader-pencil__block::before { content: ''; position: absolute; left: 5px; top: -3px; width: 36px; height: 7px; border-radius: 1px; background: linear-gradient(light-dark(#50535b, #c6c8cd), light-dark(#42454c, #393c41)); }
  .loader-pencil__block::after { content: ''; position: absolute; left: 26px; top: -1.5px; width: 4.5px; height: 4.5px; border-radius: 50%; background: var(--metal); box-shadow: inset 0 0 0 1px color-mix(in oklab, light-dark(#fbfcfd, #121417) 40%, transparent); }
  .loader-pencil__shaving { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; fill: none; stroke-linecap: round; transform-origin: 180px 26px; animation: loader-pencil-drop ${T}s infinite; }
  .loader-pencil__ribbon { stroke: var(--wood); stroke-width: 7; stroke-dasharray: 1; animation: loader-pencil-curl ${T}s infinite; }
  .loader-pencil__rim { stroke: var(--paint); stroke-width: 1.8; stroke-dasharray: 1; animation: loader-pencil-curl ${T}s infinite; translate: 0 -3px; }
  @keyframes loader-pencil-slide {
    0% { transform: translateX(0); animation-timing-function: linear; }
    55% { transform: translateX(6px); animation-timing-function: cubic-bezier(0.3, 0, 0.2, 1); }
    68%, 84% { transform: translateX(-38px); animation-timing-function: cubic-bezier(0.55, 0, 0.4, 1); }
    96%, 100% { transform: translateX(0); }
  }
  @keyframes loader-pencil-turn { 0% { background-position: 0 0; animation-timing-function: linear; } 55%, 100% { background-position: 0 -120px; } }
  @keyframes loader-pencil-curl { 0%, 4% { stroke-dashoffset: 1; } 55%, 100% { stroke-dashoffset: 0; } }
  @keyframes loader-pencil-drop {
    0%, 56% { transform: none; opacity: 1; animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
    82% { transform: translate(-14px, 92px) rotate(-70deg); opacity: 0; }
    100% { transform: none; opacity: 0; }
  }
  @keyframes loader-pencil-glint { 0%, 68% { opacity: 0; transform: translateX(-4px); } 74% { opacity: 1; } 82%, 100% { opacity: 0; transform: translateX(3px); } }
  @media (prefers-reduced-motion: reduce) {
    .loader-pencil * { animation: none !important; }
    .loader-pencil__pencil { transform: translateX(-38px); }
    .loader-pencil__ribbon, .loader-pencil__rim { stroke-dashoffset: 0; }
  }
`;

export function Preview() {
  return (
    <>
      <style href="kit-loaders-pencil" precedence="kit">
        {css}
      </style>
      <div className="loader-pencil" role="status" aria-label="Loading">
        <span className="loader-pencil__pencil" aria-hidden="true">
          <span className="loader-pencil__eraser" />
          <span className="loader-pencil__ferrule" />
          <span className="loader-pencil__body" />
          <span className="loader-pencil__cone" />
          <span className="loader-pencil__glint" />
        </span>
        <span className="loader-pencil__block" aria-hidden="true" />
        <svg className="loader-pencil__shaving" viewBox="0 0 230 128" aria-hidden="true">
          <path className="loader-pencil__ribbon" d={SHAVING} pathLength={1} />
          <path className="loader-pencil__rim" d={SHAVING} pathLength={1} />
        </svg>
      </div>
    </>
  );
}

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

Install with shadcn

npx shadcn@latest add @movin/loader-pencil
Source on GitHub

About this component

A pencil twists in a sharpener while a curled shaving grows out of it, then it pulls out with a fresh point and goes back in.

Dependencies
None
Difficulty
Advanced
Triggers
continuous, loop, draw
Techniques
keyframes, svg, stroke dashoffset, clip path
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

The root has role="status" and aria-label="Loading"; everything inside it is aria-hidden. Under prefers-reduced-motion the pencil sits pulled out and sharp with the shaving beside it.

More loaders

See all
Quarters
Record
Relay
Shuffle