Skip to main content
Back to examples
Popular features/Context menu
Loading live demo…from dhtmlxcode.com
Popular features

Context menu

Open a custom right-click menu on a task to add a subtask, duplicate, indent or outdent, convert to a milestone, or delete it.

gantt.plugins({ tooltip: true });

gantt.message({
	text: "<b>Right-click</b> a task for task actions, or right-click the grid header to show and hide columns.",
	expire: -1
});

gantt.config.date_format         = "%Y-%m-%d";
gantt.config.auto_types          = true;
gantt.config.open_tree_initially = true;
gantt.config.grid_resize         = true;
gantt.config.order_branch        = "marker";
gantt.config.order_branch_free   = true;
gantt.config.row_height          = 34;
gantt.config.bar_height          = 22;

const formatGridDate = gantt.date.date_to_str("%d %M");
const priorityColors = {
	high: "#e5484d",
	medium: "#f5a623",
	low: "#3aa757"
};

/* ---------------------------------------------------------------------------
 *  DATASET
 * ------------------------------------------------------------------------ */
let demoData = {
	tasks: [
		{ id: 1, text: "Release 4.0", type: "project", open: true },
		{ id: 10, text: "Planning", type: "project", parent: 1, open: true },
		{ id: 11, text: "Scope freeze",        parent: 10, start_date: "2026-07-06", duration: 3, progress: 1,   priority: "high" },
		{ id: 12, text: "Capacity planning",   parent: 10, start_date: "2026-07-09", duration: 3, progress: 0.6, priority: "medium" },
		{ id: 13, text: "Stakeholder sign-off", parent: 10, start_date: "2026-07-12", duration: 2, progress: 0.3, priority: "medium" },
		{ id: 14, text: "Risk assessment",     parent: 10, start_date: "2026-07-13", duration: 2, progress: 0,   priority: "low" },
		{ id: 20, text: "Engineering", type: "project", parent: 1, open: true },
		{ id: 21, text: "Feature flags",       parent: 20, start_date: "2026-07-14", duration: 5, progress: 0.4, priority: "high" },
		{ id: 22, text: "API schema review",   parent: 20, start_date: "2026-07-16", duration: 3, progress: 0.5, priority: "medium" },
		{ id: 23, text: "Perf hardening",      parent: 20, start_date: "2026-07-20", duration: 6, progress: 0.1, priority: "medium" },
		{ id: 24, text: "Code freeze",         parent: 20, type: "milestone", start_date: "2026-07-27", progress: 0, priority: "high" },
		{ id: 25, text: "Bug bash",            parent: 20, start_date: "2026-07-28", duration: 3, progress: 0,   priority: "low" },
		{ id: 26, text: "Regression testing",  parent: 20, start_date: "2026-07-29", duration: 3, progress: 0,   priority: "medium" },
		{ id: 30, text: "Launch", type: "project", parent: 1, open: true },
		{ id: 31, text: "Release notes",       parent: 30, start_date: "2026-07-30", duration: 2, progress: 0,   priority: "medium" },
		{ id: 32, text: "Marketing assets",    parent: 30, start_date: "2026-07-31", duration: 3, progress: 0,   priority: "low" },
		{ id: 33, text: "Staged rollout",      parent: 30, start_date: "2026-08-03", duration: 2, progress: 0,   priority: "high" },
		{ id: 34, text: "Ship 4.0",            parent: 30, type: "milestone", start_date: "2026-08-05", progress: 0, priority: "high" },
		{ id: 40, text: "Post-launch", type: "project", parent: 1, open: true },
		{ id: 41, text: "Hotfix triage",       parent: 40, start_date: "2026-08-06", duration: 4, progress: 0,   priority: "high" },
		{ id: 42, text: "Customer feedback review", parent: 40, start_date: "2026-08-10", duration: 3, progress: 0, priority: "medium" },
		{ id: 43, text: "Support ticket triage", parent: 40, start_date: "2026-08-11", duration: 3, progress: 0, priority: "medium" },
		{ id: 44, text: "Docs update",         parent: 40, start_date: "2026-08-13", duration: 2, progress: 0,   priority: "low" },
		{ id: 45, text: "Usage telemetry review", parent: 40, start_date: "2026-08-14", duration: 2, progress: 0, priority: "low" },
		{ id: 50, text: "Retrospective", type: "project", parent: 1, open: true },
		{ id: 51, text: "Metrics review",      parent: 50, start_date: "2026-08-17", duration: 2, progress: 0,   priority: "medium" },
		{ id: 52, text: "Survey collection",   parent: 50, start_date: "2026-08-18", duration: 2, progress: 0,   priority: "low" },
		{ id: 53, text: "Retro meeting",       parent: 50, start_date: "2026-08-19", duration: 1, progress: 0,   priority: "low" },
		{ id: 54, text: "Action items writeup", parent: 50, start_date: "2026-08-20", duration: 1, progress: 0,  priority: "medium" },
		{ id: 55, text: "Next cycle kickoff",  parent: 50, type: "milestone", start_date: "2026-08-21", progress: 0, priority: "high" }
	],
	links: [
		{ id: 1, source: 11, target: 12, type: "0" }, { id: 2, source: 12, target: 13, type: "0" },
		{ id: 3, source: 13, target: 14, type: "0" }, { id: 4, source: 14, target: 21, type: "0" },
		{ id: 5, source: 21, target: 22, type: "0" }, { id: 6, source: 22, target: 23, type: "0" },
		{ id: 7, source: 23, target: 24, type: "0" }, { id: 8, source: 24, target: 25, type: "0" },
		{ id: 9, source: 25, target: 26, type: "0" }, { id: 10, source: 26, target: 31, type: "0" },
		{ id: 11, source: 31, target: 32, type: "0" }, { id: 12, source: 32, target: 33, type: "0" },
		{ id: 13, source: 33, target: 34, type: "0" }, { id: 14, source: 34, target: 41, type: "0" },
		{ id: 15, source: 41, target: 42, type: "0" }, { id: 16, source: 42, target: 43, type: "0" },
		{ id: 17, source: 43, target: 44, type: "0" }, { id: 18, source: 44, target: 45, type: "0" },
		{ id: 19, source: 45, target: 51, type: "0" }, { id: 20, source: 51, target: 52, type: "0" },
		{ id: 21, source: 52, target: 53, type: "0" }, { id: 22, source: 53, target: 54, type: "0" },
		{ id: 23, source: 54, target: 55, type: "0" }
	]
};

