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で実行して確かめたものです(2026-10-23)。jsdomに無い getBBox・scrollTo・innerText は補って実行しており、実ブラウザでの描画速度や操作感は検証の対象外です。
まとめ
- 最新版:npmの1.2.2(2026-02-25)。GitHubのタグはv1.0.3で止まっているため、版はnpmで確認する。
- 読み込み:CDNは
frappe-gantt.umd.jsとfrappe-gantt.cssを版番号付きで読む。frappe-gantt.min.jsは0.6.1が最後で、1.x系には無い。 - タスク:必須は
startとend(またはduration)。追加・削除は配列を作り直してrefresh()に渡す。 - 日本語化:
language: 'ja'で月名は日本語になるが、Todayボタンと既定のポップアップは英語のまま。週の見出しはGantt.VIEW_MODEを展開したview_modesで直す。 - 保存処理:
on_date_changeとon_progress_changeで受けてAPIへ送る。on_date_changeの終了日時は内部の終了境界の1秒前で、日単位のタスクなら最終日の23:59:59。 - React・Vue:npmのラッパーは0.x系で止まっている。マウント時に1回
new Gantt()し、データの変更はrefresh()で渡す。 - 向く用途:数十行程度のタスクを閲覧し、日付をドラッグで直す社内ツールやMVP。左側の一覧表・親子タスク・クリティカルパスが要るなら別のライブラリを選ぶ。
Frappe Ganttの特徴と最新版1.2.2までの変更点
GitHubのスター数は6,151、npmのダウンロード数は2026年9月19日〜10月18日の30日間で942,350件です(2026-10-23に取得)。配布はnpmのfrappe-ganttパッケージ、ソースはGitHubのfrappe/ganttで、2026年1月6日更新の公式ドキュメント(docs.frappe.io)の設定ページもあります。ただし公式ドキュメントはREADMEとほぼ同じ内容で、祝日ラベルのキー名(ドキュメントは label、ソースは name)や date_format の既定値(ドキュメントは YYYY-MM-DD、ソースは YYYY-MM-DD HH:mm)は、1.2.2のソースと食い違ったままです。
機能の範囲:できることとできないこと
1.2.2でできるのは、タスクバーの描画、ドラッグでの日付変更、両端のハンドルでの期間変更、進捗バーの変更、依存関係の矢印表示、表示単位の切り替え(Hour・Quarter Day・Half Day・Day・Week・Month・Yearの7種)、土日と祝日の色分け、非稼働日を期間計算から外す処理、ダークモードです。
一方で、次の機能はソースにありません。
- チャート左側に担当者や工数を並べる一覧表(列)
- 親タスク・子タスクの階層やグループ化(Issue #538「usage of task groups」は2026-10-23時点でオープン)
- 画面外の行を描かない仮想化。タスク数ぶんの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表示のバー位置計算を修正 |
| master(未リリース) | 未公開 | view_modes の部分指定を既定値と合成 |
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 を使う実装に変わり、日本語表示が可能になっています。
1.2.2の公開後も、masterブランチには修正が入っています。2026年3月4日のコミット7fa142dは、view_modes に名前と一部の項目だけを渡したとき既定値と合成する修正で、1.2.2ではこの書き方が例外になります(日本語化の節で扱います)。2026年10月23日時点でこれを含む版はnpmに出ていません。
導入手順: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 も文字列なら空白が _ になりますが、配列で渡した dependencies の要素は変換されません。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年を超えるタスクは、コンソールにエラーを出して配列から除外され、描画されません。開始と終了の逆転は、差を年単位で比べているため1年未満だと検出されません。jsdomで試すと、2026-10-31開始・2026-10-30終了のタスクと、同じ日の15時開始・9時終了のタスクは除外されずに残り、2年逆転したタスクだけが除外されました。日付の前後関係は、渡す前にアプリ側で検証してください。
オプションで表示を調整する:表示モード・祝日・日本語化
表示モードの切り替え
初期表示は 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の設定を渡しますが、1.2.2では書き方を誤ると動きません。jsdomで試すと、次の3つが起きました。
{ name: 'Week', lower_text: ... }のように一部の項目だけを渡すと、コンストラクタがTypeError: Cannot destructure property 'duration'で止まるview_modesを渡すとview_modeの指定は無視され、配列の先頭の表示モードで始まる(view_mode: 'Month'を書いても先頭がDayならDayになる)view_modesは既定の7種を置き換える。渡さなかった表示モードはセレクトボックスからも消える
既定値は Gantt.VIEW_MODE.WEEK のように公開されているので、それを展開してから上書きします。表示モードの name はセレクトボックスの表示名にも使われるため、日本語にすると選択肢も日本語になります。
const V = Gantt.VIEW_MODE;
const gantt = new Gantt('#gantt', tasks, {
language: 'ja',
view_mode_select: true,
view_modes: [
// 先頭が初期表示になる
{ ...V.WEEK, name: '週', lower_text: (d) => `${d.getMonth() + 1}/${d.getDate()}〜` },
{ ...V.DAY, name: '日' },
{ ...V.MONTH, name: '月' },
],
});
gantt.change_view_mode('月'); // 名前を変えたら呼び出しも新しい名前で
この設定で、週の下段は「9/8〜」「9/15〜」のように並びました。名前を変えたあとに change_view_mode('Week') を呼ぶと、該当する表示モードが見つからず TypeError になります。セレクトボックスの先頭にある見出し「Mode」は英語のまま残ります。
英語のまま残るTodayボタンと既定のポップアップ
language: 'ja' は日付の書式にしか効きません。1.2.2で英語のまま表示されるのは、右上の「Today」ボタンと、バーをクリックしたときの既定のポップアップです。ポップアップは「10月 08 – 10月 21 (14 days)」「Progress: 40%」のように表示されました。
Todayボタンの文字をDOMで書き換えても、change_view_mode() や refresh() で描き直すたびに「Today」に戻ります。today_button: false でボタンを消し、自前のボタンから gantt.scroll_current() を呼ぶほうが確実です。ポップアップは、後述の popup 関数で日本語の内容を組み立てます。
const gantt = new Gantt('#gantt', tasks, { language: 'ja', today_button: false });
const todayButton = document.createElement('button');
todayButton.type = 'button';
todayButton.textContent = '今日';
document.querySelector('#gantt').before(todayButton);
todayButton.addEventListener('click', () => gantt.scroll_current());
イベントと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() に渡します。
タスクの追加・削除と依存関係の更新
1.2.2にはタスクを1件追加・削除するメソッドがありません。アプリ側でタスクの配列を持ち、変更のたびに作り直した配列を refresh() に渡します。コピーを渡すのは、Frappe Ganttが受け取ったオブジェクトに _start・_end などを書き足すからです。
let tasks = [
{ id: 'design', name: '基本設計', start: '2026-10-01', end: '2026-10-07', progress: 100, dependencies: [] },
{ id: 'impl', name: '実装', start: '2026-10-08', end: '2026-10-21', progress: 40, dependencies: ['design'] },
];
const gantt = new Gantt('#gantt', tasks.map((t) => ({ ...t, dependencies: [...t.dependencies] })), { language: 'ja' });
function render() {
gantt.refresh(tasks.map((t) => ({ ...t, dependencies: [...t.dependencies] })));
}
function addTask(task) {
// 文字列・配列のどちらで渡されても id の配列にそろえる
const dependencies = (Array.isArray(task.dependencies)
? task.dependencies
: (task.dependencies || '').split(','))
.map((id) => String(id).trim().replaceAll(' ', '_'))
.filter(Boolean);
tasks = [...tasks, { ...task, dependencies }];
render();
}
function removeTask(id) {
// 削除したidを、ほかのタスクの依存関係からも外す
tasks = tasks
.filter((t) => t.id !== id)
.map((t) => ({ ...t, dependencies: t.dependencies.filter((d) => d !== id) }));
render();
}
依存関係から外す処理を省いても、エラーは出ません。jsdomで先行タスクだけを消すと、後続タスクの dependencies に消えたidが残ったまま、矢印だけが黙って消えました。保存先のデータに存在しないidが残るので、削除時に一緒に外しておきます。
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' }) のように現在の表示モードも一緒に渡します。ただし view_modes を指定している場合は、view_mode を渡しても配列の先頭の表示モードに戻るため、更新後に change_view_mode() で現在の表示モードを指定し直します。
見た目のカスタマイズ: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・Vue 3で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・Vueのどちらでも、マウント時に1回だけ new Gantt() し、データの変更は refresh() で渡す形にします。
Reactでの実装(useRefで本体を包む)
useRefの使い方|DOM参照・値の保持とuseStateとの違い【React 19.3対応】で扱っている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のキーが増えていないことを確認しました。
Vue 3・Nuxtでの実装
Vueでも考え方は同じです。onMounted で生成し、watch で refresh() を呼びます。インスタンスは画面に反映させる値ではないので、ref にせず通常の変数に置きます。Composition APIの書き方はComposition APIとは?書き方とOptions APIとの違い・移行判断を解説にまとめています。
<script setup>
import { ref, toRaw, watch, onMounted, onBeforeUnmount } from 'vue';
import Gantt from 'frappe-gantt';
const props = defineProps({
tasks: { type: Array, required: true },
viewMode: { type: String, default: 'Week' },
});
const emit = defineEmits(['date-change']);
const el = ref(null);
let gantt = null; // リアクティブにしない
// Proxyを外したコピーを渡す(ライブラリが _start などを書き足すため)
const copy = () => props.tasks.map((t) => ({ ...toRaw(t) }));
onMounted(() => {
gantt = new Gantt(el.value, copy(), {
view_mode: props.viewMode,
language: 'ja',
on_date_change: (task, start, end) => emit('date-change', task.id, start, end),
});
});
watch(() => props.tasks, () => gantt?.refresh(copy()), { deep: true });
watch(() => props.viewMode, (mode) => gantt?.change_view_mode(mode));
onBeforeUnmount(() => {
el.value.innerHTML = '';
gantt = null;
});
</script>
<template>
<div ref="el" />
</template>
Vue 3.5.44で、同じ処理をrender関数で書いたコンポーネントをjsdomで動かしました。親のタスク配列に push するとバーが1本から2本に増え、表示を消すと .gantt-container が0になり、親のstateのキーは増えていませんでした。CSSの読み込みは、Viteの節で書いた相対パスの指定が必要です。
NuxtでSSRをしていても、<ClientOnly> で囲む必要はありません。DOMの無いNode.jsで import Gantt from 'frappe-gantt' は成功し、new Gantt() を呼んだ時点で ReferenceError: document is not defined になりました。onMounted はブラウザでしか実行されないので、上のコードのまま動きます。なお、2024年の0.6.1系の解説には「今日の日付が表示範囲に無いとエラーになる」とありますが、1.2.2では2022年だけのタスクも、空の配列もエラーなく描画されました。
ERPNext・Frappe FrameworkのGanttビューとの関係
ERPNextのプロジェクト管理にあるGanttビューは、このFrappe Ganttで描かれています。ERPNextの土台であるFrappe Frameworkの package.json を見ると、依存バージョンの指定範囲は系列で違います。表はpackage.jsonの指定であり、実際に解決される版は各環境のロックファイルで確認します。
| Frappe Framework | package.jsonの指定 |
|---|---|
| version-15 | frappe-gantt ^0.6.0 |
| version-16 | frappe-gantt ^1.1.0 |
| develop | frappe-gantt ^1.2.1 |
version-15のサイトでカスタムアプリからガントの見た目を変えるなら、この記事の1.x系のオプションではなく0.6系の書き方になります。どのDocTypeをどの項目で描くかは field_map で決まります。ERPNextのTaskでは task_calendar.js が、開始を exp_start_date、終了を exp_end_date、名前を subject に割り当て、gantt: true を指定しています。バーの色は、公式の手順「Make A Colorful Gantt Chart」のとおり、TaskにColor型の項目を足して一覧表示に含めると付きます。ERPNextの画面で足りるなら、Frappe Ganttを自分で組み込む必要はありません。
Frappe Ganttを選ばないほうがよい場面と代替ライブラリ
Frappe Ganttが向いているのは、数十行程度のタスクを閲覧し、日付と進捗をドラッグで直せれば足りる画面です。社内の進捗共有ページやMVPなら、依存パッケージが無く、約48KBのUMDファイル1つとCSS1つで済む手軽さが効きます。
次の条件に1つでも当てはまるなら、最初から別のライブラリを選んだほうが工数は小さくなります。Frappe Ganttで後から作り足すには、一覧表や階層を自前で描き、SVGと行の位置を同期させる必要があるからです。
- チャートの左に担当者・工数・状態の列が要る。WBSのような親子構造で折りたたみたい
- 大量のタスクを表示する。仮想化が無いので、想定する行数と端末で描画時間を測ってから決める
- スマートフォンでバーをドラッグして編集させたい
- クリティカルパス、自動スケジューリング、担当者ごとの負荷の平準化が要る。平準化の考え方は平準化とは?ガントチャートで担当者の負荷をならすリソース平準化の仕組みとMicrosoft Projectの設定で扱っています
| ライブラリ | 最新版 | ライセンス | 向く用途 |
|---|---|---|---|
| Frappe Gantt | 1.2.2 | MIT | 小規模の閲覧+簡単な編集 |
| SVAR Gantt | 2.8.1 | MIT(PRO版は有償) | React・Vue・Svelteで一覧表と階層 |
| DHTMLX Gantt(Community) | 10.0.3 | MIT | 一覧表・階層・大量データ |
| DHTMLX Gantt(PRO) | 10.0.3 | 商用 | 自動計算・クリティカルパス |
| Bryntum Gantt | 7.3.7 | 商用 | 自動スケジューリング・リソース管理 |
| Mermaid(gantt記法) | 12.2.0 | MIT | 文書に埋める静的な工程表 |
版は2026年10月23日時点のnpmの値です(Bryntumはnpmの試用版パッケージ)。
Frappe Ganttの次に検討しやすいのは、無料で一覧表と階層が付く2つです。SVAR React GanttのREADMEによると、無料版に一覧表(グリッド)・サブタスクの階層・大量データ向けの仮想化が入り、自動スケジューリング、クリティカルパス、リソース管理はPRO版の機能です。React・Vue・Svelte向けに別パッケージがあり、いずれも2026年10月12日に2.8.1が出ています。
DHTMLX Ganttの無料版(Community Edition)は、2026年6月の10.0.0でGPLからMITライセンスに変わりました。9.x系まではGPL-2.0だったので、社内システムに組み込むときの判断材料が変わっています。npmのdhtmlx-gantt 10.0.3のREADMEによると、Community Editionでも一覧表・ツリー・4種類の依存関係・大量データ向けの描画が使えます。PDFやExcelへの書き出しはDHTMLXのオンラインエクスポートサービス経由なので、利用条件と外部送信の可否を別途確認してください。自動スケジューリング、クリティカルパス、リソース管理は有償のPRO版に限られます。
工程表を生産管理や受注管理の画面に組み込む規模になると、ライブラリより先に、実績をどこで入力し誰が計画を直すかというデータの持ち方が問題になります。業務システムとして作る場合は生産管理システム開発で相談を受けています。
編集が要らず、設計書やREADMEに工程表を載せたいだけなら、JavaScriptを書かずにテキストで描けるMermaidのgantt記法で足ります。書き方はMermaid記法の使い方|無料のLive Editor(mermaid.live)でフローチャート・シーケンス図を作成にまとめています。画面を作る必要自体が無ければ、Jiraの使い方|スペース作成・エピックの使い分けと「使いにくい」の解消法【2026年10月】のようなSaaSのタイムラインで済ませます。
よくある質問
Frappe Ganttは商用利用できますか?
できます。MITライセンスなので、著作権表示とライセンス文を残せば、商用のシステムに組み込んで再配布しても構いません。本体にnpmの実行時依存パッケージはありませんが、併用するReactやラッパー、組み込む素材などのライセンスは別途確認が必要です。
Frappe Ganttの最新バージョンはいくつですか?
2026年10月23日時点では、2026年2月25日にnpmで公開された1.2.2です。GitHubのタグとReleasesはv1.0.3で止まっているため、npm view frappe-gantt version かnpmのページで確認してください。masterブランチには2026年3月の修正が入っていますが、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やVueで使えますか?
使えます。npmのReact用ラッパーは0.x系に依存したまま更新が止まっているので、Reactなら useRef、Vueなら ref で取った要素に new Gantt() を直接呼ぶ実装にします。タスクはコピーを渡し、アンマウント時に要素の中身を空にします。document に登録されるリスナーは解除できないため、マウントを繰り返す画面ではインスタンスを使い回します。
依存関係は複数のタスクに設定できますか?
できます。dependencies: 'design, review' のようなカンマ区切りの文字列か、['design', 'review'] の配列で、先行タスクのidを複数書きます。move_dependencies が既定の true のままなら、先行タスクをドラッグすると後続タスクも一緒に動きます。