BlazorBasics.SignaturePad
1.1.1
dotnet add package BlazorBasics.SignaturePad --version 1.1.1
NuGet\Install-Package BlazorBasics.SignaturePad -Version 1.1.1
<PackageReference Include="BlazorBasics.SignaturePad" Version="1.1.1" />
<PackageVersion Include="BlazorBasics.SignaturePad" Version="1.1.1" />
<PackageReference Include="BlazorBasics.SignaturePad" />
paket add BlazorBasics.SignaturePad --version 1.1.1
#r "nuget: BlazorBasics.SignaturePad, 1.1.1"
#:package BlazorBasics.SignaturePad@1.1.1
#addin nuget:?package=BlazorBasics.SignaturePad&version=1.1.1
#tool nuget:?package=BlazorBasics.SignaturePad&version=1.1.1
BlazorBasics.SignaturePad
A pad the user signs on with a finger, a stylus or a mouse, and a signature that comes back as a PNG or as an SVG.
There is no JavaScript in this package. Not a module, not an interop call, not a <script>
tag. Capture is pointer events, drawing is SVG, and the PNG is written in C#: a rasterizer and
a PNG encoder, both in this library. So there is nothing to load, nothing to dispose, nothing
that breaks under prerendering, and nothing that needs a wwwroot file to be served.
It has no opinion about what you do with the signature. Storing it, uploading it, showing it on a summary page and deciding whether it is legally binding are all yours; every application concern is a parameter or a callback.
Only dependency: Microsoft.AspNetCore.Components.Web.
Install
dotnet add package BlazorBasics.SignaturePad
// Program.cs — optional. A pad works with nothing registered.
builder.Services.AddBlazorSignaturePad();
The bare minimum
@page "/sign"
<SignaturePad @bind-Value="Signature" OnSigned="Store" />
@if (Png.Length > 0)
{
<img src="@Preview" alt="Signature" />
}
@code {
SignatureData Signature = new();
byte[] Png = [];
string Preview = string.Empty;
void Store(SignatureResult result)
{
Png = result.Png;
Preview = result.PngDataUrl;
}
}
You get a bordered box with a dashed baseline, an invitation to sign, an undo button and a clear button, and a PNG cropped to the ink after every stroke. You do not get a modal, a form field, a validation message or a place to put the file: those are the application's.
What comes out
OnSigned hands over a SignatureResult after every stroke, and only when something is
listening — producing an image is real work and nobody should pay for it unasked.
result.Png // byte[], a real PNG file
result.Svg // string, a standalone SVG document
result.PngDataUrl // ready for the src of an img
result.Width // pixels
result.SignedAt
The same is available on demand through a @ref (ToPng(), ToSvg(), ToPngDataUrl(),
GetResult()) and straight off the data (Signature.ToPng(), Signature.Render()), for the
code that has the strokes but not the component.
The size is never asked for. It comes from the ink: the bounding box, plus a margin, times a scale. That is the only measurement available without JavaScript, and it also gives an image that is reusable whatever the pad happened to be rendered at.
<SignaturePad ExportOptions="@(new SignatureExportOptions {
Scale = 3, // pixels per pad unit
Padding = 12, // blank margin around the ink
Background = "#ffffff", // empty means transparent
MaximumSize = 2000 })" />
Ink that breathes
By default every point is drawn at the same thickness, which looks the same on every device.
Set WidthMode and the line thins as the hand moves faster, the way a pen behaves:
<SignaturePad WidthMode="SignatureWidthMode.Velocity" MinimumWidth="1" MaximumWidth="5" />
SignatureWidthMode.Pressure follows the pointer instead, which is only meaningful with a
stylus: everything else reports a flat 0.5 and the result is the constant one.
Binding, undo and reloading
Value is a SignatureData: a list of strokes, a list of points, plain and serializable.
Store it and hand it back later and the pad carries on where it was left, undo stack and all.
<SignaturePad @ref="Pad" @bind-Value="Signature" ShowRedo="true" />
<button @onclick="Pad.Clear">Start over</button>
<button @onclick="Pad.Undo" disabled="@(!Pad.CanUndo)">Back one stroke</button>
To show a signature without letting anybody draw on it, there is a second component that costs no rasterizing at all — it draws the same strokes as SVG:
<SignatureView Value="Signature" Height="140">
<EmptyContent><em>Not signed yet.</em></EmptyContent>
</SignatureView>
Customization
| Slot | Replaces |
|---|---|
ToolbarContent |
the whole undo/redo/clear row |
PromptContent |
the invitation shown over the empty pad |
ChildContent |
free content laid over the drawing area, such as a printed name |
EmptyContent |
what SignatureView shows with no ink (that component only) |
Texts are a record, so a localized application builds one from its own resources. The library depends on no localization stack and looks nothing up:
new SignatureTexts { Prompt = "Firme aquí", Clear = "Borrar", Undo = "Deshacer" }
Theming is CSS custom properties on the root element, so the host never needs ::deep:
.my-pad {
--bbsig-height: 220px;
--bbsig-bg: #ffffff;
--bbsig-ink: #1d4ed8;
--bbsig-border: #d1d5db;
--bbsig-border-active: #6b7280;
--bbsig-radius: 10px;
--bbsig-baseline: #d1d5db;
--bbsig-prompt: #9ca3af;
--bbsig-action: #374151;
--bbsig-action-danger: #b91c1c;
}
<SignaturePad Class="my-pad" />
--bbsig-height is only written inline when you pass Height. Left alone it stays a custom
property, which is what lets you make the pad shorter on a phone from a media query.
The line to sign on
Where the baseline sits is four more custom properties, and they behave the same way — set them in your own stylesheet, from a media query, or wherever else CSS reaches:
.my-pad {
--bbsig-baseline-x: 6%; /* where it starts, from the left edge */
--bbsig-baseline-y: 76%; /* how far down it sits */
--bbsig-baseline-width: 88%; /* how long it is */
--bbsig-baseline-thickness: 1px;
--bbsig-baseline-style: dashed; /* any border style: solid, dotted... */
--bbsig-baseline: #d1d5db; /* the color */
}
The same six are parameters, taking any CSS length, for when a single pad differs and a class is more than it is worth. Only what you pass is written inline, so the rest stays overridable:
<SignaturePad BaselineY="60%" BaselineX="0" BaselineWidth="100%" BaselineStyle="solid" />
ShowBaseline="false" leaves it out altogether.
The baseline is a positioned box and not an SVG <line> for one reason: a line is placed by its
x1, y1, x2 and y2 attributes, those are not CSS properties in any browser, and the
component's own stylesheet is scoped — so a rule of yours with the same selector loses on
specificity anyway. Custom properties are what cascades into a scoped stylesheet, so everything
worth moving is one.
Notes from the field
- Running off the edge does not chop the stroke in two. A signature is usually bigger than the
box it is asked for. Leaving with the button still held keeps the stroke open, and coming back
carries on writing the same line; the part drawn outside was never seen by anybody, so the
return is joined to the exit with a straight line. Releasing the button out there ends it, which
is noticed on the first move that comes back with no button held — there is no
pointerupto hear out of bounds withoutsetPointerCapture, and that is JavaScript. SetContinueOnReentry="false"for the older behaviour, where the edge ends the stroke. - What is drawn outside is exported even though it was never visible. With a touch pointer the browser keeps delivering moves off the element, the pad clips them, and the export crops to the ink — including the part beyond the border. Nothing here can measure the element without JavaScript, so nothing here can clamp it.
- Everything inside the pad is transparent to the pointer.
offsetXis measured from whatever the event landed on, so if the paths could receive events the coordinates would jump the moment you drew over an earlier stroke. This is why the SVG haspointer-events: none. touch-action: noneis not optional. Without it a finger scrolls the page instead of signing. It is in the component's own stylesheet; do not override it.- Pass
InkColoras an expression, not as a bare identifier.InkColor=MY_COLORin Razor hands over the literal text"MY_COLOR", because unquoted values on astringparameter are taken as literals — only non-string parameters are parsed as C#. An unreadable color makes the browser fall back tostroke: none, so the paths are in the DOM, perfectly formed, and nothing paints. Meanwhile the export still works, in black, because the rasterizer parses the color itself and falls back. WriteInkColor="@MY_COLOR"orInkColor="#2e3092". - The strokes are not the image. Storing
SignatureDatalets somebody carry on drawing; storing the PNG does not. Most applications want the PNG, and then the pad is a pad again on the next visit — which is usually what you want anyway. - Rasterize on a pause, not on every stroke. A signature is three or four strokes in quick succession. Waiting ~400ms after the last one turns four images into one, which matters in WebAssembly.
Advanced
ISignatureRenderer is the whole export surface. Register your own and the pad uses it, which
is the way in if you would rather render on the server, hand the work to a native library or
watermark what comes out:
builder.Services.AddSingleton<ISignatureRenderer, MyRenderer>();
builder.Services.AddBlazorSignaturePad();
License
Free — see LICENSE.md.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.Components.Web (>= 10.0.10)
-
net8.0
- Microsoft.AspNetCore.Components.Web (>= 8.0.27)
-
net9.0
- Microsoft.AspNetCore.Components.Web (>= 9.0.17)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version 1.1.1: The baseline is placed from CSS. It is no longer an SVG line, whose x1, y1, x2
and y2 attributes are not CSS properties and could therefore never be moved from a stylesheet,
but a box driven by the custom properties --bbsig-baseline-x, --bbsig-baseline-y,
--bbsig-baseline-width, --bbsig-baseline-thickness and --bbsig-baseline-style, with matching
BaselineX, BaselineY, BaselineWidth, BaselineThickness, BaselineStyle and BaselineColor
parameters. Defaults are unchanged; the dash pattern is now the browser's dashed border
instead of stroke-dasharray 6 6.
Version 1.0.0: First release. SignaturePad and SignatureView components, pointer capture with
no JavaScript, quadratic smoothing, constant/velocity/pressure stroke width, undo and redo,
SVG and PNG export written in C#, customizable icons/texts/slots and CSS custom properties
for theming.