/* ---------------------------------------------------------------------------
 *  GRID
 * ------------------------------------------------------------------------ */
gantt.config.columns = [
	{ name: "text", label: "Task", tree: true, width: 240, resize: true },
	{ name: "priority", label: "Priority", align: "left", width: 110, resize: true, template: function (task) {
		if (!task.priority || gantt.isSummaryTask(task)) {
			return "";
		}
		return `<span class='prio'><i style='background:${priorityColors[task.priority]}'></i>${task.priority}</span>`;
	}},
	{name: "start_date", label: "Start", align: "center", width: 100, resize: true, template: function (task) {
		return task.type === gantt.config.types.project ? "" : formatGridDate(task.start_date);
	}},
	{ name: "add", width: 40, resize: true }
];

gantt.config.grid_width = 480;

gantt.templates.tooltip_text = function (startDate, endDate, task) {
	return "<b>" + task.text + "</b>";
};

/* ---------------------------------------------------------------------------
 *  SCALE
 * ------------------------------------------------------------------------ */
gantt.config.scale_height = 50;
gantt.config.scales = [{ unit: "month", step: 1, format: "%F %Y" }, { unit: "day", step: 1, format: "%d" }];

/* ---------------------------------------------------------------------------
 *  CONTEXT MENU
 * ------------------------------------------------------------------------ */
const contextMenu = document.getElementById("ctx");

const contextMenuIcons = {
	taskAbove: "<polyline points='6 13 12 7 18 13'/><polyline points='6 19 12 13 18 19'/>",
	taskBelow: "<polyline points='6 5 12 11 18 5'/><polyline points='6 11 12 17 18 11'/>",
	childFirst: "<path d='M5 19V9a4 4 0 0 1 4-4h9'/><polyline points='14 2 18 5 14 8'/>",
	childLast: "<path d='M5 5v10a4 4 0 0 0 4 4h9'/><polyline points='14 16 18 19 14 22'/>",
	link: "<path d='M9 12h6'/><path d='M8 8H6a4 4 0 0 0 0 8h2'/><path d='M16 8h2a4 4 0 0 1 0 8h-2'/>",
	duplicate: "<rect x='9' y='9' width='11' height='11' rx='2'/><path d='M5 15V5a2 2 0 0 1 2-2h10'/>",
	indent: "<polyline points='9 8 13 12 9 16'/><line x1='4' y1='6' x2='4' y2='18'/>",
	outdent: "<polyline points='15 8 11 12 15 16'/><line x1='20' y1='6' x2='20' y2='18'/>",
	flag: "<line x1='5' y1='3' x2='5' y2='21'/><path d='M5 4h11l-2 4 2 4H5'/>",
	check: "<polyline points='4 12 10 18 20 6'/>",
	trash: "<polyline points='4 7 20 7'/><path d='M9 7V4h6v3'/><path d='M6 7l1 13h10l1-13'/>"
};

