loading

1. Install the package

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.

Shell
dotnet add package Bit.Butil

2. Add the script

Add 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.

HTML
<script src="_content/Bit.Butil/bit-butil.js"></script>
<script src="_framework/blazor.web.js"></script>

3. Register the services

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.

C#
using Bit.Butil;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.Services.AddBitButilServices();

await builder.Build().RunAsync();

Use any API

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.

Razor
@inject Bit.Butil.Clipboard clipboard
@inject Bit.Butil.Crypto crypto
@inject Bit.Butil.Keyboard keyboard

@code {
    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);
    }
}

Check that it worked

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.

C#
await localStorage.SetItem("butil-check", DateTime.Now.ToString("HH:mm:ss"));

var value = await localStorage.GetItem("butil-check");
var secure = await window.IsSecureContext();
Live sample
setup check output
Results will appear here when you interact with the samples.

Optional: tree-shake the JavaScript

BitButilTrimScripts / BitButilScriptScan / BitButilScriptModule

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.

XML
<!-- 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>
Note:
The full story is one page awayJavaScript trimming covers the rest of it: the three signals the trimming works from and which one suits which hosting model, why these properties belong in the project you publish rather than in a shared 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.

Optional: lazy scripts

BitButil.UseLazyScripts

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.

XML
<!-- No <script> tag needed anywhere -->
<PropertyGroup>
  <BitButilLazyScripts>true</BitButilLazyScripts>
</PropertyGroup>

Optional: the same switches from C#

AddBitButilServices

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.

C#
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>

Optional: fast invoke

BitButil.UseFastInvoke

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.

C#
BitButil.UseFastInvoke();

// switch back at any time:
BitButil.UseNormalInvoke();
Warning:
Browser support varies by API Butil exposes the browser's own capabilities, so each feature is only as available as the underlying web API. Every page links to its MDN reference, and the browser-support page lists which APIs need HTTPS, a permission prompt or a specific engine - and will feature-detect all of them live in whatever browser you are reading this in.
Note:
Where to next?Render modes covers how Butil behaves under WebAssembly, Server, Hybrid and prerendering - worth ten minutes before you write much, and JavaScript trimming is worth another ten before you publish. Otherwise, start with the most used APIs: LocalStorage, Clipboard, Keyboard, Crypto and WebAuthn.
An unhandled error has occurred. Reload 🗙