AstrolUI AstrolUI
Yellow Orange Red Green Blue Purple Custom color
Light mode
AstrolBoard

AstrolBoard<TBox, TRelation> renders a Board<TBox, TRelation>: a list of boxes (Box<TBox>, with content of type TBox) and of relations between them (Relation<TRelation>). You decide how each box looks with BoxRender; the board changes position and size on the same object and raises OnChangeBoard so you can save it.

1. Minimal example

100%
Design
Ana
Build
Luis
Test
Eva

Move a box.

@using Astrol.Component.Board
@using Astrol.Entities.Basic.UI.Board
 
@* Drag the boxes, resize them from the corners, Ctrl+click to select several, arrows to move them,
 Ctrl+Z / Ctrl+Y, Ctrl+F to search and Ctrl+wheel to zoom. *@
<div style="height:380px">
 <AstrolBoard TBox="WorkItem" TRelation="object"
 Board="board"
 BackgroundType="BackgroundType.Points"
 MinWidth="80"
 MinHeight="60"
 OnChangeBoard="OnChange">
 <BoxRender>
 <div style="height:100%; padding:8px; border-radius:8px; border:1px solid var(--color-gray-a); background:var(--layout-menu-bg); overflow:hidden">
 <b>@context.BOX_NAME</b>
 <div style="font-size:.8em; opacity:.8">@context.Content.Owner</div>
 </div>
 </BoxRender>
 </AstrolBoard>
</div>
<p aria-live="polite">@message</p>
 
@code {
 private string message = "Move a box.";
 private readonly Board<WorkItem, object> board = Create();
 
 private void OnChange(Board<WorkItem, object> changed)
 {
 var first = changed.Boxes[0];
 message = $"{first.BOX_NAME}: X {first.X:0}, Y {first.Y:0}, {first.Width:0} x {first.Height:0}";
 }
 
 private static Board<WorkItem, object> Create()
 {
 var b = new Board<WorkItem, object>();
 b.Boxes.Add(new Box<WorkItem> { X = 30, Y = 40, Width = 160, Height = 90, BOX_NAME = "Design", Content = new WorkItem { Owner = "Ana" } });
 b.Boxes.Add(new Box<WorkItem> { X = 290, Y = 40, Width = 160, Height = 90, BOX_NAME = "Build", Content = new WorkItem { Owner = "Luis" } });
 b.Boxes.Add(new Box<WorkItem> { X = 290, Y = 220, Width = 160, Height = 90, BOX_NAME = "Test", Content = new WorkItem { Owner = "Eva" } });
 b.Relations.Add(new Relation<object> { BoxIdA = b.Boxes[0].Id, BoxIdB = b.Boxes[1].Id });
 b.Relations.Add(new Relation<object> { BoxIdA = b.Boxes[1].Id, BoxIdB = b.Boxes[2].Id });
 return b;
 }
 
 public class WorkItem
 {
 public string Owner { get; set; } = string.Empty;
 }
}

2. Data model

  • Board<TBox, TRelation>: Id (string), Scale (float, zoom; 1 = 100%), Boxes (List<Box<TBox>>) and Relations (List<Relation<TRelation>>).
  • Box<TBox>: Id (string, unique), BOX_NAME (text used by the search), X, Y, Width, Height (float, in board units without zoom), Color (color in the mini-map), Content (TBox, your data) and ShowGuides (bool?, guides for this box only).
  • Relation<TRelation>: BoxIdA and BoxIdB (the boxes it joins; if they are the same a loop is drawn at its bottom-right corner), Vertices (List<Vertex>, intermediate points), AnchorA / AnchorB (BoxAnchor?, fixed point of each box where the line starts and ends), Arrow (RelationArrow?), Label (string) and Content (TRelation).
  • Vertex: Id, X, Y and Order (the line goes through the vertices in that order).
  • Position: X, Y (received by the menu events, in board pixels; divide by Scale to get model units).
  • To add or remove boxes or relations, change the lists of the same Board object: the board detects it when it renders again and adds it to the history. If you assign another Board object, the board starts again (selection and history).