function createIcon(iconPath) {
	return "<svg class='dhx_ic' viewBox='0 0 24 24'>" + iconPath + "</svg>";
}

function renderMenuItems(menuItems) {
	return menuItems.map(function (menuItem, menuItemIndex) {
		if (menuItem.separator) {
			return "<div class='ctx__sep'></div>";
		}

		const leadingContent = menuItem.checkable
			? "<span class='ctx__check" +
				(menuItem.checked ? "" : " ctx__check--empty") +
				"'>" +
				createIcon(contextMenuIcons.check) +
			"</span>"
			: createIcon(menuItem.icon);

		return "<div class='ctx__item " +
			(menuItem.danger ? "danger " : "") +
			(menuItem.disabled ? "disabled" : "") +
			"' data-index='" +
			menuItemIndex +
			"'>" +
			leadingContent +
			menuItem.label +
		"</div>";
	}).join("");
}

function bindMenuItems(menuItems) {
	contextMenu.querySelectorAll(".ctx__item").forEach(function (menuElement) {
		menuElement.addEventListener("click", function () {
			const menuItem = menuItems[
				Number(menuElement.getAttribute("data-index"))
			];

			if (!menuItem || menuItem.disabled) {
				return;
			}

			if (menuItem.keepOpen) {
				menuItem.action();
			} else {
				closeMenu();
				menuItem.action();
			}
		});
	});
}

function buildTaskMenu(task) {
	const isMilestone = task.type === gantt.config.types.milestone;
	const isRootTask = task.parent === gantt.config.root_id || task.parent === 0 || task.parent == null;
	const isCompleted = (task.progress || 0) >= 1;

	const menuItems = [
		{
			label: "Add task above",
			icon: contextMenuIcons.taskAbove,
			action: function () {
				addSibling(task.id, "above");
			}
		},
		{
			label: "Add task below",
			icon: contextMenuIcons.taskBelow,
			action: function () {
				addSibling(task.id, "below");
			}
		},
		{
			label: "Add first subtask",
			icon: contextMenuIcons.childFirst,
			disabled: isMilestone,
			action: function () {
				addChild(task.id, "first");
			}
		},
		{
			label: "Add last subtask",
			icon: contextMenuIcons.childLast,
			disabled: isMilestone,
			action: function () {
				addChild(task.id, "last");
			}
		},
		{ separator: true },
		{
			label: "Add successor",
			icon: contextMenuIcons.link,
			action: function () {
				addSuccessor(task.id);
			}
		},
		{
			label: "Duplicate",
			icon: contextMenuIcons.duplicate,
			action: function () {
				duplicateTask(task.id);
			}
		},
		{ separator: true },
		{
			label: "Indent",
			icon: contextMenuIcons.indent,
			disabled: !canIndent(task.id),
			action: function () {
				indent(task.id);
			}
		},
		{
			label: "Outdent",
			icon: contextMenuIcons.outdent,
			disabled: isRootTask,
			action: function () {
				outdent(task.id);
			}
		},
		{ separator: true },
		{
			label: isMilestone ? "Convert to task" : "Convert to milestone",
			icon: contextMenuIcons.flag,
			action: function () {
				toggleMilestone(task.id);
			}
		},
		{
			label: isCompleted ? "Mark as not done" : "Mark complete",
			icon: contextMenuIcons.check,
			action: function () {
				setDone(task.id, !isCompleted);
			}
		},
		{ separator: true },
		{
			label: "Delete",
			icon: contextMenuIcons.trash,
			danger: true,
			action: function () {
				gantt.deleteTask(task.id);
			}
		}
	];

	contextMenu.innerHTML = "<div class='ctx__head'>" + DHX.ui.escape(task.text) + "</div>" + renderMenuItems(menuItems);
	bindMenuItems(menuItems);
}

function getColumnLabel(column) {
	if (typeof column.label === "string" && column.label) {
		return column.label;
	}
	return column.name === "add" ? "Add" : column.name;
}

function toggleColumn(columnName) {
	const column = gantt.getGridColumn(columnName);
	if (!column) {
		return;
	}

	column.hide = !column.hide;
	gantt.render();
	buildColumnsMenu();
	positionOpenMenu();
}

