Skip to main content

Comments and Hyperlinks

Two ways to attach something to a cell beyond its value: a comment (Excel's classic yellow note, shown on hover) and a hyperlink (clickable navigation to a URL, a file, or another part of the workbook).

Comments

Creating

CreateComment() makes a new comment; GetComment() returns the existing one, creating it if there isn't one yet — so in practice GetComment() is what you want:

using XLibur.Excel;

var ws = workbook.Worksheet("Data");

ws.Cell("B2").GetComment().AddText("Reviewed on 2026-01-21");

if (ws.Cell("B2").HasComment)
{
Console.WriteLine(ws.Cell("B2").GetComment().Text);
}

ws.Cell("B2").GetComment().Delete();

Comments are rich text, so AddText appends runs you can format independently:

ws.Cell("C4").GetComment()
.AddText("Warning: ").SetBold().SetFontColor(XLColor.Red)
.AddText("this figure is provisional.").SetItalic();

Author and signature

A comment carries an author name. AddSignature() prepends a bold line with it, which is what Excel does when a user adds a note:

using var workbook = new XLWorkbook { Author = "Reporting Service" };
var ws = workbook.Worksheets.Add("Data");

var comment = ws.Cell("A1").GetComment();
comment.SetAuthor("Data Team");
comment.AddSignature();
comment.AddText("Figures are unaudited.");

The workbook's Author is the default for new comments.

Visibility

Comments are hidden by default and appear on hover. Pin one open with SetVisible():

ws.Cell("A1").GetComment().SetVisible();
ws.Cell("A1").GetComment().SetVisible(false);

// Show every comment on the sheet
foreach (var cell in ws.CellsUsed(XLCellsUsedOptions.All, c => c.HasComment))
{
cell.GetComment().SetVisible();
}

Positioning and size

A comment is a drawing, so it has the same anchor model as a picture, plus its own size:

var comment = ws.Cell("B2").GetComment();

comment.Position.SetColumn(4).SetRow(2);

comment.Style
.Size.SetWidth(30) // in column-width units, like IXLColumn.Width
.Size.SetHeight(10); // in row-height units, like IXLRow.Height

// Or let Excel size it to the text
comment.Style.Size.SetAutomaticSize();

Appearance

Style groups the same options Excel exposes in the Format Comment dialog:

var comment = ws.Cell("B2").GetComment();

comment.Style
.ColorsAndLines.SetFillColor(XLColor.LightYellow)
.ColorsAndLines.SetLineColor(XLColor.DarkGray)
.ColorsAndLines.SetLineWeight(1)
.ColorsAndLines.SetFillTransparency(0.1);

comment.Style
.Alignment.SetHorizontal(XLDrawingHorizontalAlignment.Left)
.Alignment.SetVertical(XLDrawingVerticalAlignment.Top)
.Alignment.SetDirection(XLDrawingTextDirection.Context);

comment.Style.Margins.SetAll(0.1);

comment.Style
.Protection.SetLocked(true)
.Protection.SetLockText(true);

// Alternate text for accessibility / web export
comment.Style.Web.AlternateText = "Provisional figure note";

Style groups available: Alignment, ColorsAndLines, Size, Margins, Protection, Properties, Web.

A note on comments vs threaded comments

These are Excel's legacy notes — the yellow sticky kind. Modern threaded comments (the ones with replies, introduced in Excel 2016) are a different part of the file format and are not created by XLibur. Excel displays legacy notes fine; it just labels them "Notes" rather than "Comments" in its newer UI.

ws.Cell("A1").Value = "XLibur on GitHub";
ws.Cell("A1").SetHyperlink(new XLHyperlink("https://github.com/XLibur/XLibur"));

// With a tooltip
ws.Cell("A2").Value = "Documentation";
ws.Cell("A2").SetHyperlink(new XLHyperlink("https://xlibur.github.io/XLibur/", "Open the docs"));

// From a Uri
ws.Cell("A3").SetHyperlink(new XLHyperlink(new Uri("https://example.com/report")));

Email and file links use the same constructor — the scheme decides the behaviour:

ws.Cell("A4").Value = "Email support";
ws.Cell("A4").SetHyperlink(new XLHyperlink("mailto:support@example.com?subject=Report"));

ws.Cell("A5").Value = "Source data";
ws.Cell("A5").SetHyperlink(new XLHyperlink(@"file:///C:\Reports\source.csv"));
note

Setting a hyperlink does not change the cell's text. Assign Value as well, or the cell will be clickable but blank.

Pass a cell or a range to link within the workbook:

var summary = workbook.Worksheet("Summary");

ws.Cell("B1").Value = "Back to summary";
ws.Cell("B1").SetHyperlink(new XLHyperlink(summary.Cell("A1")));

ws.Cell("B2").Value = "Jump to the detail block";
ws.Cell("B2").SetHyperlink(new XLHyperlink(ws.Range("D40:H60")));

// Or by address string
ws.Cell("B3").Value = "Q1 figures";
ws.Cell("B3").SetHyperlink(new XLHyperlink("'Q1 Sales'!A1"));

A defined name works too, and survives the target moving:

ws.Range("D40:H60").AddToNamed("DetailBlock");
ws.Cell("B4").SetHyperlink(new XLHyperlink("DetailBlock"));
var cell = ws.Cell("A1");

if (cell.HasHyperlink)
{
var link = cell.GetHyperlink();

Console.WriteLine(link.IsExternal
? $"external: {link.ExternalAddress}"
: $"internal: {link.InternalAddress}");

Console.WriteLine(link.Tooltip);
}

// Every hyperlink on the sheet. Cell is null for a hyperlink
// not attached to a worksheet, so guard before dereferencing.
foreach (var link in ws.Hyperlinks)
{
Console.WriteLine($"{link.Cell?.Address.ToString() ?? "(detached)"}: {link.Tooltip}");
}

Removing

ws.Cell("A1").SetHyperlink(null);
ws.Hyperlinks.Delete(ws.Cell("A1").Address);

Removing a hyperlink also resets the cell's font colour and underline back to the sheet default if it was using the theme hyperlink style.

Styling

XLibur does not automatically apply the blue-and-underlined look. Use the theme's hyperlink colour so it stays in step with the rest of the workbook:

ws.Cell("A1").Style
.Font.SetFontColor(XLColor.FromTheme(XLThemeColor.Hyperlink))
.Font.SetUnderline(XLFontUnderlineValues.Single);

For a link whose target is computed, the worksheet function is often simpler than a real hyperlink object — and it recalculates:

ws.Cell("C2").FormulaA1 = "=HYPERLINK(\"https://tracker.example.com/\" & A2, \"Open \" & A2)";

A worked example

An index sheet linking to every data sheet, with a comment explaining each one:

using XLibur.Excel;

using var workbook = new XLWorkbook { Author = "Reporting Service" };

var index = workbook.Worksheets.Add("Index");
index.Cell("A1").Value = "Report contents";
index.Cell("A1").Style.Font.SetBold().Font.SetFontSize(14);

var sheets = new[]
{
("Sales", "Revenue by region and quarter."),
("Costs", "Direct and indirect costs, excluding tax."),
("Margin", "Derived from Sales and Costs; do not edit directly."),
};

var row = 3;
foreach (var (name, description) in sheets)
{
var sheet = workbook.Worksheets.Add(name);
sheet.Cell("A1").Value = name;

// Link from the index to the sheet
index.Cell(row, 1).Value = name;
index.Cell(row, 1).SetHyperlink(new XLHyperlink(sheet.Cell("A1"), $"Go to {name}"));
index.Cell(row, 1).Style
.Font.SetFontColor(XLColor.FromTheme(XLThemeColor.Hyperlink))
.Font.SetUnderline(XLFontUnderlineValues.Single);

// Explain it in a comment
var comment = index.Cell(row, 1).GetComment();
comment.SetAuthor("Reporting Service");
comment.AddSignature();
comment.AddText(description);
comment.Style
.Size.SetAutomaticSize()
.ColorsAndLines.SetFillColor(XLColor.LightYellow);

// And a link back
sheet.Cell("C1").Value = "Back to index";
sheet.Cell("C1").SetHyperlink(new XLHyperlink(index.Cell("A1")));

row++;
}

index.Columns().AdjustToContents();
workbook.SaveAs("Report.xlsx");

Where to next