Framework-agnostic interactive SVG body map with 70+ anatomical regions, intensity visualization, and zero dependencies.
Pure TypeScript. No React, no framework lock-in. Works everywhere the DOM exists.
Detailed anatomical mapping covering head, torso, arms, legs, hands and feet.
0-10 color gradient from neutral through yellow, orange, to deep red.
Anterior (front) and posterior (back) views with smooth transitions.
Full type definitions, declaration maps, and ESM + CJS dual publish.
Keyboard navigation, ARIA labels, and semantic roles on every path.
Click any muscle region to select it. Use the controls on the right to switch views and toggle muscle groups.
npm install body-muscles
yarn add body-muscles
pnpm add body-muscles
<script type="module">
import { BodyChart, ViewSide } from 'https://esm.sh/body-muscles';
</script>
esm.sh — CDN that serves npm packages as native ES modules.
<!-- Global: window.BodyMuscles -->
<script src="https://unpkg.com/body-muscles/dist/umd/body-muscles.umd.min.js"></script>
<script>
const { BodyChart, ViewSide } = BodyMuscles;
</script>
Works everywhere — no bundler needed, supports file://.
import { BodyChart, ViewSide } from 'body-muscles';
const chart = new BodyChart(document.getElementById('container'), {
view: ViewSide.FRONT,
bodyState: {},
onMuscleClick: (id, name) => {
console.log(`Clicked: ${name} (${id})`);
},
onMuscleHover: (id) => {
console.log('Hovered:', id);
},
});
// Update body state
chart.update({
bodyState: {
'biceps-left': { intensity: 7, selected: true },
'chest-upper-right': { intensity: 4, selected: false },
}
});
// Switch to back view
chart.update({ view: ViewSide.BACK });
// Cleanup when done
chart.destroy();
new BodyChart(container, options)Creates and mounts an interactive SVG body map inside the given DOM element.
| Property | Type | Default | Description |
|---|---|---|---|
view |
ViewSide |
— | Anatomical view: "FRONT" or "BACK" |
bodyState |
BodyState |
— | Map of muscle IDs to { intensity, selected } |
onMuscleClick |
(id, name) => void |
noop |
Fired when a muscle region is clicked |
onMuscleHover |
(id | null) => void |
noop |
Fired on hover enter/leave |
className |
string |
"" |
Extra CSS class for the container |
ariaLabel |
string |
auto | Accessibility label |
showViewLabel |
boolean |
false |
Show "Anterior / Posterior" overlay label |
enableTransitions |
boolean |
true |
Smooth CSS transitions |
| Method | Description |
|---|---|
update(options) |
Merge partial options. Changing view triggers a full re-render; other changes update paths in-place. |
destroy() |
Remove the chart from the DOM and clean up all event listeners. |
enum ViewSide { FRONT = "FRONT", BACK = "BACK" }
type MuscleId = string;
interface BodyPartState {
intensity: number; // 0-10
selected: boolean;
}
type BodyState = Partial<Record<MuscleId, BodyPartState>>;
| Export | Description |
|---|---|
MUSCLE_MAP |
All 70+ muscle definitions (front + back) |
FRONT_MUSCLES |
Anterior-view muscle definitions |
BACK_MUSCLES |
Posterior-view muscle definitions |
MUSCLE_GROUPS |
Named groups: Head & Neck, Shoulders, Arms, Chest, Back, Abdominals, Legs, Hands & Feet |
INTENSITY_COLORS |
Color map (0-10) from slate → yellow → orange → red |
| Function | Description |
|---|---|
getMuscleColor(state, isHovered) |
Returns hex color for a given BodyPartState |
filterMuscles(view) |
Returns MuscleDef[] for the given view |
createBodyPartState(intensity?, selected?) |
Factory with validation |
isValidIntensity(value) |
Type guard for 0-10 integer |
extractMuscleSide(id) |
Returns "left" | "right" | "central" |
extractMuscleGroup(id) |
Returns base group string |
<div id="body-map"></div>
<script type="module">
import { BodyChart, ViewSide } from './dist/esm/index.js';
const state = {};
const chart = new BodyChart(document.getElementById('body-map'), {
view: ViewSide.FRONT,
bodyState: state,
onMuscleClick(id, name) {
const cur = state[id] || { intensity: 0, selected: false };
state[id] = { ...cur, selected: !cur.selected };
chart.update({ bodyState: state });
},
});
</script>
import { useRef, useEffect, useState } from 'react';
import { BodyChart, ViewSide } from 'body-muscles';
function BodyMap() {
const ref = useRef(null);
const chartRef = useRef(null);
const [bodyState, setBodyState] = useState({});
useEffect(() => {
chartRef.current = new BodyChart(ref.current, {
view: ViewSide.FRONT,
bodyState,
onMuscleClick(id) {
setBodyState(prev => ({
...prev,
[id]: { intensity: prev[id]?.intensity ?? 0, selected: !prev[id]?.selected },
}));
},
});
return () => chartRef.current?.destroy();
}, []);
useEffect(() => {
chartRef.current?.update({ bodyState });
}, [bodyState]);
return <div ref={ref} />;
}
<template>
<div ref="container" />
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { BodyChart, ViewSide } from 'body-muscles';
const container = ref(null);
const bodyState = ref({});
let chart;
onMounted(() => {
chart = new BodyChart(container.value, {
view: ViewSide.FRONT,
bodyState: bodyState.value,
onMuscleClick(id) {
const cur = bodyState.value[id] || { intensity: 0, selected: false };
bodyState.value = { ...bodyState.value, [id]: { ...cur, selected: !cur.selected } };
},
});
});
watch(bodyState, (s) => chart?.update({ bodyState: s }), { deep: true });
onUnmounted(() => chart?.destroy());
</script>
<script>
import { onMount, onDestroy } from 'svelte';
import { BodyChart, ViewSide } from 'body-muscles';
let container;
let chart;
let bodyState = {};
onMount(() => {
chart = new BodyChart(container, {
view: ViewSide.FRONT,
bodyState,
onMuscleClick(id) {
const cur = bodyState[id] || { intensity: 0, selected: false };
bodyState = { ...bodyState, [id]: { ...cur, selected: !cur.selected } };
chart.update({ bodyState });
},
});
});
onDestroy(() => chart?.destroy());
</script>
<div bind:this={container} />