Interactive SVG body map with 70+ muscles, intensity visualization, and zero dependencies.
Works with React, Vue, Svelte, Angular, or vanilla JavaScript. No framework required.
file:// too)npm install body-muscles
yarn add body-muscles
pnpm add body-muscles
CDN (ESM):
<script type="module">
import { BodyChart, ViewSide } from "https://esm.sh/body-muscles";
</script>
CDN (UMD) — no bundler needed, works from filesystem:
<script src="https://unpkg.com/body-muscles/dist/umd/body-muscles.umd.min.js"></script>
<script>
const { BodyChart, ViewSide } = BodyMuscles;
</script>
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 an interactive body map inside the given DOM element.
| Option | Type | Default | Description |
|---|---|---|---|
view |
ViewSide |
— | FRONT or BACK anatomical view |
bodyState |
BodyState |
— | Map of muscle IDs to intensity & selection |
onMuscleClick |
(id: MuscleId, name: string) => void |
() => {} |
Click handler |
onMuscleHover |
(id: MuscleId | null) => void |
() => {} |
Hover state change handler |
className |
string |
"" |
CSS class for the container wrapper |
ariaLabel |
string |
"" |
Accessibility label for the SVG |
showViewLabel |
boolean |
false |
Show "Front View" / "Back View" indicator |
enableTransitions |
boolean |
true |
Smooth CSS transitions on state changes |
| Method | Description |
|---|---|
update(options: Partial<Options>) |
Merge new options. View change triggers re-render. |
destroy() |
Remove all DOM elements and 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 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 src="https://unpkg.com/body-muscles/dist/umd/body-muscles.umd.min.js"></script>
<script>
const { BodyChart, ViewSide } = BodyMuscles;
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} />
{muscle_group}-{side} → biceps-left
{muscle_group}-{sub_group}-{side} → shoulder-front-left
{singular} → spine
body-muscles/
├── src/
│ ├── BodyChart.ts # Main class
│ ├── types.ts # TypeScript types & utilities
│ ├── index.ts # Public exports
│ ├── data/ # SVG path data & muscle definitions
│ └── utils/ # getMuscleColor, filterMuscles
├── dist/
│ ├── esm/ # ESM build
│ └── umd/ # UMD build (browser/CDN)
├── docs/ # Documentation site
├── scripts/ # Build scripts
├── package.json
└── tsconfig.json