Veto
Components

Gantt

Today's work to the minute, or the whole plan: jobs, projects and servers on the left, a timeline on the right. It opens on today; filters, zoom from fifteen minutes to months, date navigation, export and dark mode are built in, and every part of it can be switched off or reworded.

Today's work

The chart opens on today: the axis covers today alone, stretched across the full width, and only the rows with work today are listed - not yesterday's, not tomorrow's. The Today / Schedule switch in the toolbar opens the whole plan, and View can be bound or set to start there.

Tasks
Platform2 Mar – 10 Apr
App shell9 Mar – 24 Mar
Component library16 Mar – 10 Apr
19:51
00:0001:0002:0003:0004:0005:0006:0007:0008:0009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:00
razor
@* The default: today alone, fitted to the width, with only today's work listed. *@
<VetoGantt Items="Tasks"
           TodayOverride="Today"
           SidebarWidth="13rem"
           Height="14rem"
           @bind-View="_view" />

@code {
    private VetoGanttView _view = VetoGanttView.Today;

    // The demos' fixed today, so the March 2026 rows are on screen. Leave TodayOverride
    // out and the chart takes the real date.
    private static readonly DateOnly Today = new(2026, 3, 18);

    // Tasks: the list written out under "A schedule".
}

Jobs by the hour

Give an item a StartTime and EndTime and it is placed to the minute. The today view draws hours, marks now, and lists only what runs today - last night's backup and tomorrow's release are not on it. Inline, a whole day of jobs sits on one line. StartTime and EndTime on the chart itself trim the day to a window: 14:00 to 20:00 below, stretched across the full width.

Tasks
Operations06:00–21:00
Support rota08:00–17:00
11:30
00:0001:0002:0003:0004:0005:0006:0007:0008:0009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:00
Tasks
Operations06:00–21:00
Deploy14:00–15:30
Cleanup19:00–21:00
Support rota08:00–17:00
14:0015:0016:0017:0018:0019:00
razor
@* Today's jobs by the hour, a whole day of them on the Operations line. *@
<VetoGantt Items="Jobs"
           TodayOverride="Today"
           TimeOverride="new TimeOnly(11, 30)"
           SubItemLayout="VetoGanttSubItemLayout.Inline"
           SidebarWidth="13rem"
           Height="11rem" />

@* Only 14:00 to 20:00, stretched across the chart. *@
<VetoGantt Items="Jobs"
           TodayOverride="Today"
           TimeOverride="new TimeOnly(11, 30)"
           StartTime="new TimeOnly(14, 0)"
           EndTime="new TimeOnly(20, 0)"
           ShowToolbar="false"
           SidebarWidth="13rem"
           Height="11rem" />

@code {
    // The demos' fixed now. Leave both overrides out and the chart takes the real clock.
    private static readonly DateOnly Today = new(2026, 3, 18);

    private const string Engineering = "var(--veto-color-primary)";
    private const string Research = "var(--veto-color-warning)";
    private const string Marketing = "var(--veto-color-success)";

    // A day of jobs on 18 March, plus one either side of it that the today view leaves out.
    private static readonly IReadOnlyList<VetoGanttItem> Jobs =
    [
        new() { Id = "ops", Name = "Operations",
                Start = new(2026, 3, 18), End = new(2026, 3, 18), StartTime = new(6, 0), EndTime = new(21, 0),
                Category = "Engineering", Color = Engineering },
        new() { Id = "etl", Name = "Data load", ParentId = "ops",
                Start = new(2026, 3, 18), End = new(2026, 3, 18), StartTime = new(6, 0), EndTime = new(8, 30),
                Category = "Engineering", Color = Engineering },
        new() { Id = "reports", Name = "Reports", ParentId = "ops",
                Start = new(2026, 3, 18), End = new(2026, 3, 18), StartTime = new(9, 0), EndTime = new(10, 0),
                Category = "Research", Color = Research },
        new() { Id = "deploy", Name = "Deploy", ParentId = "ops",
                Start = new(2026, 3, 18), End = new(2026, 3, 18), StartTime = new(14, 0), EndTime = new(15, 30),
                Category = "Engineering", Color = Engineering },
        new() { Id = "cleanup", Name = "Cleanup", ParentId = "ops",
                Start = new(2026, 3, 18), End = new(2026, 3, 18), StartTime = new(19, 0), EndTime = new(21, 0),
                Category = "Engineering", Color = Engineering },
        new() { Id = "support", Name = "Support rota",
                Start = new(2026, 3, 18), End = new(2026, 3, 18), StartTime = new(8, 0), EndTime = new(17, 0),
                Category = "Marketing", Color = Marketing },
        new() { Id = "backup", Name = "Backup (last night)",
                Start = new(2026, 3, 17), End = new(2026, 3, 17), StartTime = new(22, 0), EndTime = new(23, 30),
                Category = "Engineering", Color = Engineering },
        new() { Id = "release", Name = "Release (tomorrow)",
                Start = new(2026, 3, 19), End = new(2026, 3, 19), StartTime = new(9, 0), EndTime = new(10, 0),
                Category = "Engineering", Color = Engineering }
    ];
}

Today's runs, by server

An operations schedule: jobs from exact timestamps, grouped by server with GroupBy, each server one line. Jobs that overlap are stacked into lanes and the row grows to fit them, so two runs at once are both visible - and a five-minute job is a five-minute sliver next to a ninety-minute one. The first chart shows 07:00 to 12:00 in hours; the second zooms to 08:00-10:00 in quarter hours.

Tasks
web-0108:00–10:45
batch-0108:00–10:00
db-0117 Mar 22:00 – 18 Mar 11:20
09:20
07:0008:0009:0010:0011:00
Tasks
web-0108:00–10:45
batch-0108:00–10:00
09:20
Wed 18 Mar, 08:0009:00
:00:15:30:45:00:15:30:45
razor
<VetoGantt Items="Runs"
           TodayOverride="Today"
           TimeOverride="new TimeOnly(9, 20)"
           StartTime="new TimeOnly(7, 0)"
           EndTime="new TimeOnly(12, 0)"
           GroupBy="ServerOf"
           SubItemLayout="VetoGanttSubItemLayout.Inline"
           ShowCategoryFilter="false"
           SidebarWidth="11rem" />

@* Quarter-hour columns for the busy part of the morning. *@
<VetoGantt Items="Runs"
           TodayOverride="Today"
           TimeOverride="new TimeOnly(9, 20)"
           StartTime="new TimeOnly(8, 0)"
           EndTime="new TimeOnly(10, 0)"
           TodayScale="VetoGanttScale.QuarterHour"
           GroupBy="ServerOf"
           SubItemLayout="VetoGanttSubItemLayout.Inline"
           ShowToolbar="false"
           SidebarWidth="11rem" />