function buildColumnsMenu() {
	const columns = gantt.config.columns;
	const visibleColumnCount = columns.filter(function (column) {
		return !column.hide;
	}).length;

	const menuItems = columns.map(function (column) {
		const isVisible = !column.hide;
		return {
			label: getColumnLabel(column),
			checkable: true,
			checked: isVisible,
			disabled: isVisible && visibleColumnCount === 1,
			keepOpen: true,
			action: function () {
				toggleColumn(column.name);
			}
		};
	});

	contextMenu.innerHTML = "<div class='ctx__head'>Grid columns</div>" + renderMenuItems(menuItems);
	bindMenuItems(menuItems);
}

const menuPosition = {
	left: 0,
	top: 0
};

function positionOpenMenu() {
	const menuRectangle = contextMenu.getBoundingClientRect();
	const menuLeft = Math.max(8, Math.min(menuPosition.left, window.innerWidth - menuRectangle.width - 8));
	const menuTop = Math.max(8, Math.min(menuPosition.top, window.innerHeight - menuRectangle.height - 8));

	contextMenu.style.left = menuLeft + "px";
	contextMenu.style.top = menuTop + "px";
}

function openTaskMenu(task, clientX, clientY) {
	menuPosition.left = clientX;
	menuPosition.top = clientY;

	buildTaskMenu(task);
	contextMenu.classList.add("open");
	positionOpenMenu();
}

function openColumnsMenu(clientX, clientY) {
	menuPosition.left = clientX;
	menuPosition.top = clientY;

	buildColumnsMenu();
	contextMenu.classList.add("open");
	positionOpenMenu();
}

function closeMenu() {
	contextMenu.classList.remove("open");
}

gantt.attachEvent("onContextMenu", function (taskId, linkId, event) {
	const gridHeader = event.target.closest(".gantt_grid_scale, .gantt_grid_head_cell");

	if (gridHeader) {
		event.preventDefault();
		openColumnsMenu(event.clientX, event.clientY);
		return false;
	}

	if (taskId == null || !gantt.isTaskExists(taskId)) {
		return true;
	}

	event.preventDefault();
	gantt.selectTask(taskId);

	openTaskMenu(gantt.getTask(taskId), event.clientX, event.clientY);
	return false;
});

document.addEventListener("mousedown", function (event) {
	if (!event.target.closest(".ctx")) {
		closeMenu();
	}
});

document.addEventListener("keydown", function (event) {
	if (event.key === "Escape") {
		closeMenu();
	}
});

gantt.attachEvent("onGanttScroll", closeMenu);

/* ---------------------------------------------------------------------------
 *  ACTIONS
 * ------------------------------------------------------------------------ */
function defaultTaskDate(task) {
	return task.start_date ? new Date(task.start_date) : gantt.date.day_start(new Date());
}

function addSibling(taskId, position) {
	const task = gantt.getTask(taskId);

	const taskIndex = gantt.getTaskIndex(taskId) + (position === "below" ? 1 : 0);

	gantt.createTask(
		{
			text: "New task",
			start_date: defaultTaskDate(task),
			duration: 3,
			parent: task.parent
		},
		task.parent,
		taskIndex
	);
}

function addChild(parentId, position) {
	const parentTask = gantt.getTask(parentId);
	const childTaskIds = gantt.getChildren(parentId);

	const taskIndex = position === "first" ? 0 : childTaskIds.length;

	gantt.open(parentId);
	gantt.createTask(
		{
			text: "New subtask",
			start_date: defaultTaskDate(parentTask),
			duration: 3,
			parent: parentId
		},
		parentId,
		taskIndex
	);
}

function addSuccessor(taskId) {
	const task = gantt.getTask(taskId);
	const successorId = gantt.createTask(
		{
			text: task.text + " (next)",
			start_date: task.end_date,
			duration: 3,
			parent: task.parent
		},
		task.parent
	);

	gantt.addLink({ source: taskId, target: successorId, type: "0"});
}

function duplicateTask(taskId) {
	const task = gantt.getTask(taskId);
	const taskCopy = {
		id: gantt.uid(),
		text: task.text + " (copy)",
		start_date: task.start_date,
		duration: task.duration,
		type: task.type,
		progress: task.progress,
		priority: task.priority,
		parent: task.parent
	};

	gantt.addTask(taskCopy, task.parent, gantt.getTaskIndex(taskId) + 1);
}

function outdent(taskId) {
	const parentId = gantt.getParent(taskId);

	if (parentId === gantt.config.root_id) {
		return;
	}

	const grandParentId = gantt.getParent(parentId);
	const taskIndex = gantt.getTaskIndex(parentId) + 1;

	gantt.moveTask(taskId, taskIndex, grandParentId);
}

function canIndent(taskId) {
	const taskIndex = gantt.getTaskIndex(taskId);
	return taskIndex > 0;
}