3. Mouse and keyboard

  • Drag a box: moves it (and all the selected ones, with the vertices of the relations between them).
  • Drag a corner or side of the box: resizes it, respecting MinWidth / MinHeight.
  • Drag a vertex: bends the relation. Drag the background: scrolls the board.
  • Drag the end of a line (with EditAnchors): fixes it to a point of the box border; double-click releases it.
  • Ctrl + click: adds a box to or removes it from the selection. Ctrl + A: selects everything. Esc: clears the selection and closes menus.
  • Arrows: move the selection 5 px (Shift + arrows: 20 px). Delete: raises OnKeyDownDelete or, when it is not used, deletes the selection and its relations.
  • Ctrl + C / V / D: copy, paste and duplicate. Drag a port (AllowConnect): connect. Shift + drag the background: select with a rectangle.
  • Ctrl + Z: undo. Ctrl + Y or Ctrl + Shift + Z: redo.
  • Ctrl + F: search boxes by BOX_NAME (ignoring case and accents). Ctrl + wheel: zoom (20% to 300%).
  • Right click: board, box, relation or vertex menu (when enabled).
  • Shortcuts only act with the focus inside the board; right click and browser shortcuts keep working in the rest of the page, and they do not act while typing in a field inside a box.

4. Context menus

  • Enable each menu with ShowBoardContextMenu, ShowBoxContextMenu, ShowRelationContextMenu or ShowVertexContextMenu and write its content in BoardContextMenu, BoxContextMenu, RelationContextMenu or VertexContextMenu.
  • When it opens, OnOpen…ContextMenu gives you the id of the box, of the relation (BoxIdA, BoxIdB) or of the vertex and the click position in board pixels (divide by Scale to insert a box there). Store them and use them in the options.
  • Inside the board the browser menu never opens: only the board, box, line or vertex menu appears.
  • The menu closes when an option is chosen, with Esc or when clicking outside, and is placed so that it fits in the visible part of the board.
  • MenuOpion: ready-made option with Icon, IconColor and IconBackground (optional; without them the icon is neutral), Text, KeyboardShortcut, HasSeparator, Status (false = disabled) and OnClick. Use NeedAuthorize="false" if you do not filter by roles (Roles). You can also use your own HTML with the p-board-menu-option class.
<AstrolBoard TBox="Task" TRelation="object" Board="board"
 ShowBoardContextMenu="true" ShowBoxContextMenu="true"
 OnOpenBoardContextMenu="p => { position = p; }"
 OnOpenBoxContextMenu="e => { (boxId, position) = e; }">
 <BoardContextMenu>
 <MenuOpion NeedAuthorize="false" Icon="astrol-plus" Text="New box here" OnClick="AddBox" />
 </BoardContextMenu>
 <BoxContextMenu>
 <MenuOpion NeedAuthorize="false" Icon="astrol-clone" Text="Duplicate" OnClick="Duplicate" HasSeparator="true" />
 <MenuOpion NeedAuthorize="false" Icon="astrol-trash" IconColor="var(--aui-danger)" Text="Delete" OnClick="Delete" />
 </BoxContextMenu>
 ...
</AstrolBoard>
private Position position = new();
private string? boxId;
 
// The position is in board pixels: divide by Scale to get model units
private void AddBox() => board.Boxes.Add(new Box<Task> { X = position.X / board.Scale, Y = position.Y / board.Scale, BOX_NAME = "New" });
private void Delete() => board.Boxes.RemoveAll(b => b.Id == boxId);

5. Line anchors

  • RelationAnchor (board parameter): Center (default, lines go from center to center) or Border (they start and end at the border of the boxes, in the direction of the other end or of the nearest vertex).
  • Relation.AnchorA / AnchorB (BoxAnchor, optional): a fixed point of each box, relative to it (X and Y from 0 to 1). Shortcuts: BoxAnchor.Top, Right, Bottom, Left, Center. When it is null, RelationAnchor is used.
  • LineStyle (board parameter): Straight (default, straight segments) or Curved (smooth curves that leave perpendicular to the side of the box and go through the vertices).
  • EditAnchors (board parameter): when hovering a line or a box the circles at its ends appear, drawn over the box (they take priority over the resize handles: if they match, move the end first); the user drags the end of a line to a point of the border (it snaps to corners, quarters and middles of the sides) and the point is stored in AnchorA / AnchorB; double-click on the end releases it. It goes into the history (Ctrl+Z) and raises OnChangeBoard.
  • A relation of a box with itself (BoxIdA = BoxIdB) is drawn as a loop at its bottom-right corner.
<AstrolBoard TBox="Task" TRelation="object" Board="board"
 RelationAnchor="RelationAnchor.Border"
 LineStyle="RelationLineStyle.Curved"
 EditAnchors="true"> ... </AstrolBoard>
// From the middle of the right side of A to the top quarter of the left side of B
board.Relations.Add(new Relation<object>
{
 BoxIdA = a.Id,
 BoxIdB = b.Id,
 AnchorA = BoxAnchor.Right, // (1, 0.5)
 AnchorB = new BoxAnchor(0, 0.25F), // x and y from 0 to 1, relative to the box
});