@code {
    // The demos' fixed now. Leave both overrides out and the chart takes the real clock.
    private static readonly DateOnly Today = new(2026, 3, 18);

    private static string? ServerOf ( VetoGanttItem job ) => job.Category;

    // Exact timestamps, as an API hands them over, with the server in Category.
    private static readonly DateTime Morning = new(2026, 3, 18);

    private static VetoGanttItem Run ( string id, string name, string server, string color,
                                       int fromHour, int fromMinute, int toHour, int toMinute )
        => new(id, name, Morning.AddHours(fromHour).AddMinutes(fromMinute), Morning.AddHours(toHour).AddMinutes(toMinute))
        {
            Category = server,
            Color = color
        };

    private static readonly IReadOnlyList<VetoGanttItem> Runs =
    [
        Run("deploy", "Deploy", "web-01", "var(--veto-color-primary)", 8, 0, 8, 30),
        Run("smoke", "Smoke test", "web-01", "var(--veto-color-primary)", 8, 30, 8, 40),
        Run("warm", "Cache warm", "web-01", "var(--veto-color-primary)", 10, 15, 10, 45),
        Run("import", "Nightly import", "batch-01", "var(--veto-color-warning)", 8, 0, 9, 30),
        Run("reindex", "Reindex", "batch-01", "var(--veto-color-warning)", 8, 45, 10, 0),
        Run("ping", "Health ping", "batch-01", "var(--veto-color-warning)", 9, 10, 9, 15),
        Run("backup", "Backup", "db-01", "var(--veto-color-success)", 7, 0, 7, 45),
        Run("vacuum", "Vacuum", "db-01", "var(--veto-color-success)", 11, 0, 11, 20),
        new VetoGanttItem("yesterday", "Late export", new DateTime(2026, 3, 17, 22, 0, 0), new DateTime(2026, 3, 17, 23, 0, 0))
        {
            Category = "db-01",
            Color = "var(--veto-color-success)"
        }
    ];
}

Row heights

RowHeight sets every row; an item's own RowHeight overrides it for that row, and its bar scales with it. Here the chart is compact at 28px and the three projects stand out at 48px. GroupRowHeight does the same for the rows GroupBy makes.

Tasks
Platform2 Mar – 10 Apr
Design system2 Mar – 13 Mar
App shell9 Mar – 24 Mar
Component library16 Mar – 10 Apr
Mobile23 Mar – 24 Apr
Feasibility spike23 Mar – 27 Mar
Build30 Mar – 24 Apr
Launch13 Apr – 1 May
Write the copy13 Apr – 22 Apr
Ship1 May
18 Mar
February 2026March 2026April 2026May 2026
2812345678910111213141516171819202122232425262728293031123456789101112131415161718192021222324252627282930123
razor
@* Compact rows, with the projects taller. *@
<VetoGantt Items="Sized"
           View="VetoGanttView.Schedule"
           TodayOverride="Today"
           RowHeight="28"
           ShowToolbar="false"
           SidebarWidth="13rem"
           Height="20rem"
           ScrollToTodayOnLoad="false" />

@* Not shown above: the rows GroupBy makes have a height of their own. *@
<VetoGantt Items="Runs" GroupBy="ServerOf" GroupRowHeight="44" />

@code {
    private static readonly DateOnly Today = new(2026, 3, 18);

    // The schedule under "A schedule", with every top-level row - a project - at 48px.
    private static readonly IReadOnlyList<VetoGanttItem> Sized =
        Tasks.Select(task => task.ParentId is null ? task with { RowHeight = 48 } : task).ToList();
}

A short range fills the width

Give the chart a Start and End a few days apart and the days widen until the range spans the whole chart, rather than drawing a week at the scale's own density and leaving the rest empty. A range longer than the chart keeps its density and scrolls. FitToWidth=false turns this off.

Tasks
Platform2 Mar – 10 Apr
Design system2 Mar – 13 Mar
App shell9 Mar – 24 Mar
Component library16 Mar – 10 Apr
Mobile23 Mar – 24 Apr
Feasibility spike23 Mar – 27 Mar
Build30 Mar – 24 Apr
Launch13 Apr – 1 May
Write the copy13 Apr – 22 Apr
Ship1 May
18 Mar
March 2026
14151617181920
razor
@* Seven days, stretched across the whole chart. *@
<VetoGantt Items="Tasks"
           View="VetoGanttView.Schedule"
           TodayOverride="Today"
           Start="new DateOnly(2026, 3, 14)"
           End="new DateOnly(2026, 3, 20)"
           ShowToolbar="false"
           SidebarWidth="13rem"
           Height="17rem" />

@* Not shown above: the same week at the scale's own density. *@
<VetoGantt Items="Tasks"
           View="VetoGanttView.Schedule"
           Start="new DateOnly(2026, 3, 14)"
           End="new DateOnly(2026, 3, 20)"
           FitToWidth="false" />

@code {
    private static readonly DateOnly Today = new(2026, 3, 18);

    // Tasks: the list written out under "A schedule".
}

Earlier and later

The arrows either side of Today scroll the timeline most of a screen at a time. With nothing more to scroll that way - and a range fitted to the width never scrolls at all - they move the range itself: the today view to the day before or after, a range of days by a step of the scale, never more than the range is long, so no day is skipped. Today brings it back. OnRangeChanged reports each new range, for a page that loads its rows a range at a time; PageBeyondRange=false keeps the arrows to scrolling.

Tasks
Operations06:00–21:00
Data load06:00–08:30
Reports09:00–10:00
Deploy14:00–15:30
Cleanup19:00–21:00
Support rota08:00–17:00
11:30
00:0001:0002:0003:0004:0005:0006:0007:0008:0009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:00
Tasks
Platform2 Mar – 10 Apr
Design system2 Mar – 13 Mar
App shell9 Mar – 24 Mar
Component library16 Mar – 10 Apr
Mobile23 Mar – 24 Apr
Feasibility spike23 Mar – 27 Mar
Build30 Mar – 24 Apr
Launch13 Apr – 1 May
Write the copy13 Apr – 22 Apr
Ship1 May
18 Mar
March 2026
16171819202122
Day: as opened · Week: as opened
razor
@* Today alone: Later scrolls to the end of the day, then moves on to the next one. *@
<VetoGantt Items="Jobs"
           TodayOverride="Today"
           TimeOverride="new TimeOnly(11, 30)"
           ShowViewPicker="false"
           ShowSearch="false"
           ShowCategoryFilter="false"
           Exportable="false"
           SidebarWidth="13rem"
           Height="11rem"
           OnRangeChanged="range => _dayRange = range" />

@* A week fitted to the width: nothing to scroll, so Later moves it a week on. The page owns
   the range - each new one is fed back as Start and End, as a page loading rows per range does. *@
<VetoGantt Items="Tasks"
           View="VetoGanttView.Schedule"
           TodayOverride="Today"
           Start="_weekRange?.Start ?? new DateOnly(2026, 3, 16)"
           End="_weekRange?.End ?? new DateOnly(2026, 3, 22)"
           ShowViewPicker="false"
           ShowScalePicker="false"
           ShowSearch="false"
           ShowCategoryFilter="false"
           Exportable="false"
           SidebarWidth="13rem"
           Height="17rem"
           OnRangeChanged="range => _weekRange = range" />

<span>
    Day: @(_dayRange is { } day ? day.Start.ToString("d MMM") : "as opened")
    · Week: @(_weekRange is { } week ? $"{week.Start:d MMM} – {week.End:d MMM}" : "as opened")
</span>

@* Not shown above: arrows that only scroll, and do nothing at either end. *@
<VetoGantt Items="Tasks" PageBeyondRange="false" />

@code {
    private static readonly DateOnly Today = new(2026, 3, 18);

    private VetoGanttRange? _dayRange;
    private VetoGanttRange? _weekRange;

    // Jobs: the list written out under "Jobs by the hour"; Tasks: under "A schedule".
}

Many jobs on one line

SubItemLayout=Inline draws a parent's jobs side by side on the parent's own line, rather than a row each - a pipeline with a run a day stays one row. The fold arrow still opens it into rows when you want the detail; Collapsible=false takes the arrow away and keeps every parent on one line.

