Getting started
Bit.Butil ships as a single NuGet package plus one script tag. Three steps and every browser API on this site is available as an injectable C# service.
Bit.Butil targets .NET 8, 9 and 10, works in Blazor WebAssembly, Blazor Server and Blazor Hybrid (MAUI, WPF, WinForms), and is annotated for trimming.
dotnet add package Bit.ButilAdd the Butil bridge script to your host page, before the Blazor script - the app boots as soon as that second script runs, so window.BitButil has to exist by then. Which host page it is depends on how you host: index.html for standalone WebAssembly and for Hybrid, Components/App.razor for a Blazor Web App, _Host.cshtml for a classic Blazor Server app. Lazy scripts, below, replace this step entirely.
<script src="_content/Bit.Butil/bit-butil.js"></script>
<script src="_framework/blazor.web.js"></script>AddBitButilServices registers every Butil class as a scoped service, matching Blazor's one-circuit / one-app-instance-per-user model. If your app prerenders, register them in the host's container as well: every component is instantiated once on the server for the prerender pass and again in the browser, so anything a page injects has to resolve in both places. Registration is trimming-aware - it discovers the services by reflection, so a published, trimmed app only registers the Butil classes your code actually injects and the rest are removed from your bundle.
using Bit.Butil;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddBitButilServices();
await builder.Build().RunAsync();Inject the class you need and call it - no IJSRuntime, no JSON juggling, no hand-written interop. Full IntelliSense included. The one habit worth forming from the start: touch the browser from OnAfterRenderAsync or an event handler, never from OnInitialized, so the code also works under prerendering.
Bit.Butil.Clipboard clipboard
Bit.Butil.Crypto crypto
Bit.Butil.Keyboard keyboard
{
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender is false) return;
await clipboard.WriteText("Hello from C#!");
await keyboard.Add(ButilKeyCodes.F10, args => { /* ... */ }, ButilModifiers.Ctrl);
}
}A round trip through the bridge and back. If this reads and writes without throwing, the script is loaded, the services are registered and the runtime is live - which rules out every setup problem at once. The buttons below run exactly this code on this page.
await localStorage.SetItem("butil-check", DateTime.Now.ToString("HH:mm:ss"));
var value = await localStorage.GetItem("butil-check");
var secure = await window.IsSecureContext();The bundle behind step 2 covers every API on this site, and a published app can shed the parts it never calls. One property turns it on, and in a Blazor WebAssembly project it is already on: a trimmed publish rebuilds bit-butil.js from only the modules the trimmed Bit.Butil.dll still calls, so an app injecting Clipboard, LocalStorage and Window ships around 23 KB of JavaScript instead of 315 KB. Publishing without trimming - a WebAssembly app with PublishTrimmed off, Blazor Server, a server host that prerenders - has no trimmed assembly to read, and BitButilScriptScan answers the same question from the app's own assemblies instead. It defaults to TypeReferences wherever the switch is on, so there too the switch is the whole of what you write; None turns it off again and TypeNames is a coarser name-matching mode. BitButilScriptModule adds modules or Bit.Butil class names on top of whatever either concluded, for an API reached by reflection or from your own JavaScript. It is publish-only, so a build - dotnet run and dotnet watch included - always keeps the full bundle, and it takes effect only in the project that publishes the app's static web assets.
<!-- In a Blazor WebAssembly project there is nothing to add. Anywhere else: -->
<PropertyGroup>
<BitButilTrimScripts>true</BitButilTrimScripts>
<!-- Implied by the switch above; write it out to pick a different value.
TypeNames is the coarser mode, None publishes the full bundle. -->
<BitButilScriptScan>TypeReferences</BitButilScriptScan>
</PropertyGroup>
<!-- Always kept, whatever the above concluded: -->
<ItemGroup>
<BitButilScriptModule Include="Clipboard;geolocation" />
</ItemGroup>
<!-- ...and to opt out entirely, wherever it is on: -->
<PropertyGroup>
<BitButilTrimScripts>false</BitButilTrimScripts>
</PropertyGroup>Directory.Build.props, the two overrides for a layout
where the defaults look in the wrong place, the build warnings it can produce - and a live check that
reads back which modules the app you are looking at actually downloaded.
The other way to ship only the JavaScript you use, and the one that works in every hosting model, trimmed or not: no script tag at all. The first call into an API imports that API's own module - _content/Bit.Butil/modules/clipboard.js for Clipboard - so the browser downloads the JavaScript for the APIs the app actually calls and nothing else. Each module file is self-contained and safe to load more than once. Set the property in every project that uses Butil, a Blazor Web App's server and client both, and drop the script tag from step 2. The cost is one extra request the first time each API is used.
<!-- No <script> tag needed anywhere -->
<PropertyGroup>
<BitButilLazyScripts>true</BitButilLazyScripts>
</PropertyGroup>The registration call takes an options callback carrying the library's runtime toggles: LazyScripts, ScriptModulesPath for when the package's static web assets are served from somewhere else such as a CDN, and FastInvoke. It is the escape hatch for hosts where the csproj is not yours to edit, and it decides only how the scripts load, not what a publish contains. A default publish keeps the bundle and drops the per-module files, so this switch on its own leaves the imports with nothing to fetch - keep the modules with BitButilIncludeScriptModules. Publish-time tree-shaking has no C# counterpart at all, since it happens inside dotnet publish.
builder.Services.AddBitButilServices(options =>
{
options.LazyScripts = true; // or false to insist on the bundle
options.ScriptModulesPath = "/cdn/butil/"; // optional, when served elsewhere
});
// ...and the csproj still has to publish the module files:
// <BitButilIncludeScriptModules>true</BitButilIncludeScriptModules>On Blazor WebAssembly, APIs backed by synchronous JavaScript (LocalStorage, SessionStorage, Cookie, Console, Location, History) can skip the async interop machinery entirely. Call it once at startup; asynchronous browser APIs are unaffected, so it cannot break them, and on Blazor Server it is a no-op.
BitButil.UseFastInvoke();
// switch back at any time:
BitButil.UseNormalInvoke();