SVG as a UI Toolkit

SVG is often treated as a format for icons and illustrations, but it is equally capable as a medium for building interface components. Its coordinate system makes precise placement and overlap straightforward, unlike CSS layout, which is built around document flow and can struggle with fixed-position, layered elements. SVG also brings real accessibility features along with it: text, links, ARIA labels, and semantic structure are all available.

Take a concrete example: a timeline component that tracks progress through a series of tasks. The idea is simple — a vertical line with a circle per task. Completed tasks are filled; incomplete ones are hollow. Clicking a circle toggles its state. That same structure can be adapted to mark a learner’s position in a course, show steps in a workflow, or visualize any checklist where context matters.

That little half circle below the author image is just SVG markup.

Setting Up the SVG

The outer SVG element needs a few deliberate choices. A flexible viewBox lets the graphic scale across viewports. To keep the component accessible, mark the SVG as presentational with role="presentation" and provide an accessible title, in this case using a title element with an id of timeline so assistive tech can reference it.

The line and circle strokes use currentColor, which allows the component to inherit the text color of its wrapper. That makes it trivial to reuse the same component in light or dark contexts without extra props.

Drawing the Line

SVG lines are defined by two coordinate pairs: x1/y1 for the start and x2/y2 for the end. Here the line runs vertically, so the x-coordinates stay fixed at 10 while the y-values are derived from data.

Two data values drive the layout:

  • The spacing between tasks (num1)
  • The top and bottom margin (num2)

The vertical span is calculated as the margin subtracted from both ends, and the distance between tasks is the number of tasks multiplied by the spacing.

Placing the Circles

Each task gets a circle. SVG circles need a center point (cx, cy) and a radius (r). The center x-coordinate is aligned with the line at 10; the radius is set to 4 to stay readable at the component’s scale. The y-coordinate is computed from the index: index times spacing, plus the margin.

The state of each task determines its fill. Completed items are filled with currentColor; incomplete ones are white. For extensibility, that background color could just as easily come from a prop, which matters for themes with alternate background colors.

===

Vue Implementation

A clean way to prototype this component is as a single-file Vue component. The template, script, and scoped styles live together. Dummy task data, plus a method to toggle a task’s done state, sit in the component’s reactivity system.

Looping over tasks with v-for builds both the circles and the label list. The index is a safe key here because the task order never changes. The click handler receives the index and flips that task’s state.

The labels themselves sit beside the SVG using CSS grid. Each label is wired into the same click event, so the entire row is clickable, not just the circle.

The full working example demonstrates how all these pieces behave together:

<script>
export default {
  data() {
    return {
      tasks: [
        {
          name: 'thing',
          done: false
        },
        // ...
      ]
    };
  },
  methods: {
    selectThis(index) {
      this.tasks[index].done = !this.tasks[index].done
    }
  }
};
</script>

React Version

Ported to React, the same concept takes on a couple of modifications driven by purpose. Instead of toggling a local done state, the task rows become links that navigate users to the relevant course page using its dynamic Next.js routes. CSS modules replace scoped SFC styles.

Task data, referred to as “missions” on the platform, is now passed in as props from the parent rather than stored in the component. Otherwise, all of the primitives remain identical: the vertical line calculates from the data length, and circles render with the same coordinate math.

<template>
  <div id="app">
    <div>
      <svg :viewBox="`0 0 30 ${tasks.length * 50}`"
           xmlns="http://www.w3.org/2000/svg" 
           width="30" 
           stroke="currentColor" 
           fill="white"
           aria-labelledby="timeline"
           role="presentation">
           <title id="timeline">timeline element</title>
        <!-- ... -->
      </svg>
    </div>
  </div>
</template>

Building Beyond This Example

Once the pattern is familiar, the applications expand quickly. The same geometry handles progress bars, draggable knobs, switch controls, loaders, and step indicators. SVG primitives can be styled with CSS, updated with reactive state, or animated and bound to context. The result is a component model based on coordinates rather than flow, one that accommodates precise, layered interface elements consistently across browsers and screen sizes.