Tasks
Nightly build16 Mar – 20 Mar
Reports16 Mar – 19 Mar
18 Mar
March 2026
141516171819202122
Tasks
Nightly build16 Mar – 20 Mar
Reports16 Mar – 19 Mar
18 Mar
March 2026
141516171819202122
razor
@* Each pipeline's jobs on its own line; the fold arrow opens them into rows. *@
<VetoGantt Items="Pipelines"
           View="VetoGanttView.Schedule"
           TodayOverride="Today"
           SubItemLayout="VetoGanttSubItemLayout.Inline"
           ShowToolbar="false"
           SidebarWidth="13rem"
           Height="9rem"
           ScrollToTodayOnLoad="false" />

@* Always one line: no fold arrows. *@
<VetoGantt Items="Pipelines"
           View="VetoGanttView.Schedule"
           TodayOverride="Today"
           SubItemLayout="VetoGanttSubItemLayout.Inline"
           Collapsible="false"
           ShowToolbar="false"
           SidebarWidth="13rem"
           Height="9rem"
           ScrollToTodayOnLoad="false" />

@code {
    private static readonly DateOnly Today = new(2026, 3, 18);

    private const string Engineering = "var(--veto-color-primary)";
    private const string Research = "var(--veto-color-warning)";

    private static readonly IReadOnlyList<VetoGanttItem> Pipelines =
    [
        new() { Id = "nightly", Name = "Nightly build", Start = new(2026, 3, 16), End = new(2026, 3, 20), Category = "Engineering", Color = Engineering },
        new() { Id = "compile", Name = "Compile", ParentId = "nightly", Start = new(2026, 3, 16), End = new(2026, 3, 16), Category = "Engineering", Color = Engineering },
        new() { Id = "test", Name = "Test", ParentId = "nightly", Start = new(2026, 3, 17), End = new(2026, 3, 17), Category = "Engineering", Color = Engineering },
        new() { Id = "package", Name = "Package", ParentId = "nightly", Start = new(2026, 3, 18), End = new(2026, 3, 18), Category = "Engineering", Color = Engineering },
        new() { Id = "deploy", Name = "Deploy", ParentId = "nightly", Start = new(2026, 3, 19), End = new(2026, 3, 20), Category = "Engineering", Color = Engineering },

        new() { Id = "reports", Name = "Reports", Start = new(2026, 3, 16), End = new(2026, 3, 19), Category = "Research", Color = Research },
        new() { Id = "extract", Name = "Extract", ParentId = "reports", Start = new(2026, 3, 16), End = new(2026, 3, 16), Category = "Research", Color = Research },
        new() { Id = "publish", Name = "Publish", ParentId = "reports", Start = new(2026, 3, 18), End = new(2026, 3, 19), Category = "Research", Color = Research }
    ];
}

A schedule

Projects collapse, tasks indent beneath them, and a summary bar spans its children as a slim rail rather than a block. Each category carries its own colour, and the filter chips are tinted to match - so the filter is also the key to the bars. Switch the scale, or use Today to jump back to now.

Tasks
Platform2 Mar – 10 Apr
Design system2 Mar – 13 Mar
App shell9 Mar – 24 Mar
Component library16 Mar – 10 Apr
Mobile23 Mar – 24 Apr
Feasibility spike23 Mar – 27 Mar
Build30 Mar – 24 Apr
Launch13 Apr – 1 May
Write the copy13 Apr – 22 Apr
Ship1 May
18 Mar
February 2026March 2026April 2026May 2026
2812345678910111213141516171819202122232425262728293031123456789101112131415161718192021222324252627282930123
razor
<VetoGantt Items="Tasks"
           View="VetoGanttView.Schedule"
           TodayOverride="Today"
           @bind-Scale="_scale"
           @bind-SelectedId="_selectedId"
           Height="26rem"
           OnExport="OnExport" />

@code {
    private VetoGanttScale _scale = VetoGanttScale.Day;
    private string? _selectedId;
    private string? _exportNote;

    // The demos' fixed today. Leave TodayOverride out and the chart takes the real date.
    private static readonly DateOnly Today = new(2026, 3, 18);

    private Task OnExport ( VetoGanttExportRequest request )
    {
        _exportNote = $"Exported {request.Items.Count} rows as {request.Format}.";
        return Task.CompletedTask;
    }

    // A colour per category, so the category chips read as a key to the bars.
    private const string Engineering = "var(--veto-color-primary)";
    private const string Design = "var(--veto-color-secondary)";
    private const string Research = "var(--veto-color-warning)";
    private const string Marketing = "var(--veto-color-success)";

    private static readonly IReadOnlyList<VetoGanttItem> Tasks =
    [
        new() { Id = "platform", Name = "Platform", Start = new(2026, 3, 2), End = new(2026, 4, 10), Category = "Engineering", Color = Engineering, Progress = 0.55 },
        new() { Id = "design", Name = "Design system", ParentId = "platform", Start = new(2026, 3, 2), End = new(2026, 3, 13), Category = "Design", Color = Design, Progress = 1 },
        new() { Id = "shell", Name = "App shell", ParentId = "platform", Start = new(2026, 3, 9), End = new(2026, 3, 24), Category = "Engineering", Color = Engineering, Progress = 0.8, DependsOn = ["design"] },
        new() { Id = "components", Name = "Component library", ParentId = "platform", Start = new(2026, 3, 16), End = new(2026, 4, 10), Category = "Engineering", Color = Engineering, Progress = 0.35, DependsOn = ["design"] },

        new() { Id = "mobile", Name = "Mobile", Start = new(2026, 3, 23), End = new(2026, 4, 24), Category = "Engineering", Color = Engineering, Progress = 0.1 },
        new() { Id = "spike", Name = "Feasibility spike", ParentId = "mobile", Start = new(2026, 3, 23), End = new(2026, 3, 27), Category = "Research", Color = Research, Progress = 0.6 },
        new() { Id = "build", Name = "Build", ParentId = "mobile", Start = new(2026, 3, 30), End = new(2026, 4, 24), Category = "Engineering", Color = Engineering, DependsOn = ["spike", "shell"] },

        new() { Id = "launch", Name = "Launch", Start = new(2026, 4, 13), End = new(2026, 5, 1), Category = "Marketing", Color = Marketing, Progress = 0 },
        new() { Id = "copy", Name = "Write the copy", ParentId = "launch", Start = new(2026, 4, 13), End = new(2026, 4, 22), Category = "Marketing", Color = Marketing, DependsOn = ["components"] },
        new() { Id = "ship", Name = "Ship", ParentId = "launch", Start = new(2026, 5, 1), End = new(2026, 5, 1), Category = "Marketing", Color = Marketing, IsMilestone = true, DependsOn = ["build", "copy"] }
    ];
}

Filters you control

Filters takes toggles of your own - any predicate over an item - and they sit in the toolbar beside the categories. The category filter and your toggles bind both ways, so the page can set them and hears every click; OnFilterChanged reports the whole picture, rows included, after any change. ItemFilter applies a rule with no toggle at all.

Tasks
Platform2 Mar – 10 Apr
Design system2 Mar – 13 Mar
App shell9 Mar – 24 Mar
Component library16 Mar – 10 Apr
Mobile23 Mar – 24 Apr
Feasibility spike23 Mar – 27 Mar
Build30 Mar – 24 Apr
Launch13 Apr – 1 May
Write the copy13 Apr – 22 Apr
Ship1 May
18 Mar
February 2026March 2026April 2026May 2026
2812345678910111213141516171819202122232425262728293031123456789101112131415161718192021222324252627282930123
No filter changed yet.
razor
<button @onclick='() => _hidden = ["Marketing"]'>Hide Marketing</button>
<button @onclick='() => _active = ["underway"]'>Only work under way</button>
<button @onclick="() => _chart.ClearFiltersAsync()">Clear filters</button>

