Interactive with React State

Adjust the controls to see the layout update in real-time - including the margin the Graphic reserves around the content

import { Graphic } from "@modular-svg/react";
import { useState } from "react";

export function InteractiveState() {
	const [radius, setRadius] = useState(20);
	const [spacing, setSpacing] = useState(10);
	const [showRect, setShowRect] = useState(true);
	const [margin, setMargin] = useState(10);

	return (
		<div>
			<div style={{ marginBottom: "1rem" }}>
				<label style={{ display: "block", marginBottom: "0.5rem" }}>
					Circle Radius: {radius}px
					<input
						type="range"
						min="10"
						max="40"
						value={radius}
						onChange={(e) => setRadius(Number(e.target.value))}
						style={{ marginLeft: "1rem", width: "200px" }}
					/>
				</label>
				<label style={{ display: "block", marginBottom: "0.5rem" }}>
					Spacing: {spacing}px
					<input
						type="range"
						min="5"
						max="30"
						value={spacing}
						onChange={(e) => setSpacing(Number(e.target.value))}
						style={{ marginLeft: "1rem", width: "200px" }}
					/>
				</label>
				<label style={{ display: "block", marginBottom: "0.5rem" }}>
					Graphic margin: {margin}px
					<input
						type="range"
						min="0"
						max="50"
						value={margin}
						onChange={(e) => setMargin(Number(e.target.value))}
						style={{ marginLeft: "1rem", width: "200px" }}
					/>
				</label>
				<label style={{ display: "block", marginBottom: "0.5rem" }}>
					<input
						type="checkbox"
						checked={showRect}
						onChange={(e) => setShowRect(e.target.checked)}
						style={{ marginRight: "0.5rem" }}
					/>
					Show Rectangle
				</label>
			</div>
			<Graphic
				style={{
					border: "1px solid #ddd",
					borderRadius: "8px",
					display: "inline-block",
				}}
				margin={margin}
			>
				<stackH spacing={spacing}>
					<circle r={radius} fill="hotpink" />
					{showRect && <rect width={radius * 2} height={radius} fill="gold" />}
					<circle r={radius} fill="cyan" />
				</stackH>
			</Graphic>
		</div>
	);
}

Interactive Event Handlers

Click on circles to see event handlers in action

import { Graphic } from "@modular-svg/react";
import { useState } from "react";

export function EventHandlers() {
	const [clicked, setClicked] = useState<string | null>(null);
	const [hovered, setHovered] = useState<string | null>(null);

	const circles = [
		{
			key: "red-circle",
			name: "Red Circle",
			fill: "#ff0000",
			hover: "#ff6666",
			stroke: "#990000",
		},
		{
			key: "green-circle",
			name: "Green Circle",
			fill: "#00ff00",
			hover: "#66ff66",
			stroke: "#009900",
		},
		{
			key: "blue-circle",
			name: "Blue Circle",
			fill: "#0000ff",
			hover: "#6666ff",
			stroke: "#000099",
		},
	];

	return (
		<div>
			<p style={{ color: "#333", fontSize: "14px", marginBottom: "1rem" }}>
				{clicked ? `You clicked: ${clicked}` : "Click on any circle below"}
			</p>
			<Graphic
				style={{
					border: "1px solid #ddd",
					borderRadius: "8px",
					display: "inline-block",
					cursor: "pointer",
				}}
				margin={10}
				title="Interactive color circles"
			>
				<stackH spacing={20}>
					{circles.map((c) => (
						<circle
							key={c.key}
							r={25}
							fill={hovered === c.name ? c.hover : c.fill}
							stroke={c.stroke}
							stroke-width={hovered === c.name ? 3 : 1}
							onClick={() => setClicked(c.name)}
							onMouseEnter={() => setHovered(c.name)}
							onMouseLeave={() => setHovered(null)}
						/>
					))}
				</stackH>
			</Graphic>
		</div>
	);
}

Click on any circle below

React Context Forwarding

React contexts automatically flow into Graphic children - no special setup required

import { Graphic } from "@modular-svg/react";
import { createContext, useContext, useState } from "react";

const ThemeContext = createContext<"light" | "dark">("light");
const SizeScaleContext = createContext(1);

function ThemedCircle() {
	const theme = useContext(ThemeContext);
	const scale = useContext(SizeScaleContext);

	const colors = {
		light: { fill: "#3b82f6", stroke: "#1e40af" },
		dark: { fill: "#1e293b", stroke: "#475569" },
	};

	return (
		<circle
			r={20 * scale}
			fill={colors[theme].fill}
			stroke={colors[theme].stroke}
			stroke-width={2}
		/>
	);
}

export function ContextForwarding() {
	const [theme, setTheme] = useState<"light" | "dark">("light");
	const [scale, setScale] = useState(1);

	return (
		<ThemeContext.Provider value={theme}>
			<SizeScaleContext.Provider value={scale}>
				<div>
					<div style={{ marginBottom: "1rem" }}>
						<label style={{ display: "block", marginBottom: "0.5rem" }}>
							<strong>Theme Context:</strong>
							<select
								value={theme}
								onChange={(e) => setTheme(e.target.value as "light" | "dark")}
								style={{ marginLeft: "1rem", padding: "4px 8px" }}
							>
								<option value="light">Light</option>
								<option value="dark">Dark</option>
							</select>
						</label>
						<label style={{ display: "block", marginBottom: "0.5rem" }}>
							<strong>Size Scale Context:</strong> {scale.toFixed(1)}x
							<input
								type="range"
								min="0.5"
								max="2"
								step="0.1"
								value={scale}
								onChange={(e) => setScale(Number(e.target.value))}
								style={{ marginLeft: "1rem", width: "200px" }}
							/>
						</label>
					</div>
					<Graphic
						style={{
							border: "1px solid #ddd",
							borderRadius: "8px",
							display: "inline-block",
						}}
						margin={10}
						data-testid="context-canvas"
					>
						<stackH spacing={15}>
							<ThemedCircle />
							<ThemedCircle />
							<ThemedCircle />
						</stackH>
					</Graphic>
				</div>
			</SizeScaleContext.Provider>
		</ThemeContext.Provider>
	);
}