Let us explore SVG in the browser. The default svg is 300x150 pixels, and it is scaled with the page.

html
<svg style="border: 1px solid gray"/>

Grid Pattern

A grid pattern helps viewers understand the coordinate system - its origin, extent, and scale. The grid pattern shows:

Centered Text Q (quadratic) C (cubic) Q...T (smooth quad) C...S (smooth cubic) butt (default) round square miter (default) round bevel
html
<svg id="gridSvg" style="border: 1px solid gray">
    <circle cx="50" cy="50" r="20" fill="blue"/>
    <rect x="-50" y="0" width="50" height="50" fill="none" stroke="red" stroke-width="2"/>
    <line x1="0" y1="0" x2="-100" y2="100" stroke="green" stroke-width="2"/>
    <text x="0" y="160"
          font-family="Georgia, serif"
          font-size="16"
          font-weight="bold"
          font-style="italic"
          fill="#141a78"
          text-anchor="middle"
          dominant-baseline="middle">
        Centered Text
    </text>

    <g transform="translate(-150, 100)">
        <!-- Triangle (polygon) -->
        <polygon points="0,-15 13,7.5 -13,7.5" fill="none" stroke="#e74c3c" stroke-width="2"/>

    </g>
    <g transform="translate(-50, 100)">
        <!-- Square (polygon) -->
        <polygon points="-12,-12 12,-12 12,12 -12,12" fill="none" stroke="#e67e22" stroke-width="2"/>
    </g>
    <g transform="translate(50, 100)">
        <!-- Pentagon (polygon) -->
        <polygon points="0,-15 14.3,-4.6 8.8,12.1 -8.8,12.1 -14.3,-4.6" fill="none" stroke="#27ae60" stroke-width="2"/>
    </g>
    <g transform="translate(150, 100)">
        <!-- Hexagon (polygon) -->
        <polygon points="15,0 7.5,13 -7.5,13 -15,0 -7.5,-13 7.5,-13" fill="none" stroke="#3498db" stroke-width="2"/>
    </g>

    <g transform="translate(-150, 140)">
        <!-- Triangle (path) -->
        <path d="M 0,-15 L 13,7.5 L -13,7.5 Z" fill="none" stroke="#e74c3c" stroke-width="2"/>
    </g>
    <g transform="translate(-50, 140)">
        <!-- Square (path) -->
        <path d="M -12,-12 L 12,-12 L 12,12 L -12,12 Z" fill="none" stroke="#e67e22" stroke-width="2"/>
    </g>
    <g transform="translate(50, 140)">
        <!-- Pentagon (path) -->
        <path d="M 0,-15 L 14.3,-4.6 L 8.8,12.1 L -8.8,12.1 L -14.3,-4.6 Z" fill="none" stroke="#27ae60" stroke-width="2"/>
    </g>
    <g transform="translate(150, 140)">
        <!-- Hexagon (path) -->
        <path d="M 15,0 L 7.5,13 L -7.5,13 L -15,0 L -7.5,-13 L 7.5,-13 Z" fill="none" stroke="#3498db" stroke-width="2"/>
    </g>

    <!-- Bezier curves in lower-left quadrant (negative x, positive y in SVG coords = negative y when yUp) -->
    <g transform="translate(-150, -50)">
        <!-- Quadratic Bezier: Q (one control point) -->
        <path d="M -40,0 Q 0,-40 40,0" fill="none" stroke="#9b59b6" stroke-width="2"/>
        <!-- Show control point -->
        <circle cx="0" cy="-40" r="3" fill="#9b59b6" opacity="0.5"/>
        <line x1="-40" y1="0" x2="0" y2="-40" stroke="#9b59b6" stroke-width="0.5" stroke-dasharray="3"/>
        <line x1="0" y1="-40" x2="40" y2="0" stroke="#9b59b6" stroke-width="0.5" stroke-dasharray="3"/>
        <text x="0" y="15" font-size="8" text-anchor="middle">Q (quadratic)</text>
    </g>
    <g transform="translate(-50, -50)">
        <!-- Cubic Bezier: C (two control points) -->
        <path d="M -40,0 C -20,-50 20,50 40,0" fill="none" stroke="#e74c3c" stroke-width="2"/>
        <!-- Show control points -->
        <circle cx="-20" cy="-50" r="3" fill="#e74c3c" opacity="0.5"/>
        <circle cx="20" cy="50" r="3" fill="#e74c3c" opacity="0.5"/>
        <line x1="-40" y1="0" x2="-20" y2="-50" stroke="#e74c3c" stroke-width="0.5" stroke-dasharray="3"/>
        <line x1="20" y1="50" x2="40" y2="0" stroke="#e74c3c" stroke-width="0.5" stroke-dasharray="3"/>
        <text x="0" y="15" font-size="8" text-anchor="middle">C (cubic)</text>
    </g>
    <g transform="translate(-150, -120)">
        <!-- Smooth Quadratic: T (reflects previous Q control point) -->
        <path d="M -40,0 Q -20,-30 0,0 T 40,0" fill="none" stroke="#3498db" stroke-width="2"/>
        <text x="0" y="15" font-size="8" text-anchor="middle">Q...T (smooth quad)</text>
    </g>
    <g transform="translate(-50, -120)">
        <!-- Smooth Cubic: S (reflects previous C control point) -->
        <path d="M -40,0 C -30,-30 -10,-30 0,0 S 30,30 40,0" fill="none" stroke="#27ae60" stroke-width="2"/>
        <text x="0" y="15" font-size="8" text-anchor="middle">C...S (smooth cubic)</text>
    </g>

    <!-- Stroke end caps in lower-right quadrant -->
    <g transform="translate(50, -50)">
        <line x1="-30" y1="0" x2="30" y2="0" stroke="#333" stroke-width="10" stroke-linecap="butt"/>
        <text x="0" y="20" font-size="8" text-anchor="middle">butt (default)</text>
    </g>
    <g transform="translate(150, -50)">
        <line x1="-30" y1="0" x2="30" y2="0" stroke="#333" stroke-width="10" stroke-linecap="round"/>
        <text x="0" y="20" font-size="8" text-anchor="middle">round</text>
    </g>
    <g transform="translate(100, -100)">
        <line x1="-30" y1="0" x2="30" y2="0" stroke="#333" stroke-width="10" stroke-linecap="square"/>
        <text x="0" y="20" font-size="8" text-anchor="middle">square</text>
    </g>

    <!-- Stroke line joins in lower-right quadrant -->
    <g transform="translate(50, -150)">
        <path d="M -20,10 L 0,-10 L 20,10" fill="none" stroke="#333" stroke-width="8" stroke-linejoin="miter"/>
        <text x="0" y="25" font-size="8" text-anchor="middle">miter (default)</text>
    </g>
    <g transform="translate(150, -150)">
        <path d="M -20,10 L 0,-10 L 20,10" fill="none" stroke="#333" stroke-width="8" stroke-linejoin="round"/>
        <text x="0" y="25" font-size="8" text-anchor="middle">round</text>
    </g>
    <g transform="translate(100, -190)">
        <path d="M -20,10 L 0,-10 L 20,10" fill="none" stroke="#333" stroke-width="8" stroke-linejoin="bevel"/>
        <text x="0" y="25" font-size="8" text-anchor="middle">bevel</text>
    </g>