<VetoGantt @ref="_chart"
           Items="Tasks"
           View="VetoGanttView.Schedule"
           TodayOverride="Today"
           Filters="TaskFilters"
           @bind-HiddenCategories="_hidden"
           @bind-ActiveFilters="_active"
           OnFilterChanged="OnFilterChanged"
           Exportable="false"
           SidebarWidth="13rem"
           Height="17rem"
           ScrollToTodayOnLoad="false" />

<span>@_filterState</span>

@code {
    private static readonly DateOnly Today = new(2026, 3, 18);

    private VetoGantt _chart = default!;
    private IReadOnlyCollection<string> _hidden = [];
    private IReadOnlyCollection<string> _active = [];
    private string _filterState = "No filter changed yet.";

    private static readonly IReadOnlyList<VetoGanttFilter> TaskFilters =
    [
        new("underway", "Under way", item => item.Progress is > 0 and < 1) { Tooltip = "Started, not finished" },
        new("unstarted", "Not started", item => item.Progress is null or 0)
    ];

    private void OnFilterChanged ( VetoGanttFilterState state )
    {
        var active = state.ActiveFilters.Count == 0 ? "none" : string.Join(", ", state.ActiveFilters);
        var hidden = state.HiddenCategories.Count == 0 ? "none" : string.Join(", ", state.HiddenCategories);

        _filterState = $"Filters: {active} · Hidden: {hidden} · {state.VisibleItems.Count} rows";
    }

    // Tasks: the list written out under "A schedule".
}

Only the controls you want

Scales lists the zoom levels the picker offers - give it one, Day here, and there is nothing to pick, so the picker goes. ExportFormats lists the formats the export menu offers; CSV alone here, and an empty list hides the menu.

Tasks
Platform2 Mar – 10 Apr
Design system2 Mar – 13 Mar
App shell9 Mar – 24 Mar
Component library16 Mar – 10 Apr
Mobile23 Mar – 24 Apr
Feasibility spike23 Mar – 27 Mar
Build30 Mar – 24 Apr
Launch13 Apr – 1 May
Write the copy13 Apr – 22 Apr
Ship1 May
18 Mar
February 2026March 2026April 2026May 2026
2812345678910111213141516171819202122232425262728293031123456789101112131415161718192021222324252627282930123
razor
@* Day only: one scale, so no picker. CSV only in the export menu. *@
<VetoGantt Items="Tasks"
           View="VetoGanttView.Schedule"
           TodayOverride="Today"
           Scales="DayOnly"
           ExportFormats="CsvOnly"
           ShowDateLine="false"
           ShowSearch="false"
           ShowCategoryFilter="false"
           SidebarWidth="13rem"
           Height="12rem" />

@* Not shown above: no export at all. *@
<VetoGantt Items="Tasks" View="VetoGanttView.Schedule" ExportFormats="[]" />

@code {
    private static readonly DateOnly Today = new(2026, 3, 18);

    private static readonly VetoGanttScale[] DayOnly = [VetoGanttScale.Day];
    private static readonly VetoGanttExportFormat[] CsvOnly = [VetoGanttExportFormat.Csv];

    // Tasks: the list written out under "A schedule".
}

Your own wording

Today's date sits on a line of its own above the timeline, outside the part that scrolls, so it stays in view wherever the timeline is scrolled - and the day above the hours sticks to the edge too. DateLineFormat changes how the date reads, DateLineTemplate draws it your own way, and ShowDateLine=false removes the line. HeaderFormat and HeaderGroupFormat set the header's two rows; every built-in text in the toolbar is a parameter.

Tasks
Operations18 Mar 06:00–21:00
Data load18 Mar 06:00–08:30
Reports18 Mar 09:00–10:00
Deploy18 Mar 14:00–15:30
Cleanup18 Mar 19:00–21:00
Support rota18 Mar 08:00–17:00
Backup (last night)17 Mar 22:00–23:30
Release (tomorrow)19 Mar 09:00–10:00
11:30
TuesdayWednesdayThursday
0:001:002:003:004:005:006:007:008:009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:000:001:002:003:004:005:006:007:008:009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:000:001:002:003:004:005:006:007:008:009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:00
razor
<VetoGantt Items="Jobs"
           View="VetoGanttView.Schedule"
           Scale="VetoGanttScale.Hour"
           TodayOverride="Today"
           TimeOverride="new TimeOnly(11, 30)"
           DateLineFormat="ddd d MMM"
           HeaderFormat="H:mm"
           HeaderGroupFormat="dddd"
           SearchPlaceholder="Search jobs"
           TodayViewLabel="Now"
           ScheduleViewLabel="Plan"
           TodayButtonText="Back to now"
           ExportText="Download"
           SidebarWidth="13rem"
           Height="11rem" />

@* Not shown above: the date line drawn your own way - or not at all. *@
<VetoGantt Items="Jobs">
    <DateLineTemplate Context="day">
        On shift: <strong>@day.ToString("dddd")</strong>
    </DateLineTemplate>
</VetoGantt>

<VetoGantt Items="Jobs" ShowDateLine="false" />

@code {
    private static readonly DateOnly Today = new(2026, 3, 18);

    // Jobs: the list written out under "Jobs by the hour".
}

Dark and light mode

The chart takes every colour from Veto's theme tokens, so it switches with the page: set up Veto.Theming once - AddVetoTheming(), the init script, a VetoThemeProvider in the layout - and any VetoThemeToggle switches the chart along with everything else. The Toolbar slot puts one in the chart's own toolbar, as in the first chart. ColorScheme pins one chart to a palette whatever the page is doing: the second is always dark, the third always light. The dark one also shows NowLabelAppearance=Text - the time on the needle with no tab behind it. To change the colours themselves, give the theme service a VetoTheme with a light and a dark palette: the chart, and every other Veto component, picks up both - and a chart pinned to one palette gets your version of it.

Tasks
Operations06:00–21:00
Data load06:00–08:30
Reports09:00–10:00
Deploy14:00–15:30
Cleanup19:00–21:00
Support rota08:00–17:00
11:30
00:0001:0002:0003:0004:0005:0006:0007:0008:0009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:00
Tasks
Operations06:00–21:00
Data load06:00–08:30
Reports09:00–10:00
Deploy14:00–15:30
Cleanup19:00–21:00
Support rota08:00–17:00
11:30
00:0001:0002:0003:0004:0005:0006:0007:0008:0009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:00
Tasks
Operations06:00–21:00
Data load06:00–08:30
Reports09:00–10:00
Deploy14:00–15:30
Cleanup19:00–21:00
Support rota08:00–17:00
11:30
00:0001:0002:0003:0004:0005:0006:0007:0008:0009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:00
razor
@* Once, in Program.cs: *@
builder.Services.AddVetoTheming();

@* Once, in <head> - before the stylesheet, so the saved theme applies before first paint: *@
<script src="_content/Veto.Theming/veto-theme-init.js"></script>
<link rel="stylesheet" href="_content/Veto.Core/VetoStyles.css" />

@* Once, in the layout: *@
<VetoThemeProvider />

