Skip to main content

🧩 Inline SVG and Manipulation

When you write SVG directly inside your HTML, every shape becomes a real DOM node — something JavaScript can create, style, animate, and wire to events. This lesson turns static vector art into living, data-driven graphics: you'll manipulate elements with the DOM API, animate with CSS, generate charts from data, and keep it all accessible.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain why inline SVG beats <img> for interactive graphics
  • Read and modify SVG elements with getAttribute/setAttribute and create them with createElementNS
  • Style and animate SVG with CSS transitions, keyframes, and the line-drawing trick
  • Generate SVG from data to build a bar chart programmatically
  • Add interactivity (hover, click, tooltips) and proper accessibility

Estimated Time: 40–50 minutes  •  Difficulty: Intermediate

Hands-on: Build a data-driven bar chart that reacts to hover and updates live.

In This Lesson

Why Inline SVG?

Inline SVG means writing the <svg> markup directly in your HTML document instead of pointing an <img> at an external .svg file. The payoff is enormous: the shapes join the page's DOM, so they behave like any other element — selectable, styleable, scriptable.

💡 A useful analogy: An <img src="chart.svg"> is a sealed photograph of a chart — you can hang it on the wall but not touch what's inside. Inline SVG is the chart itself, with every bar, label, and axis handed to you as a component you can paint, move, and click.
flowchart TD A[SVG in the DOM] --> B[Direct element access] A --> C[CSS styling & :hover] A --> D[JavaScript manipulation] A --> E[Per-shape events] A --> F[No extra HTTP request]

Because each element is a DOM node, inline SVG unlocks:

  • Direct manipulation of individual shapes via JavaScript
  • CSS styling, including hover states and media queries
  • Interactive behaviors — clicks, drags, tooltips
  • Dynamic updates driven by data or application state

Embedding Methods Compared

There are several ways to put SVG on a page. They trade convenience for control — and only inline SVG gives you full JavaScript access.

MethodDOM accessCSS controlJS controlBest for
Inline <svg>…</svg>FullFullFullInteractive graphics
<img src="x.svg">NoneFilters onlyNoneStatic images
background: url(x.svg)NoneFilters onlyNoneDecoration
<object data="x.svg">LimitedInternal onlyLimitedIsolated widgets
<!-- Inline: every child is reachable from JS and CSS -->
<svg width="200" height="200" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
  <circle id="dot" cx="50" cy="50" r="40" fill="#3b82f6" stroke="#0d47a1" stroke-width="2" />
  <text x="50" y="54" text-anchor="middle" fill="#fff" font-size="10">Inline</text>
</svg>

💡 Rule of thumb

If the graphic needs to react — to hover, clicks, data, or state — inline it. If it's purely decorative or a fixed picture, an <img> caches better and keeps your HTML lean.

Manipulating with JavaScript

Reading and changing existing shapes uses the same DOM methods you already know. The one twist: SVG lives in its own XML namespace, so creating new elements requires createElementNS, not createElement.

// Read & change an existing element
const dot = document.getElementById('dot');
dot.setAttribute('fill', 'tomato');
dot.setAttribute('r', '30');
dot.addEventListener('click', () => dot.setAttribute('fill', 'seagreen'));

// Create a NEW element — must use the SVG namespace
const SVG_NS = 'http://www.w3.org/2000/svg';
const rect = document.createElementNS(SVG_NS, 'rect');
rect.setAttribute('x', '10');
rect.setAttribute('y', '10');
rect.setAttribute('width', '80');
rect.setAttribute('height', '80');
rect.setAttribute('fill', '#f59e0b');

const svg = document.querySelector('svg');
svg.appendChild(rect);          // it appears immediately

⚠️ createElement silently fails for SVG

If you use document.createElement('rect'), the browser creates an HTML element named "rect" that renders as nothing. It won't throw an error — it just won't show up. Always use createElementNS with the http://www.w3.org/2000/svg namespace for SVG.

Styling & Animating with CSS

Inline SVG elements respond to CSS just like HTML — element, class, ID, and pseudo-class selectors all work. The SVG-specific properties (fill, stroke, stroke-width, stroke-dasharray) sit alongside standard ones like opacity and transform.

