> For the complete documentation index, see [llms.txt](https://docs.apryse.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.apryse.com/web/get-started/samples/showcase-demo-custom-annotations.md).

# Custom Annotations Showcase Demo Code Sample

Draw custom annotations choosing from multiple animated annotations, color, opacity, and stroke thickness

{% hint style="info" %}
**Requirements**

*These packages are required to use these features in production. Trial keys have unlimited access to all features*

<a href="/web/get-started/readme.md" class="button primary">Web SDK</a><a href="https://showcase.apryse.com/custom-annotations" class="button primary">Live demo</a>
{% endhint %}

Easily draw custom annotations in a PDF document. Set your annotation customization from multiple options in an interactive fashion that set the color, opacity, and stroke thickness.

This demo lets you:

* Upload a PDF file to which customized annotations can be added
* Customize annotations using controls that set the color, opacity, and stroke thickness
* Add custom annotations

### **Implementation steps**

To add custom annotations capability to a PDF with WebViewer:

Step 1: Choose your [get started with your preferred web stack for WebViewer](/web/get-started/readme.md) Step 2: Add the ES6 JavaScript sample code provided in this guide

Once you generate your license key, it will automatically be included in your sample code below.

{% @apryse-license-key/apryse-license-key platform="WEB\_VIEWER" variant="compact" %}

{% tabs %}
{% tab title="index.js" %}

<pre class="language-js" data-line-numbers><code class="lang-js">// ES6 Compliant Syntax
// GitHub Copilot v1.0, GPT-4.1, September 11, 2025
// File: index.js

import WebViewer from '@pdftron/webviewer';

// Custom Annotation Demo
// 
// This code demonstrates how to draw custom annotations inside a document
// and customize their appearance, including color, opacity and 
// stroke thickness.
//

// Custom Triangle Annotation Tool name
const TRIANGLE_TOOL_NAME = 'AnnotationCreateTriangle';

// Default Document
const defaultDoc = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/comp_sci_cheatsheet.pdf';

// Default custom annotation properties
let fillColor = '#F7D038';
let strokeColor = '#EB7532';
let width = 5;
let isCustomToolActive = true;
let opacity = 1;

// Tool instance
let toolInstance = null;

// Default colors for color picker
const DEFAULT_COLORS = [
    '#EB7532', // Orange
    '#F7D038', // Yellow
    '#A3E048', // Green
    '#519A64', // Dark Green
    '#33BBE6', // Blue
    '#4355DB', // Dark Blue
    '#D23BE7', // Purple
    '#E6261F', // Red
]

const customizeUI = async (instance) => {
    // Load default document
    await instance.Core.documentViewer.loadDocument(defaultDoc, {
        extension: 'pdf',
    });

    // Initialize custom annotation tool
    await initCustomAnnotTool(instance);
    setToolInstanceStyle(instance);

    // Add event listeners
    const { documentViewer } = instance.Core;
    documentViewer.addEventListener('toolModeUpdated', toolModeUpdated);
    documentViewer.addEventListener('toolUpdated', setUIState);
    documentViewer.addEventListener('documentLoaded', setTriangleTool);
};

const setTriangleTool = (instance) => {
    try {
        instance.UI.setToolMode(TRIANGLE_TOOL_NAME);
    } catch (error) {
        console.error('Calling setToolMode on instance error:', error);
    }
};

const setUIState = (updatedTool) => {
    if (isCustomToolActive) {
        const { defaults } = updatedTool;
        const { StrokeColor, FillColor, StrokeThickness, Opacity } = defaults;

        const fill = !FillColor.toHexString() ? '#000000' : FillColor.toHexString();
        const stroke = !StrokeColor.toHexString() ? '#000000' : StrokeColor.toHexString();
        fillColor = fill;
        strokeColor = stroke;
        width = Math.floor(StrokeThickness);
        opacity = Opacity;
    }
};

const toolModeUpdated = (type) => {
    const { name } = type;
    isCustomToolActive = (name === TRIANGLE_TOOL_NAME);
};

const setToolInstanceStyle = (instance) => {
    const { Annotations } = instance.Core;
    const style = toolInstance.defaults;
    toolInstance.setStyles({
        ...style,
        FillColor: new Annotations.Color(fillColor),
        StrokeColor: new Annotations.Color(strokeColor),
        StrokeThickness: width,
        Opacity: opacity,
    });
};

const initCustomAnnotTool = async (instance) => {
    const { Annotations, documentViewer, Tools } = instance.Core;
    Annotations.SelectionAlgorithm.canvasVisibilityPadding = 50;

    const TriangleAnnotation = TriangleAnnotationFactory(Annotations, documentViewer);
    const TriangleCreateTool = TriangleCreateToolFactory(Annotations, Tools, TriangleAnnotation);

    // Register the annotation type so that it can be saved to XFDF
    documentViewer
        .getAnnotationManager()
        .registerAnnotationType(TriangleAnnotation.prototype.elementName, TriangleAnnotation);
    toolInstance = new TriangleCreateTool(documentViewer);
    toolInstance.defaults.FillColor = new Annotations.Color(fillColor);
    toolInstance.defaults.StrokeColor = new Annotations.Color(strokeColor);
    toolInstance.defaults.StrokeThickness = width;
    toolInstance.defaults.Opacity = opacity;
    instance.UI.registerTool(
        {
            toolName: TRIANGLE_TOOL_NAME,
            toolObject: toolInstance,
            buttonImage:
                '&#x3C;svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">' +
                '&#x3C;path d="M12 7.77L18.39 18H5.61L12 7.77M12 4L2 20h20L12 4z"/>' +
                '&#x3C;path fill="none" d="M0 0h24v24H0V0z"/>' +
                '&#x3C;/svg>',
            buttonName: 'triangleToolButton',
            tooltip: 'Triangle',
        },
        TriangleAnnotation
    );

    // documentLoaded has already happened by the time this file is loaded, so use a different event
    await documentViewer.getAnnotationsLoadedPromise();

    const topHeader = instance.UI.getModularHeader('tools-header');
    // This is the ribbonGroup already
    const shapesGroup = topHeader.getItems()[1];
    const shapesGroupedItems = shapesGroup.items[0];

    const customTriangleButton = new instance.UI.Components.ToolButton({
        dataElement: 'custom-triangle-button',
        toolName: TRIANGLE_TOOL_NAME,
    });

    shapesGroupedItems.setItems([...shapesGroupedItems.items, customTriangleButton]);

    setTriangleTool(instance);
};

// Custom Triangle Annotation section
//
// This code defines a custom triangle annotation
//

// Control Handler
const TriangleControlHandleFactory = (Annotations) => {
    const TriangleControlHandle = function (annotation, index) {
        this.annotation = annotation;
        // set the index of this control handle so that we know which vertex it corresponds to
        this.index = index;
    };

    TriangleControlHandle.prototype = new Annotations.ControlHandle();

    // returns a rect that should represent the control handle's position and size
    TriangleControlHandle.prototype.getDimensions = function (annotation, selectionBox, zoom) {
        let x = annotation.vertices[this.index].x;
        let y = annotation.vertices[this.index].y;
        const width = Annotations.ControlHandle.handleWidth / zoom;
        const height = Annotations.ControlHandle.handleHeight / zoom;

        // adjust for the control handle's own width and height
        x -= width * 0.5;
        y -= height * 0.5;
        return new Annotations.Rect(x, y, x + width, y + height);
    };

    TriangleControlHandle.prototype.draw = function (ctx, annotation, selectionBox, zoom) {
        const dim = this.getDimensions(annotation, selectionBox, zoom);
        ctx.fillStyle = '#FFFFFF';
        ctx.beginPath();
        ctx.moveTo(dim.x1 + dim.getWidth() / 2, dim.y1);
        ctx.lineTo(dim.x1 + dim.getWidth(), dim.y1 + dim.getHeight());
        ctx.lineTo(dim.x1, dim.y1 + dim.getHeight());
        ctx.closePath();
        ctx.stroke();
        ctx.fill();
    };

    // this function is called when the control handle is dragged
    TriangleControlHandle.prototype.move = function (annotation, deltaX, deltaY) {
        annotation.vertices[this.index].x += deltaX;
        annotation.vertices[this.index].y += deltaY;

        // recalculate the X, Y, width and height of the annotation
        let minX = Number.MAX_VALUE;
        let maxX = -Number.MAX_VALUE;
        let minY = Number.MAX_VALUE;
        let maxY = -Number.MAX_VALUE;
        for (let i = 0; i &#x3C; annotation.vertices.length; ++i) {
            const vertex = annotation.vertices[i];
            minX = Math.min(minX, vertex.x);
            maxX = Math.max(maxX, vertex.x);
            minY = Math.min(minY, vertex.y);
            maxY = Math.max(maxY, vertex.y);
        }

        const rect = new Annotations.Rect(minX, minY, maxX, maxY);
        annotation.setRect(rect);
        // return true if redraw is needed
        return true;
    };
    return TriangleControlHandle;
};

// Selection Model
const TriangleSelectionModelFactory = (Annotations, documentViewer) => {
    const TriangleSelectionModel = function (annotation, canModify) {
        Annotations.SelectionModel.call(this, annotation, canModify, false, documentViewer);
        if (canModify) {
            const controlHandles = this.getControlHandles();
            const TriangleControlHandle = TriangleControlHandleFactory(Annotations);
            controlHandles.push(new TriangleControlHandle(annotation, 0));
            controlHandles.push(new TriangleControlHandle(annotation, 1));
            controlHandles.push(new TriangleControlHandle(annotation, 2));
        }
    };

    TriangleSelectionModel.prototype = new Annotations.SelectionModel();

    TriangleSelectionModel.prototype.drawSelectionOutline = function (ctx, annotation, zoom) {
        if (typeof zoom !== 'undefined') {
            ctx.lineWidth = Annotations.SelectionModel.selectionOutlineThickness / zoom;
        } else {
            ctx.lineWidth = Annotations.SelectionModel.selectionOutlineThickness;
        }

        // changes the selection outline color if the user doesn't have permission to modify this annotation
        if (this.canModify()) {
            ctx.strokeStyle = Annotations.SelectionModel.defaultSelectionOutlineColor.toString();
        } else {
            ctx.strokeStyle =
                Annotations.SelectionModel.defaultNoPermissionSelectionOutlineColor.toString();
        }

        ctx.beginPath();
        ctx.moveTo(annotation.vertices[0].x, annotation.vertices[0].y);
        ctx.lineTo(annotation.vertices[1].x, annotation.vertices[1].y);
        ctx.lineTo(annotation.vertices[2].x, annotation.vertices[2].y);
        ctx.closePath();
        ctx.stroke();

        const dashUnit = Annotations.SelectionModel.selectionOutlineDashSize / zoom;
        const sequence = [dashUnit, dashUnit];
        ctx.setLineDash(sequence);
        ctx.strokeStyle = 'rgb(255, 255, 255)';
        ctx.stroke();
    };

    TriangleSelectionModel.prototype.testSelection = function (annotation, x, y, pageMatrix) {
        // the canvas visibility test will only select the annotation
        // if a user clicks exactly on it as opposed to the rectangular bounding box
        return Annotations.SelectionAlgorithm.canvasVisibilityTest(annotation, x, y, pageMatrix);
    };

    return TriangleSelectionModel;
};

// Annotation Factory
const TriangleAnnotationFactory = (Annotations, documentViewer) => {
    const TriangleAnnotation = function () {
        Annotations.MarkupAnnotation.call(this);
        this.Subject = 'Triangle';
        this.vertices = [];
        const numVertices = 3;
        for (let i = 0; i &#x3C; numVertices; ++i) {
            this.vertices.push({
                x: 0,
                y: 0,
            });
        }
    };

    TriangleAnnotation.prototype = new Annotations.MarkupAnnotation();

    TriangleAnnotation.prototype.elementName = 'triangle';

    const triangleSelectionModel = TriangleSelectionModelFactory(Annotations, documentViewer);
    TriangleAnnotation.prototype.selectionModel = triangleSelectionModel;

    TriangleAnnotation.prototype.draw = function (ctx, pageMatrix) {
        // the setStyles function is a function on markup annotations that sets up
        // certain properties for us on the canvas for the annotation's stroke thickness.
        this.setStyles(ctx, pageMatrix);

        ctx.beginPath();
        ctx.moveTo(this.vertices[0].x, this.vertices[0].y);
        ctx.lineTo(this.vertices[1].x, this.vertices[1].y);
        ctx.lineTo(this.vertices[2].x, this.vertices[2].y);
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
    };

    TriangleAnnotation.prototype.resize = function (rect) {
        // this function is only called when the annotation is dragged
        // since we handle the case where the control handles move
        const annotRect = this.getRect();
        const deltaX = rect.x1 - annotRect.x1;
        const deltaY = rect.y1 - annotRect.y1;

        // shift the vertices by the amount the rect has shifted
        this.vertices = this.vertices.map(function (vertex) {
            vertex.x += deltaX;
            vertex.y += deltaY;
            return vertex;
        });
        this.setRect(rect);
    };

    TriangleAnnotation.prototype.serialize = function (element, pageMatrix) {
        const el = Annotations.MarkupAnnotation.prototype.serialize.call(this, element, pageMatrix);
        el.setAttribute(
            'vertices',
            Annotations.XFDFUtils.serializePointArray(this.vertices, pageMatrix)
        );
        return el;
    };

    TriangleAnnotation.prototype.deserialize = function (element, pageMatrix) {
        Annotations.MarkupAnnotation.prototype.deserialize.call(this, element, pageMatrix);
        this.vertices = Annotations.XFDFUtils.deserializePointArray(
            element.getAttribute('vertices'),
            pageMatrix
        );
    };

    return TriangleAnnotation;
};

// Create Tool
const TriangleCreateToolFactory = (Annotations, Tools, TriangleAnnotation) => {
    const TriangleCreateTool = function (documentViewer) {
        // TriangleAnnotation is the constructor function for our annotation we defined previously
        Tools.GenericAnnotationCreateTool.call(this, documentViewer, TriangleAnnotation);
    };

    TriangleCreateTool.prototype = new Tools.GenericAnnotationCreateTool();

    TriangleCreateTool.prototype.mouseMove = function (e) {
        // call the parent mouseMove first
        Tools.GenericAnnotationCreateTool.prototype.mouseMove.call(this, e);
        if (this.annotation) {
            this.annotation.vertices[0].x = this.annotation.X + this.annotation.Width / 2;
            this.annotation.vertices[0].y = this.annotation.Y;
            this.annotation.vertices[1].x = this.annotation.X + this.annotation.Width;
            this.annotation.vertices[1].y = this.annotation.Y + this.annotation.Height;
            this.annotation.vertices[2].x = this.annotation.X;
            this.annotation.vertices[2].y = this.annotation.Y + this.annotation.Height;

            // update the annotation appearance
            this.documentViewer.getAnnotationManager().redrawAnnotation(this.annotation);
        }
    };

    return TriangleCreateTool;
};



// WebViewer section
// 
// This code initializes the WebViewer with the basic settings
// that are found in the default showcase WebViewer
//

const searchParams = new URLSearchParams(window.location.search);
const history = window.history || window.parent.history || window.top.history;
const licenseKey = '<code class="expression">visitor.claims.wvKey || "YOUR_WEBVIEWER_LICENSE_KEY"</code>';
const element = document.getElementById('viewer');

// Initialize WebViewer with the specified settings
WebViewer({
    path: '/lib',
    licenseKey: licenseKey,
    enableFilePicker: true,
}, element).then((instance) => {
    // Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool
    const cloudyTools = [
        instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
        instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
        instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
        instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
    ];
    instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
    instance.UI.disableTools(cloudyTools);

    // Set default toolbar group to Annotate
    instance.UI.setToolbarGroup('toolbarGroup-Annotate');

    // Set default tool on mobile devices to Pan.
    // https://apryse.atlassian.net/browse/WVR-3134
    if (isMobileDevice()) {
        instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
    }

    instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
        if (searchParams.has('file')) {
            searchParams.delete('file');
            history.replaceState(null, '', '?' + searchParams.toString());
        }
    });

    instance.Core.annotationManager.enableAnnotationNumbering();

    instance.UI.NotesPanel.enableAttachmentPreview();

    // Add the demo-specific functionality
    customizeUI(instance).then(() => {
        // Create UI controls after demo is initialized
        createUIControls(instance);
    });
});