@* The chart follows the page, with a toggle in its own toolbar: *@
<VetoGantt Items="Jobs"
           TodayOverride="Today"
           TimeOverride="new TimeOnly(11, 30)"
           ShowSearch="false"
           ShowCategoryFilter="false"
           Exportable="false"
           SidebarWidth="13rem"
           Height="11rem">
    <Toolbar>
        <VetoThemeToggle />
    </Toolbar>
</VetoGantt>

@* Always dark, whatever the page is in - and the time on the needle with no tab: *@
<VetoGantt Items="Jobs"
           TodayOverride="Today"
           TimeOverride="new TimeOnly(11, 30)"
           ColorScheme="VetoColorScheme.Dark"
           NowLabelAppearance="VetoGanttNowLabelAppearance.Text"
           ShowToolbar="false"
           SidebarWidth="13rem"
           Height="9rem" />

@* Always light: *@
<VetoGantt Items="Jobs"
           TodayOverride="Today"
           TimeOverride="new TimeOnly(11, 30)"
           ColorScheme="VetoColorScheme.Light"
           ShowToolbar="false"
           SidebarWidth="13rem"
           Height="9rem" />

@code {
    private static readonly DateOnly Today = new(2026, 3, 18);

    // Jobs: the list written out under "Jobs by the hour".
}

@* Not shown above: switching from code... *@
@inject IVetoThemeService Theme
<button @onclick="() => Theme.ToggleAsync()">Dark / light</button>

@* ...and your own colours, one palette per mode. The bars take Primary unless an item
   has a Color of its own; a Color given as a token - var(--veto-color-success) - follows
   the mode too. *@
@code {
    protected override Task OnInitializedAsync () => Theme.SetThemeAsync(new VetoTheme
    {
        Light = new VetoPalette
        {
            Primary = "#0f766e",
            Danger = "#b91c1c",            // the now marker and its time
            Surface = "#ffffff",
            Text = "#0f172a",
            Border = "#e2e8f0"
        },
        Dark = new VetoPalette
        {
            Primary = "#2dd4bf",
            Danger = "#f87171",
            Surface = "#0b1220",
            Text = "#e2e8f0",
            Border = "#1e293b"
        }
    });
}

Selection

Bars are buttons, so they are reachable by keyboard and announce themselves. OnItemSelected gives you the row that was activated; SelectedId can be bound both ways.

Nothing selected — click a bar above.
razor
@* The chart is the one under "A schedule"; these lines report what it selects and exports. *@
<VetoGantt Items="Tasks"
           View="VetoGanttView.Schedule"
           TodayOverride="Today"
           @bind-Scale="_scale"
           @bind-SelectedId="_selectedId"
           Height="26rem"
           OnExport="OnExport" />

<span>@(_selectedId is null ? "Nothing selected — click a bar above." : $"Selected: {_selectedId}")</span>
<span>@_exportNote</span>

@* Not shown above: OnItemSelected hands you the whole row, not just its id. *@
<VetoGantt Items="Tasks" View="VetoGanttView.Schedule" OnItemSelected="item => _detail = item" />

@code {
    private VetoGanttItem? _detail;

    // _scale, _selectedId, _exportNote, OnExport, Today and Tasks: as under "A schedule".
}

Right-click menu

ItemContextMenu is a menu of your own for the row under a right-click - on its bar or its name - opened at the pointer. It is handed the item, and filled with VetoMenuItem, VetoMenuLabel and VetoMenuDivider as any VetoMenu is: each item does what its OnClick says, and choosing one closes the menu. Escape or a click elsewhere closes it too; the arrow keys move between items. Leave it null and right-click is the browser's own.

Tasks
Platform2 Mar – 10 Apr
Design system2 Mar – 13 Mar
App shell9 Mar – 24 Mar
Component library16 Mar – 10 Apr
Mobile23 Mar – 24 Apr
Feasibility spike23 Mar – 27 Mar
Build30 Mar – 24 Apr
Launch13 Apr – 1 May
Write the copy13 Apr – 22 Apr
Ship1 May
18 Mar
February 2026March 2026April 2026May 2026
2812345678910111213141516171819202122232425262728293031123456789101112131415161718192021222324252627282930123
Right-click a bar or a name.
razor
<VetoGantt Items="Tasks"
           View="VetoGanttView.Schedule"
           TodayOverride="Today"
           ShowToolbar="false"
           SidebarWidth="13rem"
           Height="17rem">
    <ItemContextMenu Context="item">
        <VetoMenuLabel Text="@item.Name" />
        <VetoMenuItem Text="Open" OnClick='() => _contextNote = $"Opened {item.Name}"' />
        <VetoMenuItem Text="Copy id" OnClick='() => _contextNote = $"Copied {item.Id}"' />
        <VetoMenuDivider />
        @* Any item can be switched off for the row it was opened on. *@
        <VetoMenuItem Text="Delete" Destructive="true" Disabled="@(item.Progress == 1)"
                      OnClick='() => _contextNote = $"Deleted {item.Name}"' />
    </ItemContextMenu>
</VetoGantt>

<span>@(_contextNote ?? "Right-click a bar or a name.")</span>

@code {
    private string? _contextNote;

    // Today and Tasks: as under "A schedule".
}

Milestones and colours

A milestone is a point rather than a span, drawn as the conventional diamond. Any row can carry its own colour - including a theme token, so it follows light and dark.

Tasks
Discovery2 Mar – 20 Mar
Alpha20 Mar
Hardening23 Mar – 17 Apr
Release17 Apr
18 Mar
February 2026March 2026April 2026
232916233061320
razor
<VetoGantt Items="Milestones"
           View="VetoGanttView.Schedule"
           TodayOverride="Today"
           Scale="VetoGanttScale.Week"
           ShowToolbar="false"
           SidebarWidth="12rem"
           Height="14rem"
           ScrollToTodayOnLoad="false" />

@code {
    private static readonly DateOnly Today = new(2026, 3, 18);

    // A milestone is a single day with IsMilestone; the colours are theme tokens, so they
    // follow light and dark.
    private static readonly IReadOnlyList<VetoGanttItem> Milestones =
    [
        new() { Id = "a", Name = "Discovery", Start = new(2026, 3, 2), End = new(2026, 3, 20), Color = "var(--veto-color-success)", Progress = 1 },
        new() { Id = "b", Name = "Alpha", Start = new(2026, 3, 20), End = new(2026, 3, 20), IsMilestone = true, Color = "var(--veto-color-warning)" },
        new() { Id = "c", Name = "Hardening", Start = new(2026, 3, 23), End = new(2026, 4, 17), Color = "var(--veto-color-secondary)", Progress = 0.4 },
        new() { Id = "d", Name = "Release", Start = new(2026, 4, 17), End = new(2026, 4, 17), IsMilestone = true, Color = "var(--veto-color-danger)" }
    ];
}

Chained tasks

A row that cannot start until another finishes lists it in DependsOn, and the chain is drawn as an arrow from the end of one bar to the start of the next. Select a row and the arrows touching it are picked out, which is how you follow a chain through a crowded schedule. The last arrow here routes back on itself, because that dependency is already broken - the successor starts before its predecessor ends.

Tasks
Survey2 Mar – 6 Mar
Draft9 Mar – 17 Mar
Review18 Mar – 24 Mar
Publish20 Mar – 26 Mar
18 Mar
February 2026March 2026
2812345678910111213141516171819202122232425262728
razor
<VetoGantt Items="Chain"
           View="VetoGanttView.Schedule"
           TodayOverride="Today"
           ShowToolbar="false"
           SidebarWidth="12rem"
           Height="15rem"
           ScrollToTodayOnLoad="false"
           @bind-SelectedId="_selected" />