.dot {
  fill: #3b82f6;
  transition: fill .3s ease, transform .3s ease;
  transform-box: fill-box;      /* so transforms pivot on the shape */
  transform-origin: center;
}
.dot:hover { fill: #ef4444; transform: scale(1.2); }

/* Pulsing keyframe animation */
@keyframes pulse {
  0%, 100% { transform: scale(1);   opacity: 1;   }
  50%      { transform: scale(1.2); opacity: 0.7; }
}
.pulse { animation: pulse 2s infinite ease-in-out; }

The line-drawing trick

A favorite SVG effect animates a path as if it's being drawn by hand. It works by setting stroke-dasharray to the path's total length (one giant dash), then animating stroke-dashoffset from that length down to zero:

.draw {
  fill: none;
  stroke: #6366f1;
  stroke-width: 2;
  stroke-dasharray: 1000;       /* >= the path length */
  stroke-dashoffset: 1000;      /* start fully "hidden" */
  animation: draw 3s forwards ease-in-out;
}
@keyframes draw { to { stroke-dashoffset: 0; } }

✅ Prefer CSS transforms over attribute tweaks for motion

Animating transform in CSS is GPU-friendly and doesn't trigger layout. Repeatedly changing geometry attributes (x, cx, width) forces the browser to recompute the shape each frame. Use CSS for movement; save attribute changes for structural updates.

Generating SVG from Data

The real power appears when SVG is built from data. Loop over an array, create one element per data point, and you have a chart — with each bar a live object you can style and wire to events. Here's a compact, modern bar chart generator:

const SVG_NS = 'http://www.w3.org/2000/svg';

const data = [
  { label: 'Jan', value: 120 },
  { label: 'Feb', value: 150 },
  { label: 'Mar', value: 180 },
  { label: 'Apr', value: 110 },
  { label: 'May', value: 200 },
];

function renderChart(svg, data) {
  const W = 500, H = 300, pad = 40;
  const max = Math.max(...data.map(d => d.value));
  const barW = (W - pad * 2) / data.length * 0.7;
  const gap  = (W - pad * 2) / data.length;

  svg.replaceChildren();        // clear any previous render

  data.forEach((d, i) => {
    const h = (d.value / max) * (H - pad * 2);
    const x = pad + i * gap;
    const y = H - pad - h;

    const bar = document.createElementNS(SVG_NS, 'rect');
    bar.setAttribute('x', x);
    bar.setAttribute('y', y);
    bar.setAttribute('width', barW);
    bar.setAttribute('height', h);
    bar.setAttribute('fill', '#3b82f6');
    bar.dataset.value = d.value;

    bar.addEventListener('mouseenter', () => bar.setAttribute('fill', '#f59e0b'));
    bar.addEventListener('mouseleave', () => bar.setAttribute('fill', '#3b82f6'));

    const label = document.createElementNS(SVG_NS, 'text');
    label.textContent = d.label;
    label.setAttribute('x', x + barW / 2);
    label.setAttribute('y', H - pad + 16);
    label.setAttribute('text-anchor', 'middle');
    label.setAttribute('font-size', '12');

    svg.append(bar, label);
  });
}

renderChart(document.getElementById('chart'), data);

Techniques on display

  • One DOM element created per data point with createElementNS
  • Values scaled to the chart with a computed max
  • Data stashed on each bar via dataset for later lookups
  • replaceChildren() to cleanly re-render when data changes

Interactivity Patterns

Because shapes are DOM nodes, they take event listeners directly. A few patterns cover most needs.

Hover feedback (CSS is usually enough)

.node { fill: #3b82f6; transition: fill .2s; }
.node:hover { fill: #ef4444; }

Click to toggle selection

shape.addEventListener('click', (event) => {
  const selected = shape.classList.toggle('selected');
  shape.setAttribute('stroke-width', selected ? 3 : 1);
  event.stopPropagation();
});

Tooltip that follows the cursor

const tip = document.getElementById('tooltip');

bar.addEventListener('mousemove', (event) => {
  tip.textContent = `Value: ${bar.dataset.value}`;
  tip.style.left = `${event.pageX + 12}px`;
  tip.style.top  = `${event.pageY - 24}px`;
  tip.style.display = 'block';
});
bar.addEventListener('mouseleave', () => { tip.style.display = 'none'; });
Interaction states of an SVG shape The same circle shown in default, hover, selected, and dragging states. default :hover selected dragging
Figure 1 — Each interaction state is just a change of attributes or classes on a live DOM node — impossible with a flat image.

Accessibility

Interactive SVG must work for keyboard and screen-reader users too. The essentials:

  • Give the SVG role="img" with a <title> and <desc>, referenced by aria-labelledby
  • Make clickable shapes focusable (tabindex="0"), give them role="button" and an aria-label
  • Support the keyboard — activate on Enter/Space, not just mouse clicks
  • Don't rely on color alone; keep contrast sufficient
<svg role="img" aria-labelledby="t d">
  <title id="t">Quarterly sales, 2025</title>
  <desc id="d">Bar chart; Q3 shows the strongest growth.</desc>

  <g role="button" tabindex="0" aria-label="Q1 sales: 250,000">
    <rect class="bar" x="50" y="150" width="50" height="100" />
    <text x="75" y="265" text-anchor="middle">Q1</text>
  </g>
</svg>
const q1 = document.querySelector('[aria-label^="Q1"]');
q1.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); showDetails('q1'); }
});
q1.addEventListener('click', () => showDetails('q1'));