</svg>
js
import {elt} from '/lib/svg.js';

const svg = elt('gridSvg');
svg.setAttribute('width', '100%');

// Grid configuration
const extent = 200;        // grid spans -extent to +extent on both axes
const majorStep = 50;      // major gridlines every N units
const minorStep = 10;      // minor gridlines every N units
const yUp = true;          // true: positive y goes up (math style), false: positive y goes down (SVG default)

// Compute viewBox with padding for labels
const padding = 20;
const viewMin = -extent - padding;
const viewSize = (extent + padding) * 2;
svg.setAttribute('viewBox', `${viewMin} ${viewMin} ${viewSize} ${viewSize}`);

// preserveAspectRatio: <align> [<meetOrSlice>]
// align: none | xMinYMin | xMidYMin | xMaxYMin | xMinYMid | xMidYMid | xMaxYMid | xMinYMax | xMidYMax | xMaxYMax
// meetOrSlice: meet (fit inside, letterbox) | slice (fill, crop overflow)
// https://www.w3.org/TR/SVG2/coords.html#PreserveAspectRatioAttribute
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');

// Generate scale labels at each major step
const xLabels = [];
const yLabels = [];
for (let v = -extent; v <= extent; v += majorStep) {
  if (v === 0) continue; // skip origin
  xLabels.push(`<text x="${v}" y="${15}" text-anchor="middle">${v}</text>`);
  // For y labels: if yUp, negate the display value (SVG y=-100 means math y=+100)
  const yDisplay = yUp ? -v : v;
  yLabels.push(`<text x="${-12}" y="${v + 4}" text-anchor="end">${yDisplay}</text>`);
}