@code {
    private static readonly DateOnly Today = new(2026, 3, 18);
    private string? _selected;

    private static readonly IReadOnlyList<VetoGanttItem> Chain =
    [
        new() { Id = "survey", Name = "Survey", Start = new(2026, 3, 2), End = new(2026, 3, 6), Progress = 1 },
        new() { Id = "draft", Name = "Draft", Start = new(2026, 3, 9), End = new(2026, 3, 17), DependsOn = ["survey"], Progress = 0.5 },
        new() { Id = "review", Name = "Review", Start = new(2026, 3, 18), End = new(2026, 3, 24), DependsOn = ["draft"] },
        // Starts before Review finishes: the arrow routes back rather than cutting through.
        new() { Id = "publish", Name = "Publish", Start = new(2026, 3, 20), End = new(2026, 3, 26), DependsOn = ["review"] }
    ];
}

Tooltips

Hovering or focusing a bar shows its name, dates, progress and description. TooltipText rewrites that text, and returning null leaves a row without one. TooltipTemplate swaps the browser's tooltip for a card you lay out yourself. ShowTooltips=false turns them off, and the bars keep their accessible names.

Tasks
Platform2 Mar – 10 Apr
Design system2 Mar – 13 Mar
App shell9 Mar – 24 Mar
Component library16 Mar – 10 Apr
Mobile23 Mar – 24 Apr
Feasibility spike23 Mar – 27 Mar
Build30 Mar – 24 Apr
Launch13 Apr – 1 May
Write the copy13 Apr – 22 Apr
Ship1 May
18 Mar
February 2026March 2026April 2026May 2026
2812345678910111213141516171819202122232425262728293031123456789101112131415161718192021222324252627282930123
Tasks
Platform2 Mar – 10 Apr
Design system2 Mar – 13 Mar
App shell9 Mar – 24 Mar
Component library16 Mar – 10 Apr
Mobile23 Mar – 24 Apr
Feasibility spike23 Mar – 27 Mar
Build30 Mar – 24 Apr
Launch13 Apr – 1 May
Write the copy13 Apr – 22 Apr
Ship1 May
18 Mar
February 2026March 2026April 2026May 2026
2812345678910111213141516171819202122232425262728293031123456789101112131415161718192021222324252627282930123
razor
@* Your own text in the native tooltip. Return null for no tooltip on that row. *@
<VetoGantt Items="Tasks"
           View="VetoGanttView.Schedule"
           TodayOverride="Today"
           ShowToolbar="false"
           SidebarWidth="12rem"
           Height="14rem"
           ScrollToTodayOnLoad="false"
           TooltipText="ShortTooltip" />

@* Or a card you lay out yourself. *@
<VetoGantt Items="Tasks"
           View="VetoGanttView.Schedule"
           TodayOverride="Today"
           ShowToolbar="false"
           SidebarWidth="12rem"
           Height="14rem"
           ScrollToTodayOnLoad="false">
    <TooltipTemplate Context="task">
        <strong>@task.Name</strong>
        <div>@task.Start.ToString("d MMM") – @task.End.ToString("d MMM") · @task.DurationDays days</div>
        @if (task.Progress is { } done)
        {
            <div>@done.ToString("P0") done</div>
        }
        @if (task.Category is not null)
        {
            <div>@task.Category</div>
        }
    </TooltipTemplate>
</VetoGantt>

@* Not shown above: no tooltips at all. *@
<VetoGantt Items="Tasks" View="VetoGanttView.Schedule" ShowTooltips="false" />

@code {
    private static readonly DateOnly Today = new(2026, 3, 18);

    private static string? ShortTooltip ( VetoGanttItem item )
        => item.IsMilestone ? item.Name : $"{item.Name} - {item.DurationDays} days";

    // Tasks: the list written out under "A schedule".
}

Driving it from code

Take an @ref and the chart's own actions are yours to call: select a row, fold the tree, zoom, scroll to a date, export, or open a row's tooltip card to point something out. Each one does what its button or bar would, callbacks included, so bound state stays in step.

Tasks
Platform2 Mar – 10 Apr
Design system2 Mar – 13 Mar
App shell9 Mar – 24 Mar
Component library16 Mar – 10 Apr
Mobile23 Mar – 24 Apr
Feasibility spike23 Mar – 27 Mar
Build30 Mar – 24 Apr
Launch13 Apr – 1 May
Write the copy13 Apr – 22 Apr
Ship1 May
18 Mar
February 2026March 2026April 2026May 2026
2812345678910111213141516171819202122232425262728293031123456789101112131415161718192021222324252627282930123
Selected: none · Scale: Day
razor
<button @onclick='() => _chart.SelectAsync("shell")'>Select App shell</button>
<button @onclick="() => _chart.CollapseAll()">Collapse all</button>
<button @onclick="() => _chart.ExpandAll()">Expand all</button>
<button @onclick="() => _chart.SetScaleAsync(VetoGanttScale.Week)">Week view</button>
<button @onclick="() => _chart.ScrollToDateAsync(new(2026, 4, 13))">Go to launch</button>
<button @onclick='() => _chart.ShowTooltip("design")'>Point out Design</button>
<button @onclick="() => _chart.HideTooltip()">Hide card</button>

<VetoGantt @ref="_chart"
           TooltipZIndex="2500"
           View="VetoGanttView.Schedule"
           Items="Tasks"
           TodayOverride="Today"
           ShowToolbar="false"
           SidebarWidth="12rem"
           Height="16rem"
           ScrollToTodayOnLoad="false"
           @bind-SelectedId="_selected"
           @bind-Scale="_scale">
    <TooltipTemplate Context="task">
        <strong>@task.Name</strong>
        <div>@task.DurationDays days</div>
    </TooltipTemplate>
</VetoGantt>

<span>Selected: @(_selected ?? "none") · Scale: @_scale</span>

@code {
    private static readonly DateOnly Today = new(2026, 3, 18);

    private VetoGantt _chart = default!;
    private string? _selected;
    private VetoGanttScale _scale = VetoGanttScale.Day;

    // Tasks: the list written out under "A schedule".
}

Dragging, and remembering

Drag a bar to move it, or its edges to resize - snapped to whatever zoom is showing. Arrow keys do the same from the keyboard, with Shift to change the end instead. Changes are kept in this browser for you alone, so a reload finds them again; Reset clears them. A summary row cannot be dragged, because its dates are the span of its children.

Tasks
Wiring9 Mar – 17 Mar
Audit4 Mar – 10 Mar
Rollout20 Mar – 31 Mar
Blueprint2 Mar – 6 Mar
18 Mar
February 2026March 2026April 2026
281234567891011121314151617181920212223242526272829303112
Order
Drag a bar, or focus one and press an arrow key.
razor
<VetoGantt Items="Editable"
           View="VetoGanttView.Schedule"
           TodayOverride="Today"
           Editable="true"
           PersistKey="veto-docs-gantt"
           SortBy="_sort"
           Height="17rem"
           SidebarWidth="13rem"
           ScrollToTodayOnLoad="false"
           ShowScalePicker="true"
           ShowCategoryFilter="false"
           ShowSearch="false"
           OnItemChanged="OnChanged" />

<span>Order</span>
<button @onclick="() => _sort = VetoGanttSort.Source">As supplied</button>
<button @onclick="() => _sort = VetoGanttSort.Name">Alphabetical</button>
<button @onclick="() => _sort = VetoGanttSort.Start">By start date</button>

<span>@(_change ?? "Drag a bar, or focus one and press an arrow key.")</span>

