diff --git a/examples/example13-trace-hover-highlighting-demo.fixture.tsx b/examples/example13-trace-hover-highlighting-demo.fixture.tsx
new file mode 100644
index 0000000..7889a98
--- /dev/null
+++ b/examples/example13-trace-hover-highlighting-demo.fixture.tsx
@@ -0,0 +1,64 @@
+import { renderToCircuitJson } from "lib/dev/render-to-circuit-json";
+import { SchematicViewer } from "lib/index";
+
+const circuit = (
+
+ {/* Components */}
+
+
+
+
+
+
+
+
+ {/* VCC POWER RAIL: Multiple traces connecting VCC pins and capacitor positive terminals */}
+ .pin8"} to={".U2 > .pin8"} />
+ .pin8"} to={".C1 > .pin1"} />
+ .pin1"} to={".C2 > .pin1"} />
+
+ {/* GROUND RAIL: Multiple traces connecting GND pins and capacitor negative terminals */}
+ .pin4"} to={".U2 > .pin4"} />
+ .pin4"} to={".C1 > .pin2"} />
+ .pin2"} to={".C2 > .pin2"} />
+ .pin2"} to={".R1 > .pin2"} />
+
+ {/* SIGNAL NET 1: U1 output through R2 to LED */}
+ .pin1"} to={".R2 > .pin1"} />
+ .pin2"} to={".LED1 > .anode"} />
+
+ {/* SIGNAL NET 2: U1 to U2 communication */}
+ .pin2"} to={".U2 > .pin1"} />
+
+ {/* SIGNAL NET 3: U2 output through R1 to ground (current sink) */}
+ .pin7"} to={".R1 > .pin1"} />
+
+ {/* LED cathode to ground */}
+ .cathode"} to={".U2 > .pin4"} />
+
+ {/* Internal chip connections (isolated) */}
+ .pin3"} to={".U1 > .pin6"} />
+ .pin2"} to={".U2 > .pin3"} />
+
+);
+
+/**
+ * Example showcasing the new trace hover highlighting feature
+ *
+ * Hover over any trace to see all connected traces in the same net highlighted.
+ * This helps visualize circuit connectivity and debug routing issues.
+ */
+export default () => {
+ const circuitJson = renderToCircuitJson(circuit);
+
+ return (
+
+
+
+ );
+};
diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx
index cdbb89a..f30fc52 100644
--- a/lib/components/SchematicViewer.tsx
+++ b/lib/components/SchematicViewer.tsx
@@ -5,6 +5,7 @@ import {
import { useChangeSchematicComponentLocationsInSvg } from "lib/hooks/useChangeSchematicComponentLocationsInSvg"
import { useChangeSchematicTracesForMovedComponents } from "lib/hooks/useChangeSchematicTracesForMovedComponents"
import { useSchematicGroupsOverlay } from "lib/hooks/useSchematicGroupsOverlay"
+import { useConnectedTracesHoverHighlighting } from "lib/hooks/useConnectedTracesHoverHighlighting"
import { enableDebug } from "lib/utils/debug"
import { useEffect, useMemo, useRef, useState } from "react"
import {
@@ -243,6 +244,14 @@ export const SchematicViewer = ({
showGroups: showSchematicGroups,
})
+ // Add trace hover highlighting
+ useConnectedTracesHoverHighlighting({
+ svgDivRef,
+ circuitJson,
+ circuitJsonKey,
+ enabled: true,
+ })
+
const svgDiv = useMemo(
() => (
;
+ circuitJson: any[];
+ circuitJsonKey?: string;
+ enabled?: boolean;
+}
+
+/**
+ * Optimized trace highlighting using CSS classes and circuit-to-svg metadata
+ */
+export const useConnectedTracesHoverHighlighting = ({
+ svgDivRef,
+ circuitJson,
+ circuitJsonKey,
+ enabled = true,
+}: useConnectedTracesHoverHighlightingOptions) => {
+ const activeNetRef = useRef(null);
+ const timeoutRef = useRef | null>(null);
+
+ useEffect(() => {
+ if (!enabled || !svgDivRef.current || !circuitJson || !circuitJsonKey) {
+ return;
+ }
+
+ const svgContainer = svgDivRef.current;
+
+ const handleTraceHover = (event: Event) => {
+ const target = event.currentTarget as SVGElement;
+
+ const traceId =
+ target.getAttribute("data-schematic-trace-id") ||
+ target.getAttribute("data-circuit-json-type") === "schematic_trace"
+ ? target.getAttribute("data-schematic-trace-id")
+ : null;
+
+ if (!traceId) return;
+
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+
+ const connectedTraces = findConnectedTraceIds(circuitJson, traceId);
+ activeNetRef.current = traceId;
+
+ const svgElement = svgContainer.querySelector("svg");
+ if (svgElement) {
+ svgElement.querySelectorAll(".trace-highlighted").forEach((el) => {
+ el.classList.remove("trace-highlighted");
+ });
+
+ connectedTraces.forEach((connectedTraceId) => {
+ const traceElement = svgElement.querySelector(
+ `[data-schematic-trace-id="${connectedTraceId}"]`
+ );
+ if (traceElement) {
+ traceElement.classList.add("trace-highlighted");
+ }
+ });
+ }
+ };
+
+ const handleTraceLeave = () => {
+ timeoutRef.current = setTimeout(() => {
+ const svgElement = svgContainer.querySelector("svg");
+ if (svgElement) {
+ svgElement.querySelectorAll(".trace-highlighted").forEach((el) => {
+ el.classList.remove("trace-highlighted");
+ });
+ }
+ activeNetRef.current = null;
+ }, 50);
+ };
+
+ const observer = new MutationObserver(() => {
+ const svgElement = svgContainer.querySelector("svg");
+ if (svgElement) {
+ addTraceHighlightingStyles(svgContainer);
+
+ const traceElements = svgElement.querySelectorAll(
+ 'g.trace[data-circuit-json-type="schematic_trace"], [data-schematic-trace-id]'
+ );
+
+ if (traceElements.length > 0) {
+ traceElements.forEach((el) => {
+ el.addEventListener("mouseenter", handleTraceHover);
+ el.addEventListener("mouseleave", handleTraceLeave);
+ });
+
+ observer.disconnect();
+ }
+ }
+ });
+
+ observer.observe(svgContainer, { childList: true, subtree: true });
+
+ return () => {
+ observer.disconnect();
+
+ const svgElement = svgContainer.querySelector("svg");
+ if (svgElement) {
+ const traceElements = svgElement.querySelectorAll(
+ 'g.trace[data-circuit-json-type="schematic_trace"], [data-schematic-trace-id]'
+ );
+ traceElements.forEach((el) => {
+ el.removeEventListener("mouseenter", handleTraceHover);
+ el.removeEventListener("mouseleave", handleTraceLeave);
+ });
+ }
+
+ removeTraceHighlightingStyles(svgContainer);
+
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+ };
+ }, [svgDivRef, circuitJsonKey, enabled]);
+
+ return {
+ currentHighlightedNet: activeNetRef.current,
+ };
+};
diff --git a/lib/utils/trace-connectivity.ts b/lib/utils/trace-connectivity.ts
new file mode 100644
index 0000000..d9c7303
--- /dev/null
+++ b/lib/utils/trace-connectivity.ts
@@ -0,0 +1,62 @@
+import { su } from "@tscircuit/soup-util";
+
+/**
+ * Finds all schematic traces that are electrically connected to the given trace
+ * @param circuitJson The circuit JSON data
+ * @param hoveredSchematicTraceId The ID of the trace being hovered
+ * @returns Array of connected trace IDs (including the original trace)
+ */
+export const findConnectedTraceIds = (
+ circuitJson: any[],
+ hoveredSchematicTraceId: string
+): string[] => {
+ try {
+ const soup = su(circuitJson);
+
+ const schematicTrace = soup.schematic_trace.get(hoveredSchematicTraceId);
+ if (!schematicTrace) {
+ return [hoveredSchematicTraceId];
+ }
+
+ const allSchematicTraces = soup.schematic_trace.list();
+ const allSourceTraces = soup.source_trace.list();
+
+ let sourceTrace = soup.source_trace.get(schematicTrace.source_trace_id);
+
+ // Fallback: find by index if ID mismatch
+ if (!sourceTrace) {
+ const schematicTraceIndex = parseInt(
+ hoveredSchematicTraceId.split("_").pop() || "0"
+ );
+ if (schematicTraceIndex < allSourceTraces.length) {
+ sourceTrace = allSourceTraces[schematicTraceIndex];
+ }
+ }
+
+ if (!sourceTrace) {
+ return [hoveredSchematicTraceId];
+ }
+
+ const connectedTraceIds = new Set([hoveredSchematicTraceId]);
+
+ // Find traces with same connectivity key
+ const connectivityKey = sourceTrace.subcircuit_connectivity_map_key;
+ if (connectivityKey) {
+ for (const otherSourceTrace of allSourceTraces) {
+ if (otherSourceTrace.subcircuit_connectivity_map_key === connectivityKey) {
+ const sourceTraceIndex = allSourceTraces.findIndex(
+ (st) => st.source_trace_id === otherSourceTrace.source_trace_id
+ );
+ if (sourceTraceIndex >= 0 && sourceTraceIndex < allSchematicTraces.length) {
+ const mappedSchematicTrace = allSchematicTraces[sourceTraceIndex];
+ connectedTraceIds.add(mappedSchematicTrace.schematic_trace_id);
+ }
+ }
+ }
+ }
+
+ return Array.from(connectedTraceIds);
+ } catch (error) {
+ return [hoveredSchematicTraceId];
+ }
+};
diff --git a/lib/utils/trace-highlighting-styles.ts b/lib/utils/trace-highlighting-styles.ts
new file mode 100644
index 0000000..9fb29ff
--- /dev/null
+++ b/lib/utils/trace-highlighting-styles.ts
@@ -0,0 +1,58 @@
+/**
+ * Optimized CSS-only approach for trace highlighting
+ * Instead of manipulating SVG, we inject CSS that works with existing circuit-to-svg structure
+ */
+export const addTraceHighlightingStyles = (svgContainer: HTMLElement): void => {
+ // Check if styles already added
+ const existingStyle = svgContainer.querySelector(
+ "style[data-trace-highlighting]"
+ );
+ if (existingStyle) {
+ return;
+ }
+
+ // Create style element
+ const styleElement = document.createElement("style");
+ styleElement.setAttribute("data-trace-highlighting", "true");
+
+ styleElement.textContent = `
+ /* Match the original .trace:hover behavior exactly */
+
+ /* Use the same filter effect as the original hover */
+ svg .trace-highlighted {
+ filter: invert(1) !important;
+ }
+
+ /* Hide crossing outlines on highlighted traces - matches original */
+ svg .trace-highlighted .trace-crossing-outline {
+ // opacity: 0 !important;
+ }
+
+ /* Ensure pointer cursor for all trace groups */
+ svg g.trace[data-circuit-json-type="schematic_trace"]:hover {
+ cursor: pointer !important;
+ }
+
+ /* Alternative selector for data-schematic-trace-id elements */
+ svg [data-schematic-trace-id]:hover {
+ cursor: pointer !important;
+ }
+ `;
+
+ // Add to container (not inside SVG)
+ svgContainer.appendChild(styleElement);
+};
+
+/**
+ * Remove trace highlighting styles
+ */
+export const removeTraceHighlightingStyles = (
+ svgContainer: HTMLElement
+): void => {
+ const styleElement = svgContainer.querySelector(
+ "style[data-trace-highlighting]"
+ );
+ if (styleElement) {
+ styleElement.remove();
+ }
+};