drawnui/drawnui.net/drawnui-blazor
Claude Code and agent skill by drawnui/drawnui.net
- community
- ext:[{"type":"hook","description":"Pre/post lifecycle hooks"}]
Checking that the SKILL.md file is still in the repository.
DrawnUI in Blazor WebAssembly apps — startup (UseDrawnUiAsync), Canvas gestures and rendering modes, BROWSER preprocessor + shared-project pattern, single-threaded WASM divergences, fullscreen/backdrop CSS, canvas blink root cause, font payload slimming (subsetting), and publishing to GitHub Pages under a repo subpath. Trigger on "drawnui blazor", "blazor canvas", "BROWSER preprocessor", "blazor wasm drawnui", "github pages drawnui", "font subsetting wasm".
Install
npx vskill@latest install drawnui/drawnui.net/drawnui-blazorSemi-extensibleMentions customization, but needs skill-specific knowledge. Read the extensible skills standard
SKILL.md
DrawnUI Blazor
DrawnUI's Blazor head (NuGet DrawnUi.Blazor.Wasm / DrawnUi.Blazor.Server): a Canvas Razor component drawing on WebGL or raster. For pure-WASM without Blazor see the drawnui-web-app skill; framework rules in drawnui, C# composition in drawnui-fluent.
Startup
(tpls/Blazor/EmptyCode/Program.cs.) UseDrawnUiAsync(settings) returns the built host without running it: builds host → Super.Services → inits SkiaFontManager + SkiaImageManager + SkiaSvg (async) → Super.Init() → app lifecycle hooks → attaches KeyboardManager when UseDesktopKeyboard. Host the tree in Razor: <Canvas Content="@RootControl" RenderingMode="@RenderingModeType.Accelerated" Gestures="@GesturesMode.Enabled" />.
Accessibility overlay and keyboard
- Each node of the accessibility snapshot is an invisible element over the canvas; interactive ones are focusable. Inside an arrow-key group (a container with
Aria.RoleList,RoleToolbar...) only the group's current item hastabindex=0, the rest-1. - Enter / Space activate, the arrow keys go to the node and then to its group (the engine moves DOM focus to the next item's element); the element's CSS outline is the focus ring, the canvas draws none.
- An element that has a role takes the pointer from the canvas: that control gets no hover.
- What an element carries:
role,aria-label, the hint astitle,aria-live; a pressed state on the attribute its role is read from (aria-checkedfor switch / checkbox / radio,aria-selectedfor option / tab,aria-pressedfor a toggle button: witharia-pressedChrome reads a switch as unchecked);aria-valuenow / min / max / valuetext / orientationfor sliders and progress bars;aria-disabledfor a control role that takes no input. Non-interactive nodes havetabindex=-1so a screen reader can be moved to them (refocus after a page closes). - A focused element is the screen reader's node and scrolls its node into view. Verify in Chrome with CDP
Accessibility.getFullAXTree(role, value, min, max, checked); itsvaluetextcomes back empty for every element, a CDP gap, not the overlay.
Shared-project pattern + BROWSER symbol
- Shared code compiles into each head from a shared-source project (
.shproj/.projitems) — never a class library (base types differ per head package). - File convention:
Foo.Shared.cs(cross-platform),Foo.Blazor.cs(Blazor partial),Foo.Maui.cs. New platform behavior goes in a platform partial, never#ifinside shared files when a partial works. - Symbols:
BROWSER(Blazor/WASM only — JS interop, WASM APIs),!BROWSER(exclude from web),ONPLATFORM(any real platform), plus MAUI'sANDROID/IOS/WINDOWS/MACCATALYST.
Gestures
Pipeline: JS gesture package → Canvas.OnTouchAction(TouchActionEventArgs) → ProcessGestures(SkiaGesturesParameters) → control tree.
GesturesMode(CanvasGesturesparameter):Disabled(default, no capture),Enabled(standard),Lock/SoftLock(panning controls).Lockappliestouch-action:none; user-select:noneon the canvas.Enabledlets AppoMobi.Blazor.Gestures (3.11.2+) settouch-actionto the axes the page can scroll (vertical always inside an iframe, 3.11.3+), so a finger pan along a page axis scrolls the page like MAUIEnabledin a ScrollView, and the wheel scrolls the page unless a control used it.- Touch mapping: Pressed→Down, Moved/Pan*→Panning, Released/Cancelled/Exited→Up, Wheel→Wheel. Multi-touch tracked per pointer id.
- Gesture callbacks arrive far slower than frames (measured 2026-08-30, prod Blazor WASM): pointer moves reach
WithGestures/Panningat roughly 16/s while the canvas renders a steady 60 fps — browser pointer coalescing plus the JS→.NET hop. Anything that follows a finger (paddle, drag handle, slider thumb) must NOT be assigned the raw event value, or it visibly steps ~16 times a second. Store the pointer value as a target and chase it in the per-frame animator:x += (target - x) * Math.Min(1, dt * 22);— reaches the target in ~3 frames, so it reads as instant and glides between events. - For gesture bugs on scaled/transformed controls prefer built-in transform-aware helpers (
HitIsInside(),HitBoxAuto,IsGestureForChild(...), rescaledargs.Event.Location) over hand-rolled transform math.
Rendering modes
| Mode | View | Use for | |
|---|---|---|---|
Default | SkiaView (raster) | simple/static UI | |
Accelerated | SkiaViewAccelerated (WebGL) | heavy animation, games, shaders |
HasDrawn detection: check paintArgs.BackendRenderTarget dimensions, NOT CanvasSize (fires late).
Single-threaded WASM divergences
| Area | MAUI | Blazor | |||
|---|---|---|---|---|---|
FrameTimeInterpolator | used for physics stability | skip — raw delta | |||
CanUseCacheDoubleBuffering | enabled | forced false (`#if WEB \ | \ | BROWSER) — no background thread exists, so ImageDoubleBuffered resolves to Image` | |
SkiaCachedStack.AsyncPlaneAllowed | async plane bake | forced false — blocking-wait on the only thread deadlocks the bake it waits for; sync SKPicture record instead. Rule: never blocking-wait on WASM main thread for work only that thread can run | |||
CanvasSize | after layout | unreliable until ResizeObserver fires; use paintArgs dimensions | |||
| Keyboard | native events | JS global listeners | |||
| PDF/file system | supported | excluded / bundle-only | |||
GetDisplayRefreshRate() | device rate | constant 60 | |||
| Offscreen cache bakes | dedicated worker threads | drained INLINE (DrainOffscreenQueueInline) — queued items would otherwise starve indefinitely under continuous frame load (observed: two lotties, first-queued control never pumped = permanent empty box). Same total main-thread cost, guaranteed execution |
Super.OnFrame ticks from a Task.Delay loop off Super.MaxFps (60 when unset), not rAF; the canvas paint itself goes through SkiaSharp's SKHtmlCanvas.requestAnimationFrame.
SkiaImageManager cache keys (Blazor) — slash-agnostic (2026-08-20)
Symptom: a control's SkiaImageManager.Instance.GetFromCache("/media/x.png") silently returns null after PreloadImages(["media/x.png"]) → sprite/overlay just missing, zero console errors. Cause: lib commit 96caf1e8 changed NormalizeFilePath from always-forcing a leading / on cache keys to storing relative keys — old-contract callers prepending / miss forever. Fixed in lib: GetCacheKey normalizes symmetrically in Add/Get/Update/RemoveFromCache, so /media/x.png == media/x.png. WASM trap inside that fix: on the browser runtime (unix path rules) Uri.TryCreate("/media/x.png", Absolute) SUCCEEDS as file:///media/x.png (fails on Windows — desktop shell tests mislead), so the root slash must be trimmed BEFORE NormalizeFilePath, not after. Debugging rule: an image cache miss draws nothing silently — check key both with and without leading slash before theorizing about render code.
Fullscreen canvas backgrounds
Real browser fullscreen targets the canvas host element (.xaml-canvas), not the page root — and browsers apply :not(:root):fullscreen::backdrop { background: black; }. If a scene expects an image/gradient behind a centered surface in fullscreen, style the actual fullscreen host AND its backdrop in GLOBAL CSS (scoped .razor.css can miss child pseudo-elements):
CSS under wwwroot/css/ resolves urls relative to that folder (../Images/...).
WebGL context loss (GPU reset, driver update, too many canvases)
Handled since 1.10.6.22 on Blazor and pure Wasm: the canvas host prevents the default on webglcontextlost (else the browser never gives the context back) and draws no frames while lost; on webglcontextrestored the same WebGL object gets a new Emscripten GL handle, the lost Skia context is abandoned without GL calls and the next frame draws everything on a new one (GPU caches fail the context check and are made again). The console shows DrawnUI: WebGL context lost / restored (pure Wasm: DrawnUI.Web: WebGL context lost / restored). Test: canvas.getContext('webgl2').getExtension('WEBGL_lose_context'), loseContext(), then restoreContext(). Older builds stayed blank until a reload.
Canvas blink on first tap (auto-height Canvas) — FIXED 2026-09-07
Symptom: <Canvas> with no HeightRequest, blank flash on the FIRST tap only, never after; other pages (fixed HeightRequest) fine. NOT context loss, NOT element recreation — the same <canvas> is RESIZED once (host 790.4 -> 755.2px, buffer 987 -> 943), and any width/height write clears the drawing buffer.
Root cause: Canvas.razor GetIntrinsicAutoCanvasSize() passed UNIT constraints to Content.Measure(), which takes PIXELS. Probe: constraints=400x∞ scale=1,25 -> 320x790,4 — content measured at 400px = 320 units, 20% too narrow, so a label wrapped an extra line and the host was reported too tall; the first text change that dirtied the parent re-measured and the canvas shrank. Fix: UnitsToPixels(constraint, scale) before Measure. Rule: DrawnUi Measure(w,h,scale) constraints are PIXELS; MeasuredSize.Units are units — never mix.
Two related defects fixed alongside (neither was the trigger):
SkiaView/SkiaViewAccelerateddid not declareWidth/Height, soSKGLView/SKCanvasViewCaptureUnmatchedValuessplatted the floats onto the html canvas as culture-formatted attributes (height="755,2"under arubrowser locale) — invalid, browser resets the buffer to 300x150. Declare params so they are consumed.- After a canvas element size change nothing requested a repaint; under
UpdateModeType.Dynamicthe cleared buffer could stay blank until the next input.Canvas.OnAfterRenderAsyncnow callsUpdate()on size change.
Diagnostic recipe: MutationObserver on .xaml-canvas-element (style) + its canvas (width/height/style) + webglcontextlost — if the size changes, it is auto-size, not GPU.
Canvas blink (random one-frame blank while idle)
Root cause: heavy CSS filter: blur() / backdrop-filter: blur() elements elsewhere on the page. Each blur is its own GPU layer; large animated ones compete with the WebGL canvas layer and the browser randomly drops a canvas frame. Page-level — the same canvas doesn't blink in a plainer app.
- NOT: canvas removal/hide (MutationObserver catches nothing), resize, Blazor re-render,
preserveDrawingBuffer(constant repaint doesn't fix it),contain(makes it worse). - Diagnose by page-level elimination:
display:nonethe blurred elements /backdrop-filter:nonethe header — blink stops → narrow to the biggest-radius animated blur. - Fix (also a perf win): replace blur-glow decoration with
radial-gradient(circle, color 0%, transparent ~68%)backgrounds + plaintransform: translate()drift. Reserve real blur for small static elements. - Often invisible to headless automation at DPR 1 — A/B on a real scaled display.
- Diagnostics warning: never pipe
Consoleinto an on-screen panel while debugging canvas rendering — the render feedback loop fakes a per-frame-render symptom.
Full-height layout CSS (required)
Without this the canvas won't fill the viewport:
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 90 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingdocs/skills/drawnui-blazor/SKILL.md
Popularity
- Repository stars
- 409
- Repository forks
- 39
- 7-day trend score
- 0
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Extension points
hookPre/post lifecycle hooks
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/drawnui/drawnui.net/drawnui-blazor)