Python supervision uses annotators such as BoxAnnotator,
MaskAnnotator, and LabelAnnotator to decide how detections are drawn.
In supervision-js, the equivalent concept is split into two public parts:
This keeps detections as semantic model output while the renderer owns the performance-sensitive drawing strategy.
For focused, live examples, open
Annotation Renderers. Each renderer page with a
playground runs it on a committed fixture, and its controls update both the
scene and a minimal setPresentation() snippet.
Use BaseBoxStyle, BaseMaskStyle, BasePolygonStyle,
BasePolylineStyle, BaseKeypointStyle, BaseLabelStyle,
BaseInteractionStyle, and BaseFocusStyle for the common path:
const session = await createMediaSession({
container,
media,
presentation: {
focusStyle: new BaseFocusStyle(),
interactionStyle: new BaseInteractionStyle(),
renderers: [
annotationRenderers.box({ style: new BaseBoxStyle() }),
annotationRenderers.mask({
style: new BaseMaskStyle({ opacity: 0.5 }),
}),
annotationRenderers.label({
style: new BaseLabelStyle({ includeConfidence: true }),
}),
],
},
});
Box shape is just another style option. Use shape and cornerRadius when
rounded rectangles are the desired box treatment:
session.setPresentation({
renderers: [
annotationRenderers.box({
style: new BaseBoxStyle({
cornerRadius: 8,
shape: BoxShape.RoundedRect,
stroke: {
alignment: BoxStrokeAlignment.Inside,
color: 0x38bdf8,
width: 3,
},
}),
}),
],
});
Enable antialiasing for the annotations, focus, and interaction overlays with one presentation option:
session.setPresentation({ annotationAntialiasing: true });
true enables FXAA at output resolution; false disables smoothing. Label
backgrounds are smoothed while text stays sharp. Consecutive
labels that do not overlap can share a background capture; overlaps split
captures to preserve blending and draw order.
session.setPresentation({ annotationAntialiasing: 2 });
2 applies FXAA with a capture at twice the output density in each dimension,
with four times as many capture pixels. The output canvas DPR stays unchanged.
Video frames retain their original rendering. Region effects smooth their coverage boundary without filtering the video inside it.
Antialiasing is off by default. A 2× capture preserves mask detail up to the original resolution before smoothing, independently of video. This adds mask preparation work and cache bytes within the configured memory budget, as well as rendering work and offscreen GPU textures. Motion previews use the configured fraction of that finer mask resolution. The renderer can retain offscreen textures for reuse after antialiasing is disabled or DPR changes. Capture resolution adjusts to the GPU's texture-size limit on large displays. A low-resolution mask preview still limits contour detail; use the mask quality controls when more source detail is needed.
Style options accept either static values or resolver functions. Use static values for global styling:
const maskStyle = new BaseMaskStyle({
color: 0x38bdf8,
mode: MaskRenderMode.FillAndStroke,
opacity: 0.65,
stroke: {
alpha: 1,
color: 0xe0f2fe,
width: 4,
},
});
Use MaskRenderMode.FillOnly or MaskRenderMode.StrokeOnly for fill-only or
outline-only masks. Stroke-only masks default to a 1px same-color outline when
no explicit stroke is provided.
Use resolver functions for per-class, per-confidence, or frame-aware styling:
const boxStyle = new BaseBoxStyle({
cornerRadius: (detection) => (detection.className === "basketball" ? 999 : 8),
fill: (detection) => ({
alpha: 0.15,
color: detection.className === "person" ? 0x22c55e : 0xa855f7,
}),
shape: BoxShape.RoundedRect,
shouldRender: (detection) => (detection.confidence ?? 0) >= 0.5,
stroke: (detection) => ({
alpha: 1,
color: detection.className === "person" ? 0x22c55e : 0xa855f7,
width: 3,
}),
});
Keep box variants in options rather than wrapper classes. That makes the style surface easier to compose as more visual knobs arrive.
Labels resolve from className, metadata.label, or a custom text
resolver. Confidence can be included without storing display text on the
detection:
const labelStyle = new BaseLabelStyle({
background: (detection) => ({
alpha: 0.78,
color: detection.className === "basketball" ? 0x7c2d12 : 0x111827,
}),
includeConfidence: true,
textStyle: {
color: 0xffffff,
fontSize: 14,
fontWeight: "700",
},
});
Use offset when labels need to move away from the default top-left box edge:
const labelStyle = new BaseLabelStyle({
background: {
cornerRadius: 6,
paddingX: 8,
paddingY: 4,
},
includeConfidence: true,
offset: (detection) => ({
x: detection.className === "basketball" ? 4 : 0,
y: 8,
}),
placement: LabelPlacement.Bottom,
});
Set labels to appear only for the active hover target when persistent labels would be too dense:
const labelStyle = new BaseLabelStyle({
includeConfidence: true,
visibilityMode: LabelVisibilityMode.HoveredOnly,
});
Vector geometry uses the same static-or-resolver style model:
const polygonStyle = new BasePolygonStyle({
fill: { alpha: 0.18, color: 0x22c55e },
stroke: { alpha: 1, color: 0x86efac, width: 3 },
});
const polylineStyle = new BasePolylineStyle({
shadowStroke: { alpha: 0.55, color: 0x000000, width: 6 },
stroke: { alpha: 1, color: 0x38bdf8, width: 4 },
});
const keypointStyle = new BaseKeypointStyle({
edgeShadowStroke: { alpha: 0.65, color: 0x000000, width: 4 },
edgeStroke: { alpha: 1, color: 0x22c55e, width: 2 },
markerFill: { alpha: 1, color: 0x22c55e },
markerStroke: { alpha: 1, color: 0xffffff, width: 2 },
radius: 6,
});
session.setPresentation({
renderers: [
annotationRenderers.keypoints({ style: keypointStyle }),
annotationRenderers.polygon({ style: polygonStyle }),
annotationRenderers.polyline({ style: polylineStyle }),
],
});
BaseKeypointStyle draws NotLabeled points as absent, Occluded points as
crosses, and Visible points as circles. Pass definitions when class-specific
skeleton vertices and edges need their own colors.
A contrast stroke drawn under the line keeps a thin class-colored path readable
where the media beneath it happens to share that color. BasePolylineStyle
takes it as shadowStroke, BaseKeypointStyle as edgeShadowStroke, and the
default presentation supplies one for both. Pass null to remove it.
Use the shared resolver when boxes, masks, labels, polygons, and keypoints should agree on class color:
const boxStyle = new BaseBoxStyle({
stroke: (detection) => ({
alpha: 1,
color: resolveDetectionClassColorStyle(detection.className).stroke,
width: 3,
}),
});
const labelStyle = new BaseLabelStyle({
background: (detection) => ({
alpha: 0.85,
color: resolveDetectionClassColorStyle(detection.className).labelBackground,
}),
textStyle: (detection) => ({
color: resolveDetectionClassColorStyle(detection.className).labelText,
}),
});
Known classes use DEFAULT_DETECTION_CLASS_STYLES. Unknown names are normalized
and deterministically assigned from DEFAULT_DETECTION_COLOR_SEQUENCE.
Presentation can change without rewriting detections:
session.setPresentation({
renderers: [
annotationRenderers.box({ style: boxStyle }),
annotationRenderers.keypoints({ style: keypointStyle }),
annotationRenderers.label({ style: labelStyle }),
annotationRenderers.mask({ style: maskStyle }),
annotationRenderers.polygon({ style: polygonStyle }),
annotationRenderers.polyline({ style: polylineStyle }),
],
});
The renderer list is authoritative: omit a renderer to disable it, and use an
empty list to disable every built-in annotation renderer. The direct
boxStyle, maskStyle, and related presentation fields remain supported for
compatibility and source-specific overrides, but new global presentation code
should prefer renderers.
Global annotation visibility can hide annotations, labels, classes, or specific detection IDs without mutating semantic frames:
session.setPresentation({
visibility: {
hiddenClasses: ["background"],
hiddenDetectionIds: ["suppressed-1"],
labelsHidden: false,
},
});
For masks, the renderer may reuse prepared ID-mask artifacts when the new style can be applied through the shader palette. If a style change affects which masks exist or how mask borders are prepared, the renderer rebuilds the affected prepared artifacts in the background.
Interaction styles draw hover and selected states in a separate overlay layer. They resolve to the same box, mask, and label style contracts as the base presentation. Pointer movement does not rebuild prepared mask artifacts:
session.setPresentation({
interactionStyle: new BaseInteractionStyle({
hovered: {
maskStyle: new BaseMaskStyle({
color: 0x38bdf8,
opacity: 0.18,
stroke: { alpha: 0.9, color: 0x67e8f9, width: 3 },
}),
},
selected: {
maskStyle: new BaseMaskStyle({
color: 0x38bdf8,
opacity: 0.28,
stroke: { alpha: 1, color: 0xfde047, width: 5 },
}),
},
}),
});
Focus styles dim the media around the selected or hovered detections. Annotation fills, outlines, and labels remain above the dim overlay. Masks and polygons keep their actual shapes; heatmap cutouts follow the regions that display visible colour, including changes to the cutoff and opacity. Other detections use the styled rectangle fallback:
session.setPresentation({
focusStyle: new BaseFocusStyle({
fill: {
alpha: 0.5,
color: 0x020617,
},
targetMode: FocusTargetMode.Selected,
}),
});
Set shape: null to disable rectangular fallback. Masks, polygons, and ready
heatmap regions still cut out their actual shapes; rectangle-only detections
remain dimmed. Heatmap cutouts use the current frame's visible heat and disappear
when that heat is absent or still loading.
Custom styles implement the same resolve(detection, context) contract as the
base styles. Return a draw instruction to render the detection, or undefined
to skip it.
const onlyPlayers: BoxStyle = {
resolve(detection) {
if (detection.className !== "player" || !detection.rect) {
return undefined;
}
return {
rect: detection.rect,
shape: BoxShape.Rect,
stroke: {
alpha: 1,
color: 0xfacc15,
width: 2,
},
};
},
};
Keep style decisions in styles. Keep detection frames focused on model output: geometry, masks, class names, confidence, ids, and metadata.