function getPreviousSiblingParentId(taskId) {
	const task = gantt.getTask(taskId);
	const siblingTaskIds = gantt.getChildren(task.parent);
	const taskPosition = siblingTaskIds.indexOf(taskId);

	return taskPosition > 0 ? siblingTaskIds[taskPosition - 1] : task.parent;
}

function toggleMilestone(taskId) {
	const task = gantt.getTask(taskId);
	if (task.type === gantt.config.types.milestone) {
		task.type = gantt.config.types.task;

		if (!task.duration) {
			task.duration = 1;
			task.end_date = gantt.calculateEndDate(task);
		}
	} else {
		if (gantt.isSummaryTask(task)) {
			return;
		}

		task.type = gantt.config.types.milestone;
	}

	gantt.updateTask(taskId);
}

function setDone(taskId, isCompleted) {
	const task = gantt.getTask(taskId);
	task.progress = isCompleted ? 1 : 0;
	gantt.updateTask(taskId);
}

function indent(taskId) {
	const newParentId = getPreviousSiblingParentId(taskId);

	if (newParentId === gantt.getTask(taskId).parent) {
		return;
	}

	gantt.open(newParentId);
	gantt.moveTask(taskId, -1, newParentId);
}

/* ---------------------------------------------------------------------------
 *  INIT
 * ------------------------------------------------------------------------ */
gantt.init("gantt_here");
gantt.parse(demoData);
<!DOCTYPE html>
<html lang="en">
<head>
	<meta http-equiv="Content-type" content="text/html; charset=utf-8">
	<title>Task and grid context menus: right-click actions | dhtmlxGantt</title>
	<script src="../../codebase/dhtmlxgantt.js?v=10.0.0"></script>
	<link rel="stylesheet" href="../../codebase/dhtmlxgantt.css?v=10.0.0">
	<link rel="stylesheet" href="../common/demo-controls/dhx_controls.css">
	<script src="../common/demo-controls/dhx_controls.js"></script>
	<link rel="preconnect" href="https://fonts.googleapis.com">
	<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
	<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
	<style>
		body { display: flex; flex-direction: column; }
		#gantt_here { width: 100%; flex: 1 1 auto; min-height: 0; }

		.ctx {
			position: fixed; z-index: 90; min-width: 216px;
			background: var(--dhx-overlay); border: 1px solid var(--dhx-line); border-radius: 12px;
			box-shadow: 0 4px 12px rgba(16,24,40,.08), 0 12px 32px rgba(16,24,40,.18);
			padding: 6px; display: none;
		}
		.ctx.open { display: block; }
		.ctx__head {
			padding: 7px 10px 8px; margin-bottom: 4px; border-bottom: 1px solid var(--dhx-line-2);
			font: 600 13px var(--dhx-font); color: var(--dhx-ink); white-space: nowrap;
			overflow: hidden; text-overflow: ellipsis; max-width: 240px;
		}
		.ctx__item {
			display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 7px;
			font: 500 13px var(--dhx-font); color: var(--dhx-ink-2); cursor: pointer; white-space: nowrap;
		}
		.ctx__item:hover { background: var(--dhx-surface-2); color: var(--dhx-ink); }
		.ctx__item .dhx_ic { width: 16px; height: 16px; color: var(--dhx-muted); }
		.ctx__item:hover .dhx_ic { color: var(--dhx-accent); }
		.ctx__item .ctx__check {
			display: inline-flex;
			align-items: center;
			justify-content: center;
			width: 16px;
			height: 16px;
			flex: none;
			color: var(--dhx-accent);
		}
		.ctx__item .ctx__check .dhx_ic { color: currentColor; }
		.ctx__item .ctx__check--empty { opacity: 0; }
		.ctx__item.danger { color: var(--dhx-danger-solid); }
		.ctx__item.danger:hover { background: var(--dhx-danger-bg); color: var(--dhx-danger-ink); }
		.ctx__item.disabled { opacity: .4; pointer-events: none; }
		.ctx__key { margin-left: auto; font: 600 11px ui-monospace, Menlo, monospace; color: var(--dhx-muted); }
		.ctx__sep { height: 1px; margin: 5px 4px; background: var(--dhx-line); }

		.prio { display: inline-flex; align-items: center; gap: 6px; font: 500 12.5px var(--dhx-font); color: var(--dhx-ink-2); }
		.prio i { width: 8px; height: 8px; border-radius: 50%; }
	</style>
</head>
<body>

<div id="gantt_here"></div>

<!-- Context menu -->
<div class="ctx" id="ctx"></div>


</body>
</html>