@code {
    private static readonly DateOnly Today = new(2026, 3, 18);

    private VetoGanttSort _sort = VetoGanttSort.Source;
    private string? _change;

    // The chart reports the change; it does not edit the list you gave it.
    private void OnChanged ( VetoGanttChange change )
        => _change = $"{change.Id}: {change.Edit} by {change.Days} days — now {change.Start:d MMM} to {change.End:d MMM}";

    // Deliberately not in alphabetical order, so the ordering buttons visibly do something.
    private static readonly IReadOnlyList<VetoGanttItem> Editable =
    [
        new() { Id = "wiring", Name = "Wiring", Start = new(2026, 3, 9), End = new(2026, 3, 17), Color = "var(--veto-color-primary)", Progress = 0.7 },
        new() { Id = "audit", Name = "Audit", Start = new(2026, 3, 4), End = new(2026, 3, 10), Color = "var(--veto-color-warning)", Progress = 1 },
        new() { Id = "rollout", Name = "Rollout", Start = new(2026, 3, 20), End = new(2026, 3, 31), Color = "var(--veto-color-success)" },
        new() { Id = "design", Name = "Blueprint", Start = new(2026, 3, 2), End = new(2026, 3, 6), Color = "var(--veto-color-secondary)", Progress = 1 }
    ];
}

Waiting for data

Two different waits. Before anything has arrived, Loading draws a placeholder chart that holds the layout, so nothing jumps when the rows land. Once there are rows on screen, Refreshing veils them instead - replacing what someone is reading with grey boxes takes away what they had.

Tasks
Survey2 Mar – 6 Mar
Draft9 Mar – 17 Mar
Review18 Mar – 24 Mar
Publish20 Mar – 26 Mar
18 Mar
February 2026March 2026
2812345678910111213141516171819202122232425262728
razor
<VetoButton Size="VetoSize.Small" Variant="VetoVariant.Outlined" Color="VetoColor.Neutral"
            OnClick="FirstLoadAsync">
    First load
</VetoButton>
<VetoButton Size="VetoSize.Small" Variant="VetoVariant.Outlined" Color="VetoColor.Neutral"
            OnClick="RefreshAsync">
    Refresh
</VetoButton>

<VetoGantt Items="Chain"
           View="VetoGanttView.Schedule"
           TodayOverride="Today"
           Loading="_loading"
           Refreshing="_refreshing"
           ShowToolbar="false"
           SidebarWidth="12rem"
           Height="15rem"
           LoadingRows="4"
           ScrollToTodayOnLoad="false" />

@code {
    private static readonly DateOnly Today = new(2026, 3, 18);

    private bool _loading;
    private bool _refreshing;

    // Nothing on screen yet: a placeholder chart holds the layout. The delay stands in
    // for the fetch.
    private async Task FirstLoadAsync ()
    {
        _loading = true;
        await Task.Delay(2200);
        _loading = false;
    }

    // Rows already on screen: veil them rather than replacing them.
    private async Task RefreshAsync ()
    {
        _refreshing = true;
        await Task.Delay(2200);
        _refreshing = false;
    }

    // Chain: the list written out under "Chained tasks".
}

Nothing to show

A filter that matches nothing says so, rather than leaving an empty panel that looks broken.

Tasks
No tasks match these filters.
18 Mar
March 2026April 2026
11121314151617181920212223242526272829303112345678
razor
<VetoGantt Items="Tasks"
           View="VetoGanttView.Schedule"
           TodayOverride="Today"
           Search="a task that does not exist"
           ShowToolbar="false"
           Height="9rem"
           ScrollToTodayOnLoad="false" />

@* The message is EmptyText, "No tasks match these filters." unless you set your own. *@

@code {
    private static readonly DateOnly Today = new(2026, 3, 18);

    // Tasks: the list written out under "A schedule".
}

Export

The formats live behind one export menu rather than a button each: the toolbar is already carrying filters, a zoom picker and date navigation, and export is the control a reader wants least often. It opens on hover by default - a click and the keyboard open it too - so it costs a single button of room until it is needed. Set ExportOpenOn, ExportAppearance and ShowExportLabel to trade that room differently.

Either format downloads what is currently filtered - what the reader can see, which is almost always what they meant. Dates are written as yyyy-MM-dd rather than in the display format, so a spreadsheet in another locale reads them as the same days.

OnExport fires first. Set Handled on the request and the component steps aside, leaving you the filtered rows to send to a server or turn into a format it does not produce.

csharp
<VetoGantt Items="Tasks" View="VetoGanttView.Schedule" OnExport="OnExport" ExportFileName="schedule" />

@code {
    private async Task OnExport(VetoGanttExportRequest request)
    {
        // request.Items holds the rows as currently filtered.
        if (request.Format == VetoGanttExportFormat.Csv)
        {
            await Server.SaveAsync(request.Items);
            request.Handled = true;   // the component will not download anything
        }
    }
}

Parameters