6. Editing tools

  • Arrows: RelationArrow (board parameter: None, End A → B, Start A ← B, Both) and Relation.Arrow for a single relation. An end with an arrow goes to the border of the box even in center mode.
  • Labels: Relation.Label (text) or RelationRender (RenderFragment<Relation>, any content), centered at half the length of the line or curve. Right-click on the label = relation menu.
  • Connect by dragging: AllowConnect. Hovering a box shows four ports outside it; drag one to another box (it is highlighted) and drop. OnConnect (BoardConnectEventArgs) receives the relation before it is added to complete it (Content, Label, Arrow…) or cancel it (Cancel = true).
  • Rectangle selection: BackgroundDrag = Pan (default, dragging the background scrolls and with Shift selects) or Select (the other way round). Ctrl adds to the selection.
  • Snapping: SnapToGrid (grid size, 0 = off) when moving and resizing, and SnapToBoxes: when moving, the box aligns with the edges and centers of the others (within 6 px) and the guide is drawn.
  • Clipboard (AllowClipboard, default true): Ctrl+C, Ctrl+V (moved 20 px each time) and Ctrl+D copy the selected boxes with the relations between them; the copies stay selected and raise OnBoxesAdded. The content is copied with CloneBox / CloneRelation or, when not given, through JSON (if that is not possible, the reference is shared).
  • Delete: if you use OnKeyDownDelete, your code decides (as before); otherwise the selection and its relations are removed and OnBoxesRemoved is raised.
  • Methods for your menus: CopySelection(), PasteAsync(), DuplicateSelectionAsync(), DeleteSelectionAsync(). Everything goes into the history (Ctrl+Z) and raises OnChangeBoard.
<AstrolBoard TBox="Task" TRelation="object" Board="board" @ref="boardRef"
 RelationAnchor="RelationAnchor.Border"
 RelationArrow="RelationArrow.End"
 AllowConnect="true" OnConnect="Connect"
 BackgroundDrag="BoardBackgroundDrag.Select"
 SnapToGrid="20" SnapToBoxes="true"
 OnBoxesAdded="Added" OnBoxesRemoved="Removed">
 <RelationRender>@context.Label</RelationRender>
 ...
</AstrolBoard>
// The relation is not on the board yet: complete it or cancel it
private void Connect(BoardConnectEventArgs<object> e)
{
 e.Relation.Label = "next";
 e.Cancel = board.Relations.Any(r => r.BoxIdA == e.Relation.BoxIdA && r.BoxIdB == e.Relation.BoxIdB);
}
 
board.Relations.Add(new Relation<object> { BoxIdA = a.Id, BoxIdB = b.Id, Label = "Yes", Arrow = RelationArrow.Both });

7. Automatic arrangement

  • ArrangeAsync(BoardLayoutOptions?) (with @ref) orders the board by layers following the direction of the relations (from BoxIdA to BoxIdB): it breaks cycles, reduces line crossings and aligns each box with its neighbors. Groups that are not connected are placed side by side and boxes without relations at the end.
  • Relations that skip layers get vertices to go around the boxes; the others are straight (their vertices are removed). With UseVertices = false all of them are removed.
  • It never adds, removes or changes relations (boxes, content, anchors) nor the size of the boxes: it only moves boxes and vertices. It goes into the history (Ctrl+Z) and raises OnChangeBoard.
  • BoardLayoutOptions: Direction (TopToBottom or LeftToRight), LayerSpacing (80), BoxSpacing (40), GroupSpacing (80), UseVertices (true), Left/Top (20, never less than MinLeft/MinTop), MaxWidth (1600, width of each row of groups; limited to MaxRight) and Animate (true).
  • BoardLayout.Arrange(board, options) does the same without the component (e.g. when you create the board or on the server).
<button @onclick="Arrange">Arrange</button>
<AstrolBoard TBox="Task" TRelation="object" Board="board" @ref="boardRef"> ... </AstrolBoard>
private AstrolBoard<Task, object>? boardRef;
 
private Task Arrange() => boardRef!.ArrangeAsync(new BoardLayoutOptions
{
 Direction = BoardLayoutDirection.LeftToRight,
 LayerSpacing = 60,
});
 
// Without the component (e.g. before showing the board):
BoardLayout.Arrange(board);

8. Guides while dragging (ShowGuides)

With ShowGuides="true", while dragging or resizing the selected box, dashed guide lines appear from its corners (outwards) and from the middle of its sides (fainter), up to the edge of the board, to align it with other boxes. They are off by default. Each box can force them with Box.ShowGuides (true / false; null follows the board). The look is changed with CSS variables:

<AstrolBoard TBox="Task" TRelation="object" Board="board" ShowGuides="true"> ... </AstrolBoard>
// Per box: null = follow the board, true / false = force it for this box
board.Boxes.Add(new Box<Task> { BOX_NAME = "Title", ShowGuides = false });
/* Optional: line style, separation and opacity of the side lines */
:root {
 --border-line: 1px dashed var(--color-red-transparent);
 --pbse-offset: 2px;
 --pbse-side-opacity: 0.45;
}

9. Performance

  • Dragging, resizing, moving vertices, scrolling the board and the mini-map happen in the browser, one paint per frame: no calls to .NET while you drag (a single one on release). In Blazor Server nothing travels over the network during the drag.
  • When a change is committed, only the boxes and relations that changed are rendered again.
  • Nothing is initialized per box: with 500 boxes the board mounts in half the time it used to and each drag step goes from ~360 ms to ~2 ms.
  • The history only stores geometry (it does not copy your TBox content).
  • If your BoxRender is expensive, keep it simple or use your own components: it renders again when the board's parent renders again.

Supported parameters

  • Board (Board<TBox, TRelation>), the board to render (required).
  • BoxRender (RenderFragment<Box<TBox>>), content of each box; context is the box.
  • BackgroundType (BackgroundType), background: None, Grids (default), Points or Quadrants.
  • ShowGuides (bool), guides while dragging (default false).
  • ShowSearch, ShowZoom (bool), search and zoom with mini-map (default true); ZoomStatus (bool), mini-map open; EnableZoom and AllowChangeScale (bool), allow zooming with the wheel and with the buttons.
  • MinWidth, MinHeight (float), minimum box size (default 100); MinLeft, MinTop (float) (default 10), MaxRight, MaxBottom (float), area where boxes can move.
  • RelationAnchor (RelationAnchor), lines start at the center (Center, default) or at the border (Border) of the boxes. LineStyle (RelationLineStyle), straight (Straight, default) or curved (Curved) lines. EditAnchors (bool), the user fixes the ends by dragging them.
  • RelationArrow (RelationArrow), arrows; RelationRender (RenderFragment<Relation>), labels. AllowConnect (bool) and OnConnect (BoardConnectEventArgs), connect by dragging. BackgroundDrag (BoardBackgroundDrag), scroll or select. SnapToGrid (float), SnapToBoxes (bool), snapping. AllowClipboard (bool), CloneBox, CloneRelation (Func), OnBoxesAdded, OnBoxesRemoved.
  • DiscontinueLine (bool), dashed relation lines. BoxDelay (bool), smooth transition when moving with the keyboard or undoing.
  • LongMemory (int), history states (default 10; 0 turns it off).
  • ShowBoardContextMenu, ShowBoxContextMenu, ShowRelationContextMenu, ShowVertexContextMenu (bool) and BoardContextMenu, BoxContextMenu, RelationContextMenu, VertexContextMenu (RenderFragment), right-click menus (use MenuOpion or the p-board-menu-option class on the options).
  • LoadComponent (RenderFragment), loading indicator. TextPlaceholderSearch, AriaLabel (string), CssClass, CssStyle (string).
  • OnChangeBoard (EventCallback<Board>), after every user change (move, resize, anchor a line, arrows, undo).
  • OnOpenBoardContextMenu (Position), OnOpenBoxContextMenu ((string, Position)), OnOpenRelationContextMenu ((string, string, Position)), OnOpenVertexContextMenu ((string, Position)) and OnChange…ContextMenuStatus (bool).
  • OnDobleClickBox (Box<TBox>), OnKeyDownDelete, OnChangeScale (float), OnChangeZoomStatus (bool).

Public methods

With @ref (private AstrolBoard<Task, object>? boardRef;):

  • ArrangeAsync(options): orders every box by its relations (see «Automatic arrangement»).
  • CopySelection(), PasteAsync(), DuplicateSelectionAsync(), DeleteSelectionAsync(): clipboard and deleting the selection.
  • UndoAsync() / RedoAsync(): undo and redo.
  • GetHistoryInfo(): CanUndo, CanRedo, CurrentIndex, TotalStates. ClearHistory(): clears the history.
  • _idBoxesSelected: ids of the selected boxes. RemoveItemSelected(ids): removes them from the selection.
  • GetSVGWidth(): content width. EndMoveBoxPosition(): recalculates the size and raises OnChangeBoard.
Markdown for AI © 2026 AstrolUI v10.0.13