// Function to check if the user is on a mobile device
const isMobileDevice = () => {
    return (
        /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(
            window.navigator.userAgent
        ) ||
        /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(
            window.navigator.userAgent.substring(0, 4)
        )
    );
}

// Cleanup function for when the demo is closed or page is unloaded
const cleanup = (instance) => {
    const { documentViewer } = instance.Core;

    if (typeof instance !== 'undefined' &#x26;&#x26; instance.UI) {
        instance.UI.unregisterTool(TRIANGLE_TOOL_NAME);
        instance.UI.setToolMode('AnnotationEdit');
        documentViewer.removeEventListener('toolModeUpdated', toolModeUpdated);
        documentViewer.removeEventListener('toolUpdated', setUIState);
        console.log('Cleaning up compare-files demo');
    }
};

// Register cleanup for page unload
window.addEventListener('beforeunload', () => cleanup(instance));
window.addEventListener('unload', () => cleanup(instance));


// UI section
// 
// Helper code to add controls to the viewer holding the buttons
// This code creates a container for the buttons, styles them, and adds them to the viewer
//


// Color Picker
const colorPicker = (filled, instance) => {
    const wrapper = document.createElement('div');

    const title = document.createElement('h2');
    title.textContent = filled ? 'Select custom annotation fill color' : 'Select custom annotation stroke color';
    title.className = 'picker-title';

    wrapper.appendChild(title);

    const colorDisplay = document.createElement('div');
    colorDisplay.className = 'color-display';

    const colorDisplayLabel = document.createElement('p');
    colorDisplayLabel.className = 'color-display-label';
    colorDisplayLabel.textContent = 'Color:';

    const colorDisplayInput = document.createElement('input');
    colorDisplayInput.className = 'color-display-input';
    colorDisplayInput.type = 'text';
    colorDisplayInput.readOnly = true;

    colorDisplayInput.onchange = (e) => {
        if (filled) {
            fillColor = e.target.value;
        } else {
            strokeColor = e.target.value;
        }
    };

    colorDisplay.appendChild(colorDisplayLabel);
    colorDisplay.appendChild(colorDisplayInput);

    wrapper.appendChild(colorDisplay);

    const checkboxContainer = document.createElement('div');
    checkboxContainer.className = 'checkbox-container';

    DEFAULT_COLORS.forEach((color) => {
        // Actual radio/checkbox input
        const checkbox = document.createElement('input');
        checkbox.className = 'checkbox-input';
        checkbox.name = filled ? 'fill-color' : 'stroke-color';
        checkbox.id = `checkbox-${filled ? 'fill' : 'stroke'}-${color.replace('#', '')}`;
        checkbox.type = 'radio';
        checkbox.value = color;

        if (filled) { // default fill color
            if (color === fillColor) {
                checkbox.checked = true;
                colorDisplayInput.value = `${color}`;
            }
        } else { // default stroke color
            if (color === strokeColor) {
                checkbox.checked = true;
                colorDisplayInput.value = `${color}`;
            }
        }

        checkbox.onchange = (e) => {
            if (e.target.checked) {
                colorDisplayInput.value = `${e.target.value}`;
                if (filled) {
                    fillColor = e.target.value;
                } else {
                    strokeColor = e.target.value;
                }
                setToolInstanceStyle(instance);
            }
        };

        checkbox.style.display = 'none'; // Hide to use custom checkbox instead

        checkboxContainer.appendChild(checkbox);

        // Custom checkbox
        const span = document.createElement('span');
        span.className = 'checkbox-checkmark';
        span.id = `checkmark-${filled ? 'fill' : 'stroke'}-${color.replace('#', '')}`;

        if (checkbox.checked) {
            span.textContent = '✓'; // U+2713
        }

        span.onclick = () => {
            checkbox.checked = true;
            checkbox.onchange({ target: checkbox });
            // Update all checkmarks of the same group
            document.querySelectorAll(`input[name=${filled ? 'fill-color' : 'stroke-color'}]`).forEach((cb) => {
                const checkmark = document.getElementById(`checkmark-${filled ? 'fill' : 'stroke'}-${cb.value.replace('#', '')}`);
                if (checkmark) checkmark.textContent = '';
            });
            document.getElementById(`checkmark-${filled ? 'fill' : 'stroke'}-${color.replace('#', '')}`).textContent = '✓'; // U+2713
        }

        span.style.backgroundColor = (filled ? color : 'transparent');
        span.style.color = (filled ? '#FFFFFF' : color);
        span.style.border = `3px solid ${color}`;

        checkboxContainer.appendChild(span);
    });

    wrapper.appendChild(checkboxContainer);

    // Opacity/thickness Slider
    const sliderWrapper = document.createElement('div');

    const sliderLabel = document.createElement('label');
    sliderLabel.className = 'slider-label';
    sliderLabel.textContent = filled ? 'Select custom annotation opacity' : 'Select custom annotation stroke thickness';
    sliderWrapper.appendChild(sliderLabel);

    const sliderInput = document.createElement('input');
    sliderInput.className = 'slider-input';
    sliderInput.type = 'range';
    sliderInput.min = filled ? '0' : '0';
    sliderInput.max = filled ? '1' : '20';
    sliderInput.step = filled ? '0.01' : '1';
    sliderInput.value = filled ? opacity : width;

    sliderInput.onchange = (e) => {
        if (filled) {
            opacity = parseFloat(e.target.value);
        } else {
            width = parseInt(e.target.value, 10);
        }
        setToolInstanceStyle(instance);
    };

    sliderWrapper.appendChild(sliderInput);

    wrapper.appendChild(sliderWrapper);

    return wrapper;
};

