Every graphic on this page was generated by kurviq/author and verified through @tecsteps/kurviq-toolbelt. None of it is hand-written SVG.

Paint servers

Gradients, patterns and reused symbols. Units are mandatory — the SVG default differs per element and silently changes the result, so an omitted value is a bug rather than a convenience.

Gradients, hatching, symbols
const { createScene, shapes } = require('kurviq/author');
const { linearGradient, hatch, symbol } = require('kurviq/author-paint');

const scene = createScene({ width: 320, height: 180 });
const ramp = linearGradient(scene, {
  units: 'objectBoundingBox',          // required, never defaulted
  from: [0, 0], to: [1, 1],
  stops: [{ offset: 0, color: '#22d3ee' }, { offset: 1, color: '#a3e635' }],
});
scene.add(shapes.rect(16, 16, 140, 66, { fill: ramp.ref, rx: 8 }));
Gradient, hatch pattern and repeated symbol chips

Strokes and markers

Width, caps, joins, dashes and arrowheads. There is one stroke-width per element — variable width along a path is not interoperable SVG, so vary it with multiple elements.

Dashes and an arrowhead
const { strokedPath, arrowhead, withMarkers } = require('kurviq/author-stroke');

const head = arrowhead(scene, { size: 7, color: '#fb7185' });
scene.add(withMarkers(
  strokedPath('M24 40L250 40', { color: '#fb7185', width: 3 }),
  { end: head },
));
// strokedPath sets fill="none" explicitly: the SVG default is black,
// and a filled open path is the classic surprise.
Dashed and capped strokes with an arrow marker

Clipping and masking

A clip is a hard geometric boundary; a mask is per-pixel. The mask type is mandatory because the same content behaves completely differently under luminance and alpha.

Clip plus a luminance fade
const { clipPath, fadeMask } = require('kurviq/author-mask');

const clip = clipPath(scene, {
  units: 'userSpaceOnUse',
  content: [shapes.circle(90, 90, 62), shapes.rect(150, 40, 140, 100)],
});
const fade = fadeMask(scene, { box: [0, 0, 320, 180], direction: 'to-right' });
scene.add(clip.apply(fade.apply(shapes.rect(0, 0, 320, 180, { fill: ramp.ref }))));
A gradient clipped to a circle and a rectangle, fading to the right

Filters

Blur, drop shadow and colour matrices. The filter region is mandatory: the SVG default clips blurs and shadows, and the symptom — a cut-off edge — never points at the cause.

Blur with an explicit region
const { blur, dropShadow, colorMatrix } = require('kurviq/author-filter');

// elementSize gives a tight filter region. Without it the default is
// deliberately generous: in objectBoundingBox units the pad is a fraction
// of the ELEMENT, so a small shape with a big blur needs a big fraction.
const soft = blur(scene, { stdDeviation: 6, elementSize: 70 });
scene.add(soft.apply(shapes.circle(66, 90, 34, { fill: '#22d3ee' })));
A blurred circle, a shadowed square and a desaturated circle

Interaction states

Hover and keyboard focus, no JavaScript. Interactivity is a property of the embedding, not the file: an <img>-embedded SVG receives no pointer events, so :hover there is dead markup. assertEmbedding() makes that an error instead of a silent no-op — which is why this demo is inlined rather than loaded as an image.

Hover with an embedding assertion
const I = require('kurviq/author-interactive');

const feature = I.hoverState(scene, {
  selector: '.chip',
  base: { fill: '#1e293b', stroke: '#94a3b8' },
  on: { fill: '#22d3ee', stroke: '#22d3ee' },
});

// Throws if this document would be shipped as <img>, where no pointer
// events arrive and :hover is dead markup.
I.assertEmbedding([feature], I.EMBEDDING.INLINE);
Hover and keyboard focus states

Animation

An AnimationPlan serializes to CSS or SMIL and can be sampled at a given time. The initial state lives only in the keyframes, so a renderer that applies CSS without animating paints the correct final frame rather than a blank image. Reduced motion settles on the final value.

A staggered entrance
const A = require('kurviq/author-animate');

const plan = A.animationPlan({ duration: 900, stagger: 120, easing: 'ease-out' });
[0, 1, 2, 3].forEach((i) =>
  plan.animate({ selector: `.bar-${i}`, property: 'opacity', from: 0, to: 1 }));
A.applyPlan(scene, plan);   // status: 'declared' never more than that
Four bars fading in with a stagger

Verify it, do not hope

Rendering, comparison and state verification live in @tecsteps/kurviq-toolbelt, so kurviq itself stays dependency-free. A state is verified by compiling it into a static document, rendering, and diffing — no browser required.

Verification
const { evaluate, validateTarget } = require('@tecsteps/kurviq-toolbelt');

// Verify an interaction state without a browser: the state is compiled
// into a static document, rendered, and diffed against the base.
const { report } = evaluate({ svg, width: 320, states: ['base', 'hover'] });
report.states.hover.visiblyDiffers;   // false means the rule paints nothing
report.states.hover.changedPixels;    // 13992 for the demo above

validateTarget(svg, 'browser-img').ok; // false: hover needs pointer events

Measured target support

Established by rendering, not read from documentation. The renderer column is our own verification engine, pinned to a version.

Featureresvg (static)<img>inline
CSS custom propertiesrenders blackinternal onlyyes
CSS / SMIL animationnever executedyesyes
pathLengthignoredyesyes
mask-type: alphafalls back to luminanceyesyes
Pointer interactionno modelno eventsyes