// Save existing content nodes before modifying
const existingNodes = [...svg.childNodes];

// Insert grid and labels
svg.innerHTML = `
  <defs>
    <pattern id="minorGrid" width="${minorStep}" height="${minorStep}" patternUnits="userSpaceOnUse">
      <path d="M ${minorStep} 0 L 0 0 0 ${minorStep}" fill="none" stroke="#e0e0e0" stroke-width="0.5"/>
    </pattern>
    <pattern id="majorGrid" width="${majorStep}" height="${majorStep}" patternUnits="userSpaceOnUse">
      <rect width="${majorStep}" height="${majorStep}" fill="url(#minorGrid)"/>
      <path d="M ${majorStep} 0 L 0 0 0 ${majorStep}" fill="none" stroke="#a0a0a0" stroke-width="1"/>
    </pattern>
  </defs>

  <!-- Grid layer (behind content) -->
  <g id="gridLayer">
    <rect x="${-extent}" y="${-extent}" width="${extent * 2}" height="${extent * 2}" fill="url(#majorGrid)"/>
    <rect x="${-extent}" y="${-extent}" width="${extent * 2}" height="${extent * 2}" fill="none" stroke="#333" stroke-width="1"/>
    <line x1="${-extent}" y1="0" x2="${extent}" y2="0" stroke="#e74c3c" stroke-width="1" opacity="0.6"/>
    <line x1="0" y1="${-extent}" x2="0" y2="${extent}" stroke="#27ae60" stroke-width="1" opacity="0.6"/>
    <circle cx="0" cy="0" r="4" fill="#e74c3c" opacity="0.8"/>
  </g>

  <!-- Content layer (on top of grid, with optional y-flip) -->
  <g id="contentLayer" ${yUp ? 'transform="scale(1,-1)"' : ''}></g>

  <!-- Labels layer (always on top, never flipped) -->
  <g id="labelLayer" font-size="8" fill="#666">
    <text x="8" y="${yUp ? 12 : -8}" font-size="10" fill="#333">(0,0)</text>
    ${xLabels.join('\n    ')}
    ${yLabels.join('\n    ')}
  </g>
`;

// Move existing content into contentLayer
const contentLayer = svg.querySelector('#contentLayer');
existingNodes.forEach(node => contentLayer.appendChild(node));

// If yUp, flip text elements back so they're readable
if (yUp) {
  contentLayer.querySelectorAll('text').forEach(text => {
    const x = text.getAttribute('x') || 0;
    const y = text.getAttribute('y') || 0;
    const existing = text.getAttribute('transform') || '';
    text.setAttribute('transform', `${existing} translate(${x},${y}) scale(1,-1) translate(${-x},${-y})`.trim());
  });
}

© 2026 simpatico