const createUIControls = (instance) => {
    const controlsContainer = document.createElement('div');
    controlsContainer.className = 'controls-container';

    // Stroke Color Picker
    controlsContainer.appendChild(colorPicker(false, instance));

    // Fill Color Picker
    controlsContainer.appendChild(colorPicker(true, instance));

    element.insertBefore(controlsContainer, element.firstChild);
};
</code></pre>

{% endtab %}

{% tab title="index.css" %}
{% code title="index.css" lineNumbers="true" %}

```css
/* CSS Standard Compliant Syntax */
/* GitHub Copilot v1.0, GPT-4.1, September 11, 2025 */
/* File: index.css */

.picker-title {
  font-weight: 900;
  font-size: 14px;
  line-height: 125%;
  color: #334250;
  letter-spacing: 0;
  padding-bottom: 10px;
  display: flex;
  justify-content: space-between;
}

.color-display {
  display: flex;
  flex-direction: row;
  align-items: center;
}

.color-display-label {
  font-size: 14px;
  line-height: 20px;
  color: #485056;
  letter-spacing: -0.3px;
  margin-right: 5px;
  font-weight: 700;
}

.color-display-input {
  width: 100%;
  min-width: 0px;
  outline: 2px solid transparent;
  outline-offset: 2px;
  transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
  transition-duration: 200ms;
  border-radius: 6px;
  background-color: #FFFFFF;
  color: #485056;
  border: 1px solid;
  box-sizing: border-box;
  font-size: 14px;
  padding-inline-start: 16px;
  padding-inline-end: 16px;
  height: 40px;
}

.checkbox-checkmark {
  border-radius: 6px;
  width: 24px;
  height: 24px;
  margin: 3px;
  cursor: pointer;
  text-align: center;
  line-height: 18px;
  display: inline-block;
  /* The following will be set dynamically in JS:
     background-color, color, border */
}

.slider-label {
  font-weight: 900;
  font-size: 14px;
  line-height: 125%;
  color: #334250;
  letter-spacing: 0;
  padding: 10px 0 10px 0;
  display: flex;
  justify-content: space-between;
}

.slider-input {
  width: 100%;
  margin: 0;
}

.controls-container {
  display: flex;
  flex-direction: row;
  gap: 20px;
}

.checkbox-container {
  display: flex;
  flex-wrap: wrap;
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

[View the full sample on GitHub](https://github.com/ApryseSDK/webviewer-samples/tree/main/showcase-demos-playground)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.apryse.com/web/get-started/samples/showcase-demo-custom-annotations.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