Hands-on Exercise

🏋️ A live, hoverable bar chart

Objective: Generate an SVG chart from data, add hover feedback, and update it on a button click.

Instructions:

  1. Start with an empty <svg id="chart" viewBox="0 0 500 300"> and a data array.
  2. Write a renderChart() that creates one <rect> per data point with createElementNS.
  3. On hover, change each bar's fill; on mouseleave, restore it.
  4. Add a "Randomize" button that replaces the values and re-renders with replaceChildren().
💡 Hint

Scale bar heights relative to the data's maximum so the tallest bar always fills the chart: height = (value / max) * chartHeight. Re-running renderChart() with new data is all "updating" requires.

✅ Sample solution
const SVG_NS = 'http://www.w3.org/2000/svg';
const svg = document.getElementById('chart');
let data = [40, 90, 60, 120, 80];

function renderChart(values) {
  const H = 300, pad = 30, max = Math.max(...values);
  const gap = (500 - pad * 2) / values.length;
  svg.replaceChildren();

  values.forEach((v, i) => {
    const h = (v / max) * (H - pad * 2);
    const bar = document.createElementNS(SVG_NS, 'rect');
    bar.setAttribute('x', pad + i * gap);
    bar.setAttribute('y', H - pad - h);
    bar.setAttribute('width', gap * 0.7);
    bar.setAttribute('height', h);
    bar.setAttribute('fill', '#3b82f6');
    bar.addEventListener('mouseenter', () => bar.setAttribute('fill', '#f59e0b'));
    bar.addEventListener('mouseleave', () => bar.setAttribute('fill', '#3b82f6'));
    svg.appendChild(bar);
  });
}

document.getElementById('shuffle').addEventListener('click', () => {
  data = data.map(() => Math.floor(Math.random() * 120) + 20);
  renderChart(data);
});

renderChart(data);

🎯 Quick Quiz

Question 1: Why must you use createElementNS instead of createElement to build SVG shapes in JavaScript?

Question 2: Which property pair is used to animate a path so it appears to be drawn by hand?

Question 3: You want a data chart where each bar has its own click handler and a screen-reader-friendly label. What's the best approach?

Best Practices

✅ Do

  • Inline SVG whenever the graphic needs to react to data, hover, or clicks
  • Use createElementNS with the SVG namespace for new elements
  • Prefer CSS transform/transitions for motion over per-frame attribute changes
  • Batch DOM insertions and use replaceChildren() for clean re-renders
  • Add role, <title>, <desc>, tabindex, and keyboard handlers

⚠️ Don't

  • Reach for createElement (no namespace) — the shape silently won't render
  • Inline enormous, repeated SVGs — use <symbol>/<use> or cache as <img>
  • Animate geometry attributes every frame when a CSS transform would do
  • Convey meaning by color alone or ship interactive SVG with no accessible names

Summary & Quiz

🎉 Key Takeaways

  • Inline SVG puts shapes in the DOM, so they're styleable, scriptable, and clickable.
  • Modify with setAttribute; create with createElementNS and the SVG namespace.
  • Use CSS for hover, transitions, keyframes, and the dash-offset line-drawing effect.
  • Generate graphics from data by looping and appending one element per data point.
  • Interactive SVG still needs ARIA, focus, and keyboard support to be accessible.

📚 Further Reading

🚀 What's Next?

You've now covered forms and all of Module 4's interactive HTML APIs. Next you'll put them to work in the Weekend Project: HTML Forms & HTML5 APIs, combining validation, drag-and-drop, Canvas, and SVG into one small application.

🎉 Nice work!

Your SVGs are now living, data-driven, and accessible. Time to build something with everything you've learned.