Frappe Ganttは、ERPNextのガントビュー用にFrappe社が作ったJavaScriptのガントチャートライブラリです。MITライセンスで、npmの依存パッケージはゼロ。SVGでバーを描き、ドラッグで日付を、ハンドルで進捗を変えられます。最新版は2026年2月25日公開の1.2.2です。
日本語の解説の多くは0.5〜0.6系が前提で、ファイル名もオプション名も1.x系とは違います。この記事の内容は、1.2.2のソースを読み、コードをnode v26.5.0とjsdomで実行して確かめたものです。jsdomに無いSVGの寸法計算とスクロールのAPIは補って実行しており、実ブラウザでの描画速度は測っていません。
まとめ
- 最新版:npmの1.2.2(2026-02-25)。GitHubのReleasesはv1.0.3で止まっているため、版はnpmで確認する。
- 読み込み:CDNは
frappe-gantt.umd.jsとfrappe-gantt.cssを版番号付きで読む。frappe-gantt.min.jsは0.6.1が最後で、1.x系には無い。 - タスク:必須は
startとend(またはduration)。依存関係はカンマ区切りの文字列か配列で書く。 - 保存処理:
on_date_changeとon_progress_changeで受けてAPIへ送る。on_date_changeに渡る終了日時は内部の終了境界の1秒前。日単位のタスクでは通常、最終日の23:59:59になる。 - 向く用途:数十行程度のタスクを閲覧し、日付をドラッグで直す社内ツールやMVP。左側の一覧表・親子タスク・クリティカルパスが要るなら別のライブラリを選ぶ。
Frappe Ganttの特徴と最新版1.2.2までの変更点
GitHubのスター数は6,128、npmのダウンロード数は2026年8月28日〜9月26日の30日間で633,227件です(2026-09-28に取得)。
機能の範囲:できることとできないこと
1.2.2でできるのは、タスクバーの描画、ドラッグでの日付変更、両端のハンドルでの期間変更、進捗バーの変更、依存関係の矢印表示、表示単位の切り替え(Hour・Quarter Day・Half Day・Day・Week・Month・Yearの7種)、土日と祝日の色分け、非稼働日を期間計算から外す処理、ダークモードです。
一方で、次の機能はソースにありません。
- チャート左側に担当者や工数を並べる一覧表(列)
- 親タスク・子タスクの階層やグループ化(Issue #538「usage of task groups」は2026-09-28時点でオープン)
- 画面外の行を描かない仮想化。タスク数ぶんのSVG要素を毎回すべて描く
- タッチ操作でのドラッグ。ドラッグ処理はmousedown・mousemove・mouseupだけで書かれていて、タッチはダブルタップの検出にしか使われていない
- PDF・画像・Excelへの書き出し
バージョン履歴:0.x系から1.x系で変わったこと
「moment.jsとSnap.svgが必要」と書く記事がありますが、この2つは2018年2月のv0.1.0(コミット「Remove moment and Snap dependencies」)で外れています。0.x系の記事から移るときに問題になるのは、依存関係ではなくファイル名とオプション名の変更です。
| 版 | npm公開日 | 主な変更 |
|---|---|---|
| 0.1.0 | 2018-03-04 | moment.js・Snap.svgへの依存を削除 |
| 0.6.1 | 2022-04-28 | frappe-gantt.min.js を含む最後の版 |
| 0.8.0 | 2024-09-11 | 日付表示をIntl化、popupオプション追加 |
| 0.9.0 | 2024-11-29 | 配布ファイルが umd.js・es.js・css に、popup_on追加 |
| 1.0.0 | 2025-01-09 | holidays・ignoreオプション追加 |
| 1.2.0 | 2026-02-17 | ダークモード追加(OS設定連動) |
| 1.2.1 | 2026-02-17 | ダークモードを data-theme 属性での切り替えに変更 |
| 1.2.2 | 2026-02-25 | Month表示のバー位置計算を修正 |
0.x系のコードで書き換えが必要になるのは主に3点です。ポップアップの custom_popup_html は popup 関数に、表示のきっかけを指定する popup_trigger は popup_on に変わりました。もう1点は language です。0.6.1までは月名を持つ言語が en・es・ru・ptBr・fr・tr・zh の7つに限られ、'ja' は指定できませんでした。0.8.0で Intl.DateTimeFormat を使う実装に変わり、日本語表示が可能になっています。
導入手順:CDN・npm・Viteでの読み込み
CDNで読み込む最小構成
jsDelivrから版番号を固定して読み込みます。UMD版はグローバルに Gantt を定義します。表示先は div で構いません。1.x系は渡した要素の中にSVGと .gantt-container を自動で作ります。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/frappe-gantt.css">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/frappe-gantt.umd.js"></script>
</head>
<body>
<div id="gantt"></div>
<script>
const tasks = [
{ id: 'design', name: '基本設計', start: '2026-10-01', end: '2026-10-07', progress: 100 },
{ id: 'impl', name: '実装', start: '2026-10-08', end: '2026-10-21', progress: 40, dependencies: 'design' },
{ id: 'test', name: '結合テスト', start: '2026-10-22', end: '2026-10-27', progress: 0, dependencies: 'impl' },
];
const gantt = new Gantt('#gantt', tasks, { language: 'ja', view_mode: 'Week' });
</script>
</body>
</html>
npmとViteで使うときのCSSの読み込み
npm install [email protected] のあと、JavaScriptは import Gantt from 'frappe-gantt' で読めます。つまずくのはCSSです。package.jsonの exports が . しか公開していないため、Vite 8.3.1で import 'frappe-gantt/dist/frappe-gantt.css' と書くと、次のエラーでビルドが止まります。
"./dist/frappe-gantt.css" is not exported under the conditions ["module", "browser", "production", "import"] from package .../node_modules/frappe-gantt
CSSファイル側で @import "frappe-gantt"; と書いてもJavaScriptファイルが読み込まれて失敗し、@import "frappe-gantt/dist/frappe-gantt.css"; も同じ exports のエラーになりました。ビルドが通ったのは、node_modules内のファイルを相対パスで指定する書き方です。
import Gantt from 'frappe-gantt';
import './node_modules/frappe-gantt/dist/frappe-gantt.css'; // main.js がプロジェクト直下にある場合
new Gantt('#gantt', tasks, { language: 'ja' });
frappe-gantt.min.js が見つからない・古い版が返る原因
frappe-gantt.min.js は0.6.1を最後に配布されなくなりました。古い記事のCDN URLを写すと、配信元によって結果が変わります(2026-09-28に実測)。
- jsDelivrで版番号なしの
/npm/frappe-gantt/dist/frappe-gantt.min.jsを読むと、応答ヘッダーはx-jsd-version: 0.8.0。0.8.0の frappe-gantt.js をjsDelivrが自動で圧縮したファイルが返り、エラーにはならない - 同じURLに
@1.2.2を付けると404 - unpkgは版番号なしのURLを1.2.2へリダイレクトし、その先が404
いちばん厄介なのは1つ目です。CSSだけ版番号なしの frappe-gantt.css(1.2.2)を読むと、JavaScriptは0.8.0、CSSは1.2.2という組み合わせになり、エラーは出ないまま表示が崩れます。CDNのURLには必ず @1.2.2 のように版を書き、JavaScriptとCSSを同じ版にそろえてください。
タスクの定義:プロパティと依存関係・期間の指定
タスクは1件1オブジェクトの配列で渡します。1.2.2のソースで参照されているプロパティは次のとおりです。
| プロパティ | 必須 | 内容 |
|---|---|---|
| start | 必須 | 開始日(例 ‘2026-10-01’) |
| end | endかduration | 終了日。時刻なしならその日の終わりまで |
| duration | endかduration | 期間(例 ‘5d’)。endが無いときだけ使う |
| id | 任意 | 省略すると name から自動生成 |
| name | 任意 | バーに表示する名前 |
| progress | 任意 | 進捗(0〜100) |
| dependencies | 任意 | 先行タスクのid |
| custom_class | 任意 | バーに付けるCSSクラス |
| color / color_progress | 任意 | バー・進捗の色 |
| description | 任意 | 既定ポップアップの副題 |
依存関係は 'design, impl' のようなカンマ区切りの文字列でも、['design', 'impl'] のような配列でも指定できます。文字列はカンマで分割され、各idの空白は _ に置き換えられます。idに空白を入れるなら、参照する側も同じ規則で変換されることを前提に書いてください。矢印は先行タスクの終わりから後続タスクへ引かれるだけで、開始同士や終了同士をつなぐ種類の区別はありません。
duration と end は同じ意味にはなりません。start: '2026-10-22', duration: '5d' はまず終了を10月27日0時と計算し、そのあと「時刻なしの終了日はその日いっぱい」という補正で24時間を足します。jsdomで実行すると、バーは10月22日から27日までの6日分になりました。end: '2026-10-26' なら5日分です。日数を合わせたいときは end で書くほうが確実です。start が無いタスク、end も duration も無いタスク、開始が終了より後のタスク、期間が10年を超えるタスクは、コンソールにエラーを出して配列から除外され、描画されません。
オプションで表示を調整する:表示モード・祝日・日本語化
表示モードの切り替え
初期表示は view_mode で指定します。既定は 'Day' です。view_mode_select: true にすると、右上に切り替え用のセレクトボックスが出ます。コードから切り替えるときは gantt.change_view_mode('Month') を呼び、第2引数に true を渡すと横スクロールの位置を保ちます。既定の列幅はDayとHour系が45px、Weekが140px、MonthとYearが120pxです。
土日・祝日の色分けと非稼働日の除外
holidays は「色 → 対象日」の対応表です。既定値は { 'var(--g-weekend-highlight-color)': 'weekend' } で、土日に色が付きます。自分で holidays を渡すとこの既定値は置き換わるため、土日の色分けを残すなら同じ行も書き足します。
const gantt = new Gantt('#gantt', tasks, {
language: 'ja',
view_mode: 'Week',
view_mode_select: true,
ignore: ['weekend'], // 土日をバーの日数計算から外す
holidays: {
'var(--g-weekend-highlight-color)': 'weekend',
'#fde2e2': [{ date: '2026-11-03', name: '文化の日' }, '2026-11-23'],
},
});
ここでREADMEとソースが食い違っています。READMEは祝日のラベルを {date: ..., label: ...} で渡すと書いていますが、1.2.2のソースが読むのは name です。label で書くと、日付の色は付いてもラベルは表示されませんでした。
ignore: ['weekend'] を付けると、土日はバーの日数から除かれ、ポップアップの日数表示にも「+ 2 excluded」のように除外日数が出ます。祝日も除外するなら、ignore の配列に '2026-11-03' のような日付文字列を追加します。
日本語表示とヘッダーの書式
language: 'ja' を指定すると、月名は Intl.DateTimeFormat で整形され「10月」のように表示されます。Week表示の下段は既定の書式が「D MMM」のため、01 9月 - 07のように日本語としては不自然な並びになります。気になる場合は view_modes でWeekの lower_text を関数で上書きします。
イベントとAPIによる変更内容の保存
受け取れるイベントと引数
イベントは on_ を付けた名前でオプションに渡します。内部では options['on_' + event] を呼んでいるだけなので、名前を間違えてもエラーは出ません。
| オプション | 発火するとき | 引数 |
|---|---|---|
| on_click | バーのクリック | task |
| on_double_click | ダブルクリック・ダブルタップ | task |
| on_hover | バーへのマウスオーバー | task, screenX, screenY, event |
| on_date_change | ドラッグ・リサイズ後 | task, start, end |
| on_progress_change | 進捗ハンドルの変更後 | task, progress |
| on_view_change | 表示モードの切り替え | mode(オブジェクト) |
| on_date_click | 日付の列を押したとき | ‘YYYY-MM-DD’ 文字列 |
保存処理では on_date_change の end に注意してください。渡されるのは内部の終了日時から1秒引いた値です。日単位のタスクでは通常、最終日の23:59:59ですが、時刻付きタスクでは終了境界に応じた時刻になります。DBに終了日を日付で持っているなら、そのまま日付部分を取り出せば一致します。翌日0時として持つ設計なら1秒足して戻します。次の例は最小構成で、response.ok の判定と保存失敗時の表示の復元は省いています。
const gantt = new Gantt('#gantt', tasks, {
on_date_change: async (task, start, end) => {
await fetch(`/api/tasks/${task.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ start: toDate(start), end: toDate(end) }),
});
},
on_progress_change: (task, progress) => {
fetch(`/api/tasks/${task.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ progress }),
});
},
});
// ローカル日付を 'YYYY-MM-DD' にする(toISOString はUTC変換するため、日本時間の午前0〜8時台では日付が前日になる)
function toDate(d) {
const p = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
}
編集させたくない画面では readonly: true を渡します。日付だけ、進捗だけを固定する readonly_dates と readonly_progress もあります。
表示を更新するメソッド
gantt.refresh(newTasks):タスク配列を丸ごと差し替えて描き直すgantt.update_task('impl', { progress: 60 }):1件だけ描き直すgantt.change_view_mode('Month', true):表示モードを変えるgantt.update_options({ readonly: true }):オプションを変えて描き直すgantt.scroll_current():今日の位置までスクロールする
READMEのサンプルには gantt.tasks.refresh() とありますが、gantt.tasks はただの配列なので、このメソッドは存在しません。タスクを追加したら、追加後の配列を gantt.refresh() に渡します。
update_task と update_options の副作用
jsdomで動かすと、2つの副作用が出ました。
1つ目は、update_task() を呼ぶと on_click が発火することです。バーを描き直すアニメーションの開始に、ライブラリが人工の click イベントを発行し(src/svg_utils.js の animateSVG)、それがバーのクリック処理まで伝わります。1回の update_task() で on_click が2回呼ばれました。on_click で詳細画面を開く実装なら、update_task() の前後でフラグを立てて無視してください。
2つ目は、update_options() が表示モードを初期値に戻すことです。内部で初期化時のオプションに差分を重ねて作り直すため、change_view_mode('Month') のあとに update_options({ readonly: true }) を呼ぶと、表示はコンストラクタで指定したWeekに戻りました。表示モードを保ちたいなら、update_options({ readonly: true, view_mode: 'Month' }) のように現在の表示モードも一緒に渡します。
見た目のカスタマイズ:CSS変数・custom_class・ダークモード・ポップアップ
1.x系の配色はCSS変数で決まっています。バーは --g-bar-color、進捗は --g-progress-color、依存関係の矢印は --g-arrow-color、今日の線は --g-today-highlight です。特定のタスクだけ色を変えるなら、タスクに custom_class を付けてセレクタで指定します。1件だけなら、タスクの color に色を書く方法もあります。
:root {
--g-bar-color: #e0ecff;
--g-progress-color: #3b82f6;
}
/* custom_class: 'is-critical' を付けたタスクだけ赤くする */
.gantt .bar-wrapper.is-critical .bar {
fill: #fde2e2;
}
.gantt .bar-wrapper.is-critical .bar-progress {
fill: #dc2626;
}
ダークモードは1.2.1以降、html 要素に data-theme="dark" を付けると切り替わります(document.documentElement.dataset.theme = 'dark')。1.2.0はOSの設定(prefers-color-scheme)に連動する方式でした。サイト側のテーマ切り替えと連動させたいなら1.2.1以降を使います。
ポップアップの中身は popup に関数を渡して組み立てます。関数は task・chart・set_title・set_subtitle・set_details・add_action を持つオブジェクトを受け取ります。false を返すとポップアップは表示されず、HTML文字列を返すとその文字列がそのまま使われます。set_title などは受け取った値を innerHTML に代入するため、利用者が入力したタスク名や担当者名はエスケープしてから渡します。
popup: ({ task, set_title, set_details, add_action }) => {
// set_title・set_details は値を innerHTML に入れるので、外部入力はエスケープする
const esc = (v) => String(v).replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
set_title(esc(task.name));
set_details(`担当: ${esc(task.owner ?? '未定')}<br>進捗: ${Number(task.progress) || 0}%`);
add_action('完了にする', (task, chart) => chart.update_task(task.id, { progress: 100 }));
},
ReactでFrappe Ganttを使う実装
npmにあるReact用ラッパーは古いまま止まっています。frappe-gantt-react の最終版は2019年5月の0.2.2で frappe-gantt ^0.3.0 に依存し、react-frappe-gantt は2021年2月の0.2.3で0.5.0に固定されています。どちらを入れても1.x系のオプションは使えません。ReactのuseRefの使い方|useStateとの違い・DOM参照・値の保持を実例で解説で扱っているDOM参照を使い、本体を直接包むほうが確実です。
import { useEffect, useRef } from 'react';
import Gantt from 'frappe-gantt';
export function GanttChart({ tasks, viewMode = 'Week', onDateChange }) {
const containerRef = useRef(null);
const ganttRef = useRef(null);
const onDateChangeRef = useRef(onDateChange);
useEffect(() => {
onDateChangeRef.current = onDateChange;
}, [onDateChange]);
// インスタンスはマウント時に1回だけ作る
useEffect(() => {
const el = containerRef.current;
ganttRef.current = new Gantt(el, tasks.map((t) => ({ ...t })), {
view_mode: viewMode,
language: 'ja',
on_date_change: (task, start, end) =>
onDateChangeRef.current?.(task.id, start, end),
});
return () => {
el.innerHTML = '';
ganttRef.current = null;
};
}, []);
// tasks が変わったら作り直さず refresh で差し替える
useEffect(() => {
ganttRef.current?.refresh(tasks.map((t) => ({ ...t })));
}, [tasks]);
useEffect(() => {
ganttRef.current?.change_view_mode(viewMode);
}, [viewMode]);
return <div ref={containerRef} />;
}
コピーを渡すのは、Frappe Ganttが受け取ったタスクに _start・_end を足し、dependencies を配列に書き換えるからです。stateを直接渡すとstateが書き換わります。クリーンアップで中身を空にするのは、StrictModeの開発時にマウントが2回走るためです。空にせず作り直すと、.gantt-container が入れ子で2つできました。コールバックをrefに入れているのは、親の再描画のたびにインスタンスを作り直さないためです。ただし1.2.2には破棄用のメソッドが無く、コンストラクタが document に登録する mouseup リスナーはクリーンアップ後も残ります。タブの切り替えなどでマウントとアンマウントを繰り返す画面では、コンポーネントを表示したままにして refresh() で中身を差し替えるほうが安全です。React 19.3.0のStrictModeで、マウント後の .gantt-container が1つ、tasks差し替え後にバーが1本から2本に増え、アンマウント後に0になること、元のstateのキーが増えていないことを確認しました。
Frappe Ganttを選ばないほうがよい場面と代替ライブラリ
Frappe Ganttが向いているのは、数十行程度のタスクを閲覧し、日付と進捗をドラッグで直せれば足りる画面です。社内の進捗共有ページやMVPなら、依存パッケージが無く、約48KBのUMDファイル1つとCSS1つで済む手軽さが効きます。
次の条件に1つでも当てはまるなら、最初から別のライブラリを選んだほうが工数は小さくなります。Frappe Ganttで後から作り足すには、一覧表や階層を自前で描き、SVGと行の位置を同期させる必要があるからです。
- チャートの左に担当者・工数・状態の列が要る。WBSのような親子構造で折りたたみたい
- 大量のタスクを表示する。仮想化が無いので、想定する行数と端末で描画時間を測ってから決める
- スマートフォンでバーをドラッグして編集させたい
- クリティカルパス、自動スケジューリング、担当者ごとの負荷の平準化が要る。平準化の考え方はガントチャートにおける平準化機能とは何か?プロジェクトリソース管理における役割と重要性を基礎から詳しく解説で扱っています
| ライブラリ | 最新版 | ライセンス | 向く用途 |
|---|---|---|---|
| Frappe Gantt | 1.2.2 | MIT | 小規模の閲覧+簡単な編集 |
| DHTMLX Gantt(Community) | 10.0.3 | MIT | 一覧表・階層・大量データ |
| DHTMLX Gantt(PRO) | 10.0.3 | 商用 | 自動計算・クリティカルパス |
| Mermaid(gantt記法) | 12.0.0 | MIT | 文書に埋める静的な工程表 |
DHTMLX Ganttの無料版(Community Edition)は、2026年6月の10.0.0でGPLからMITライセンスに変わりました。9.x系まではGPL-2.0だったので、社内システムに組み込むときの判断材料が変わっています。10.0.3のREADMEによると、Community Editionでも一覧表・ツリー・4種類の依存関係・大量データ向けの描画が使えます。PDFやExcelへの書き出しはDHTMLXのオンラインエクスポートサービス経由なので、利用条件と外部送信の可否を別途確認してください。自動スケジューリング、クリティカルパス、リソース管理は有償のPRO版に限られます。
編集が要らず、設計書やREADMEに工程表を載せたいだけなら、JavaScriptを書かずにテキストで描けるMermaidのgantt記法で足ります。書き方はMermaid記法の使い方を完全ガイド|無料のLive Editorでフローチャート・シーケンス図を作成にまとめています。既製品で操作が合わなければD3.js v7の基本設計とv6以前から刷新されたモジュール構成の全体像で扱うD3.jsで自作し、画面を作る必要自体が無ければJiraの使い方と基本機能|作業項目・エピック・ボードを実務目線で解説のようなSaaSのタイムラインで済ませます。
よくある質問
Frappe Ganttは商用利用できますか?
できます。MITライセンスなので、著作権表示とライセンス文を残せば、商用のシステムに組み込んで再配布しても構いません。本体にnpmの実行時依存パッケージはありませんが、併用するReactやラッパー、組み込む素材などのライセンスは別途確認が必要です。
Frappe Ganttの最新バージョンはいくつですか?
2026年9月28日時点では、2026年2月25日にnpmで公開された1.2.2です。GitHubのReleasesページはv1.0.3(2025年2月)で更新が止まっているため、npm view frappe-gantt version かnpmのページで確認してください。
frappe-gantt.min.js はどこでダウンロードできますか?
1.x系には含まれていません。0.9.0以降の配布ファイルは dist/frappe-gantt.umd.js(scriptタグ用)、dist/frappe-gantt.es.js(ESモジュール用)、dist/frappe-gantt.css の3つです。min.jsを読む古いコードは、umd.jsとcssを版番号付きで読む形に書き換えます。
Frappe GanttはReactで使えますか?
使えます。npmのReact用ラッパーは0.x系に依存したまま更新が止まっているので、useRef で取った要素に new Gantt() を直接呼ぶ実装にします。タスクはコピーを渡し、クリーンアップで要素の中身を空にします。document に登録されるリスナーは解除できないため、マウントを繰り返す画面ではインスタンスを使い回します。
依存関係は複数のタスクに設定できますか?
できます。dependencies: 'design, review' のようなカンマ区切りの文字列か、['design', 'review'] の配列で、先行タスクのidを複数書きます。move_dependencies が既定の true のままなら、先行タスクをドラッグすると後続タスクも一緒に動きます。