Parameter Type Default Description
Items IReadOnlyList<VetoGanttItem> [] The rows to chart. Hierarchy comes from each item's ParentId.
Scale VetoGanttScale Day QuarterHour, Hour, Day, Week or Month - the schedule view's zoom. Supports @bind-Scale.
Search string? null Filters rows by name. Supports @bind-Search.
SelectedId string? null The selected row. Supports @bind-SelectedId.
Start / End DateOnly? null The axis range. Worked out from the items when null.
TodayOverride DateOnly? null The day treated as today. Set it to keep a test or screenshot stable.
SidebarWidth string "18rem" Width of the panel on the left.
ShowDateLine bool true Shows today's date on a line of its own above the timeline, which stays in view however the timeline is scrolled.
DateLineFormat string "dddd d MMMM yyyy" How the date line reads - any .NET date format.
DateLineTemplate RenderFragment<DateOnly>? null The date line drawn your own way, given today's date.
HeaderFormat / HeaderGroupFormat string? null Formats for the header's lower and upper rows. Null keeps each scale's own.
SearchPlaceholder string "Filter tasks" The text in the empty search box.
TodayViewLabel / ScheduleViewLabel string "Today" / "Schedule" The labels of the view switch.
TodayButtonText / ExportText string "Today" / "Export" The texts of the Today button and the export menu.
RowHeight double 36 Height of every row in pixels, unless an item sets its own. A number because the dependency arrows are drawn from it.
GroupRowHeight double? null Height of the rows GroupBy makes. Null takes RowHeight.
ShowDependencies bool true Whether dependency arrows are drawn.
ShowTooltips bool true Whether hovering or focusing a bar shows a tooltip. Bars keep their accessible name either way.
TooltipText Func<VetoGanttItem, string?>? null Rewrites the native tooltip text. Return null to give a row no tooltip.
TooltipTemplate RenderFragment<VetoGanttItem>? null A hover card of your own in place of the native tooltip. Wins over TooltipText.
TooltipZIndex int? null How high the hover card layers. Null uses the --veto-z-tooltip token, 1000. The card is placed against the window, so the chart's scrolling never cuts it off.
View VetoGanttView Today Today: only today, fitted to the width, listing only today's work. Schedule: the whole plan. Supports @bind-View.
ShowViewPicker bool true Whether the toolbar shows the Today / Schedule switch.
FitToWidth bool true Widens the days of a short range until it fills the chart. A long range keeps its density and scrolls.
SubItemLayout VetoGanttSubItemLayout Rows Rows: a row per sub-item. Inline: every sub-item on its parent's line.
Collapsible bool true Whether parents can be folded. Off, rows stay open and inline parents stay on one line.
GroupBy Func<VetoGanttItem, string?>? null Groups top-level rows under a row per key - a server, a project - without writing those rows yourself.
StackOverlaps bool true Stacks jobs that overlap on one line into lanes, growing the row. Off, they draw over each other.
Filters IReadOnlyList<VetoGanttFilter> [] Toggles of your own in the toolbar, each a predicate over an item. Several on must all keep a row.
ActiveFilters IReadOnlyCollection<string>? null Ids of the Filters that are on. Supports @bind-ActiveFilters.
HiddenCategories IReadOnlyCollection<string>? null Categories switched off. Supports @bind-HiddenCategories.
ShowFilters bool true Whether the toolbar shows a toggle for each of the Filters.
ItemFilter Func<VetoGanttItem, bool>? null A rule with no toggle: rows it rejects are never listed.
OnFilterChanged EventCallback<VetoGanttFilterState> — Raised after any filter change, with every filter and the rows they leave.
Scales IReadOnlyList<VetoGanttScale> QuarterHour, Hour, Day, Week, Month The zoom levels the picker offers. One level hides the picker.
ExportFormats IReadOnlyList<VetoGanttExportFormat> Csv, Json The formats the export menu offers. Empty hides the menu.
StartTime / EndTime TimeOnly? null Trims the axis to a window of the day, such as 14:00 to 20:00, which is then fitted to the width.
TodayScale VetoGanttScale Hour The scale the today view is drawn at.
MinHourWidth double 44 The narrowest an hour is drawn. A day that would need less scrolls instead, so its time labels stay readable.
TimeOverride TimeOnly? null The time treated as now, for the marker at the hour scale. Set it with TodayOverride to keep a screenshot stable.
ShowNowLabel bool true Shows the current time on top of the now marker, in the header.
NowLabelFormat string? null The format of that time. Null reads HH:mm on the hour scales and the date on coarser ones.
NowBandHeight string "1rem" The height of the band at the foot of the header the time sits in, clear of the hour and day labels.
NowLabelAppearance VetoGanttNowLabelAppearance Tab Tab: the time on a tab in the marker's colour. Text: the time alone, no tab.
ColorScheme VetoColorScheme Inherit Inherit follows the page's theme. Light or Dark pins this chart to one palette.
LiveNowMarker bool true Moves the now marker and its time as the clock moves. Has no effect with TimeOverride set.
SortBy VetoGanttSort Source Source, Name, Start, End or Duration. Applied within each level of the tree.
SortDescending bool false Reverses whatever SortBy produces.
Editable bool false Lets bars be dragged to move, and arrow keys to do the same.
Resizable bool true Whether dragging an edge changes duration. Requires Editable.
OnItemChanged EventCallback<VetoGanttChange> — Raised after a move or resize. The chart does not edit your list.
PersistKey string? null Remembers this reader's edits in their own browser under this key.
PersistenceModuleUrl string? the PGlite CDN Where PGlite loads from. Null, or a failed load, falls back to localStorage.
ShowResetButton bool true Shows a control that clears what was stored about this reader.
Height string? null The chart's height. Null lets it grow to fit.
BarColor string var(--veto-color-primary) Colour for bars with none of their own.
ShowToolbar bool true Whether the toolbar renders at all.
ShowSearch / ShowCategoryFilter bool true Which filters appear in the toolbar.
ShowScalePicker / ShowNavigation bool true Whether the zoom picker, and the Earlier / Today / Later buttons, appear.
PageBeyondRange bool true Earlier and Later move the range itself once the timeline cannot scroll further that way. Off, they only scroll.
OnRangeChanged EventCallback<VetoGanttRange> — Raised when Earlier, Later or Today moves the range, with its first and last day.
Exportable bool true Whether the export menu appears in the toolbar.
ExportOpenOn VetoMenuTrigger Hover What opens the export menu. A click and the keyboard work either way.
ExportAppearance VetoMenuAppearance IconAndText Whether the formats show their icon, their name, or both.
ShowExportLabel bool true Whether the export trigger shows the word Export beside its icon.
ExportPlacement VetoMenuPlacement BottomEnd Which corner of the trigger the export menu hangs from.
ShowToday / ShowWeekends bool true The today marker and weekend shading.
ShowProgress / ShowBarLabels / ShowDates bool true Progress fills, names on bars, dates in the sidebar.
ScrollToTodayOnLoad bool true Whether the timeline scrolls the now marker to its centre by itself: on load, and whenever the view or scale changes.
ExportFileName string "gantt" Base name for exported files, without an extension.
OnItemSelected EventCallback<VetoGanttItem> — Raised when a bar is activated.
ItemContextMenu RenderFragment<VetoGanttItem>? null A menu of VetoMenuItems for the row under a right-click, opened at the pointer. Null leaves the browser's own.
OnExport EventCallback<VetoGanttExportRequest> — Raised before an export; set Handled to take it over.
Toolbar RenderFragment? null Extra controls at the end of the toolbar.

Methods (through @ref)

Parameter Type Default Description
SelectAsync(id) Task — Selects a row as a click on its bar would, raising SelectedIdChanged and OnItemSelected. Null clears it.
Expand(id) / Collapse(id) void — Opens or folds one row's children.
ExpandAll() / CollapseAll() void — Opens or folds every row that has children.
SetScaleAsync(scale) Task — Zooms as the scale picker does, raising ScaleChanged.
SetViewAsync(view) Task — Switches between today's work and the whole schedule, raising ViewChanged.
SetSearchAsync(text) Task — Sets the text filter, as typing does.
SetCategoryVisibleAsync(category, visible) Task — Switches a category on or off, as its chip does.
SetFilterActiveAsync(id, active) Task — Switches one of the Filters on or off, as its toggle does.
ClearFiltersAsync() Task — Clears the text, the categories and the toggles. ItemFilter stays.
ScrollToTodayAsync() Task — Scrolls the now marker to the centre, moving a paged range back first.
ScrollToDateAsync(date) Task — Brings a date into view; one outside the axis scrolls to the nearer end.
ScrollEarlierAsync() / ScrollLaterAsync() Task — Scrolls by most of a screen, or pages the range at either end, as the Earlier and Later buttons do.
ExportAsync(format) Task — Exports the filtered rows as CSV or JSON, going through OnExport first.
ShowTooltip(id) / HideTooltip() void — Opens or closes a row's TooltipTemplate card. Needs a template: the native tooltip cannot be opened from code.
ResetAsync() Task — Clears this reader's stored edits and puts every row back to its supplied dates.

VetoGanttItem

Parameter Type Default Description
Id string required Unique across the chart.
Name string required Shown in the panel on the left.
Start / End DateOnly required Inclusive at both ends.
StartTime / EndTime TimeOnly? null The time of day the work starts on Start and ends on End. Null covers the whole day.
new(id, name, startsAt, endsAt) DateTime or DateTimeOffset — Builds an item from exact timestamps. The end is exclusive; an offset is shown in local time.
RowHeight double? null This row's height in pixels, overriding the chart's. Its bar and lanes scale with it.
ParentId string? null The row this one sits under. Null for a top-level row.
Color string? null Any CSS colour, including a --veto-* token.
Progress double? null 0 to 1. Null hides the progress fill for this row.
Category string? null Used for the category filter.
IsMilestone bool false Draws a point in time rather than a span.
DependsOn IReadOnlyList<string> [] Ids of rows that must finish first. Each is drawn as an arrow into this row.
Description string? null Extra detail in the row's tooltip.
Something went wrong. Reload 🗙