A UI rendering engine on top of CanvasKit: layouts, controls, gestures, effects and animations
helloreact.drawnui.net · github.com/DrawnUi/DrawnUi.React · MIT · Publish 90
100 000 recycled cells · last tapped: -
200 uneven cells · MeasureVisible · LoadMore at both ends
Post 1
Renders idle ui product idle pixel drawn ui product recycled time canvas idle itself itself catalog renders renders feed in catalog pixel.
#1 · 138 chars
Post 2
Catalog in the ui every catalog cells cells in product idle cells pixel canvas the drawn in pixel every itself rows renders product pixel.
#2 · 138 chars
Post 3
Timeline and social itself in rows in idle cells refines in estimates idle measure news ui itself skia visible uneven and drawn product the uneven.
#3 · 147 chars
Post 4
News rest recycled recycled renders and product product drawn canvas refines and pixel the renders renders rows cells idle canvas recycled product timeline rows canvas uneven.
#4 · 175 chars
Post 5
Uneven the uneven visible refines cells skia canvas uneven timeline and idle in and measure every recycled visible product news renders social timeline renders rows product in.
#5 · 176 chars
Post 6
In visible every and ui pixel and and visible and rest rows every idle time pixel social the itself measure social news timeline measure recycled pixel itself in measure.
#6 · 170 chars
Post 7
Rest measure and idle and product social feed every itself the social in rows catalog pixel estimates and the timeline in uneven timeline time news measure rows idle visible itself.
#7 · 181 chars
Post 8
Estimates cells product rows drawn uneven pixel pixel news social estimates product renders feed canvas itself catalog in uneven estimates visible idle social catalog cells refines cells uneven estimates visible catalog.
#8 · 220 chars
SkiaImage · Aspect
Same 512×512 photo in a 220×120 box. Overflow is clipped to the box.
AspectCover
AspectFit
AspectFill
Aspect="AspectCover"
Aspect="AspectFit"
Aspect="AspectFill"
AspectFitFill
Fill
Fit
Aspect="AspectFitFill"
Aspect="Fill"
Aspect="Fit"
FitFill
Cover
None
Aspect="FitFill"
Aspect="Cover"
Aspect="None"
Effects · AddEffect, Blur, Zoom, offsets
BlackAndWhite
Sepia
Pastel
InvertColors
Tint #0D6EFD Multiply
Darken={80}
Lighten={80}
Contrast={1.5}
Saturation={2}
Blur={3}
ZoomX/Y={1.8}
HorizontalOffset={-40}
HSL Gamma=0.6 (hue) Sat=1 Bright=0.5
Custom filters · AddEffect="Custom" + PaintColorFilter / PaintImageFilter
PaintColorFilter = MakeMatrix (R↔B)
PaintImageFilter = MakeDilate(4)
SkiaImageTiles · TileWidth/TileHeight, TileOffsetX animates
SkiaImageManager preload queue · idle
SkiaSvg
Source="images/drawnui.svg" WidthRequest={200} LockRatio={1}
TintColor
SvgString (inline markup) at three sizes
Rasterized by the browser at the displayed pixel size, re-rasterized only when that size changes.
SkiaShape
Stroke is drawn inside the bounds; children are clipped to the shape.
ContextMenu
Right-click / long-press the shape: its ContextMenu handler takes the request and the browser menu stays away. Elsewhere on the canvas the browser menu shows as usual.
right-click me
ContextMenu -> toast
FillGradient / StrokeGradient
Linear · Angle={45}
Circular at (0.5, 0.5)
Oval · Light={1.4}
Sweep · TileMode Repeat
Gradient text, line by line
bg + text
StrokeGradient · StrokeWidth 8
SkiaLabel FillGradient → glyphs (GradientByLines)
Label: BackgroundColor + FillGradient = both
Bevel / Emboss
BevelType=Bevel · Depth 4
BevelType=Emboss · Depth 4
Circle · Bevel, colored edges
Polygon (star) · Emboss
Path (heart) · Bevel
Sharp rectangle · Bevel, Opacity 1
Shadows
Shadows=[{Y:4, Blur:6, Opacity:.5}]
Colored, offset X
Two shadows (glow + drop)
ShadowOnly + hollow ClipBackgroundColor
Types
Rectangle CornerRadius={16}
CornerRadius(24, 0, 0, 24)
Circle + stroke
Ellipse
SkiaLabel
Word wrap · HorizontalOptions=Fill
DrawnUI draws every pixel itself: text is shaped and rasterized by Skia, so a label wraps by words, respects MaxLines with an ellipsis, aligns horizontally and vertically, and never leaves the canvas for a native view. This paragraph is long on purpose so it wraps across several lines at whatever width the layout gives it.
Spans — <TextSpan> children: color, size, bold, italic, underline, strikeout, background, Tapped
One label, many styles: bold, italic, colored, bigger, underlined, struck out, highlighted and a tappable link → that wraps with the rest of the paragraph like any other word.
Last span tap: nothing yet
Spans with colored emoji — the DrawnUI .NET demo; emoji sequences are shaped into one glyph
ANY span can be made tappable ! Use colored emojis 🌐🚒🙎🏽👻🤖 Format text as BiG and small! Add another tappable link! Still in development but.. You can already use the nuget!
SkiaRichLabel with an emoji run — the DrawnUI .NET demo
Just put a simple markdown string and this control will auto-split it into spans. It will recognize links a link rendered with SkiaMarkdownLabel Here it auto-created a span for 🚀🐴🤖👻 and switched back. More info... Subclass this control or consume LinkTappedCommand property or LinkTapped event handler to react to taps.
Right to left — Arabic, shaped and in bidirectional order · starts at the right (HorizontalTextAlignment Start)
مرحبًا بكم في عصر العناصر البصرية المصممة حيث الدقة تلتقي بالإبداع في كل تفصيل نقدم لكم تجربة مستخدم فريدة ومتطورة استمتع بالسلاسة والأداء العالي في التصميمات نحن نبتكر لنجعل تجربتكم أكثر تميزًا وسهولة
SkiaRichLabel — markdown in Text, rendered as spans
Heading 1 Heading 2 Heading 3 A paragraph with bold, italic, strikethrough, inline code and a tappable link. Soft line breaks stay inside the paragraph. • Bullet item with bold • Second bullet 1. Numbered item 2. Another one, emphasised const label = new SkiaRichLabel(); label.Text = "# Hello";
Last link tapped: none
MaxLines={2} · TailTruncation (default)
DrawnUI draws every pixel itself: text is shaped and rasterized by Skia, so a label wraps by words, respects MaxLines with an ellipsis, aligns horizontally and vertically, and never leaves the canvas for a native view. This paragraph is long on purpose so it wraps across several lines at whatever width the layout gives it.
LineSpacing={1.6}
DrawnUI draws every pixel itself: text is shaped and rasterized by Skia, so a label wraps by words, respects MaxLines with an ellipsis, aligns horizontally and vertically, and never leaves the canvas for a native view. This paragraph is long on purpose so it wraps across several lines at whatever width the layout gives it.
HorizontalTextAlignment Start / Center / End
Start aligned text wraps inside its own column
Center aligned text wraps inside its own column
End aligned text wraps inside its own column
VerticalTextAlignment in a 90pt box
Start
Center
SkiaLayout types
One class, six Type values; the aliases preset Type (+ HorizontalOptions=Fill for SkiaStack / SkiaLayer / SkiaWrap / SkiaGrid / SkiaMasonry). Children position themselves with HorizontalOptions / VerticalOptions / Margin, the container adds Spacing and Padding.
Absolute · SkiaLayer
Children overlap in one cell; alignment + Margin place them (WPF-style, no X/Y)
Start/Start
Center/Start
End/Start
Start/Center
Center/Center
End/Center
Start/End
Margin(0,0,0,12)
End/End
Icon + text with an Absolute layer instead of a grid (cheaper): label gets the icon's width as Margin
Margin={new Thickness(48, 0, 0, 0)} VerticalOptions=Center — no second column to measure.
IsClippedToBounds — a child larger than its parent
overflows (default)
IsClippedToBounds
ClipEffects={false}
Column · SkiaStack
ZIndex draws later (on top); HorizontalFillRatio/VerticalFillRatio = fraction of the box; Left/Top nudge the drawn output
ZIndex=2 · FillRatio 0.5 × 0.75
ZIndex=1, declared second
Left=-20 Top=-10 · ZIndex=3
Vertical stack, Spacing between children, each child aligns horizontally on its own
Start (default)
HorizontalOptions=Center
HorizontalOptions=End
HorizontalOptions=Fill
WidthRequest={200}
Row · SkiaRow
Horizontal stack; the row is as tall as its tallest child, children align vertically
Start
Center
Fill
Margin(16,0,0,0)
End
A Row gives children an infinite width: Fill on the main axis auto-sizes (MAUI stack semantics). Use SkiaGrid with a * column when something must take the remaining width.
Grid · SkiaGrid
ColumnDefinitions="*, 2*, Auto" RowDefinitions="Auto, 60" · ColumnSpacing/RowSpacing 8
*
2*
Auto (this label)
Row 1 = 60pt
Column 1
Auto
ColumnSpan / RowSpan
ColumnSpan=2
RowSpan=2
0,1
1,1
Common Controls
SkiaSwitch, SkiaCheckbox, SkiaRadioButton, SkiaButton, SkiaProgress, SkiaSlider — the same tree per card, only ControlStyle changes (the Fiddle 'Looks' snippet).
Last: interact with any control
Live — Unset
Default
Windows — Fluent
Cupertino — iOS
Material — Android
Material3 — Android
Carousel & Drawer
SkiaCarousel playground (Sandbox MainPageCarousels)
1
2
3
4
Selected Index: 0 · InTransition: false · Looping disabled - bounded scroll · ItemAppearing 0
Drag me
Swipe Speed
Current: 1.0x
Drawer content
Everything inside is a normal drawn tree: buttons keep working, the drawer only takes vertical drags. Release with a flick to snap open or closed.
SidesOffset={40} Spacing={12} — neighbours peek in · SelectedIndex=1
Slide 1
Slide 2
Slide 3
Slide 4
IsLooped + ItemsSource/ItemTemplate (12 recycled cells) · SelectedIndex=0
Item 12
Item 1
Item 2
recycled cell · index 11
recycled cell · index 0
recycled cell · index 1
Wraps last → first both ways (virtual anchors); LinearSpeedMs=350 = one slide per 350 ms without Bounces; cells are recycled through ItemTemplate.
DynamicSize — auto height follows the selected slide · SelectedIndex=0
80 pt
160 pt
recycled cell · index 0
recycled cell · index 1
No HeightRequest: the carousel measures the selected cell (80 / 160 / 110 pt) and re-measures on every index change.
SkiaDrawer — drag the header below, or:
IsOpen: false
Direction=FromBottom HeaderSize=56, sits in a SkiaLayer with VerticalOptions=End; snaps by velocity, Bounces enabled.
Lottie & GIF
SkiaLottie — Source="lottie/shield.json" Repeat={-1} · Started
SpeedRatio
Skottie renders the vector animation every frame into an Operations cache (a picture); the animator is the C# RangeAnimator over InPoint..OutPoint.
ColorTint / Colors — colors replaced in the JSON before parsing (C# ApplyTint)
IsOn toggle — AutoPlay={false}, DefaultFrame=0 / DefaultFrameWhenOn=-1 · IsOn=false
Stopped animations show DefaultFrame, or DefaultFrameWhenOn (-1 = last frame) when IsOn: the C# recipe for animated checkboxes.
SkiaGif — Source="images/banana.gif" Aspect=AspectFitFill · 8 frames, 800 ms, playing
Every frame is decoded once (CanvasKit AnimatedImage); the animator runs over 0..DurationMs and picks the frame by time, like C# GifAnimation.
SkiaShell
Route=shell · NavigationStack=[shell] · Popups=0 · Modals=0 · Toasts=0
Pages live in the URL hash (#/shell/shapes); the browser Back button closes the top popup, then the top modal, then pops the page — the C# GoBack order.
Pages — GoToAsync slides the page in from the right (PagesAnimationSpeed 200 ms), GoBackAsync slides it out
Tabs — a nested SkiaShell with Tabs: per-tab navigation stacks, AnimateTabs (C# SkiaViewSwitcher slide + fade), PopTabToRootAsync
Home
Tab 0 · stack []
RouteChanged ''
SkiaBackdrop — the Sandbox MainPageBackdrop frosted glass, same tree
Wonnabe Frosted Glass
Popups — OpenPopupAsync(content, options)
Modals — PushModalAsync(content, { useGestures, animated })
Toasts — ShowToast(text | content, msShowTime)
SkiaEditor
Single line — Text="" · IsFocused=false · · submitted: ""
A focused editor is mirrored by a hidden textarea (TextInputProxy): IME composition, the mobile soft keyboard, autocorrect, native paste / cut / undo land in the drawn editor through the same stub methods the physical keyboard uses.
ControlStyle — Cupertino, Material, Material3, Windows (C# ApplyControlStyleVisuals palettes)
IsPassword — 0 chars hidden behind bullets, KeyboardType Numeric / Email input modes
Multiline — MaxLines={4}: Enter inserts a line, the box scrolls to the caret
Multiline + AutoHeight — MaxLines={-1}: the editor grows with the text
Chat input — MaxLines={3} ReturnType="Send": Enter submits and keeps focus, Shift+Enter breaks the line · 0 sent
Keyboard Input
KeyboardManager attaches window-level keydown / keyup listeners like the DrawnUi.Blazor and Wasm heads (drawnui-keyboard.js): shortcuts, game input, drawn editors. Click the page, then press letters, arrows, modifiers or function keys.
Keyboard input ready
Press letters, arrows, modifiers, or function keys
Last key: waiting
Modifiers: shift false, ctrl false, alt false
KeyChar (printable, no Ctrl/Alt): ""
Recent events
Waiting for input
Waiting for input
Waiting for input
Waiting for input
Waiting for input
SkiaScroll
Header + Footer in the flow · Tag="Header" / Tag="Footer" children scroll with the content
Header (70 pt) · scrolls away
Row 1
Row 2
Row 3
Row 4
HeaderSticky · the header stays at the top, drawn over the content
Row 5
Row 6
Sticky header
Row 7
Under sticky 1
Row 8
Under sticky 2
Row 9
Under sticky 3
Row 10
Under sticky 4
Row 11
Under sticky 5
HeaderBehind + HeaderParallaxRatio=0.5 · the content covers the header, which moves at half speed
Row 12
Under sticky 6
Row 13
Under sticky 7
Parallax cover
Row 14
Under sticky 8
Under sticky 9
Footer (50 pt) · after the content
Content over the cover 1
Under sticky 10
Content over the cover 2
Under sticky 11
Content over the cover 3
Under sticky 12
SkiaScrollBar Tag="ScrollBar" IsDraggable · drag the thumb or press the track, auto-hides 1 s after scrolling · horizontal: ScrollBarsVisibility="Horizontal"
Content over the cover 4
Under sticky 13
Content over the cover 5
Under sticky 14
Scrollbar row 1
Content over the cover 6
Scrollbar row 2
Content over the cover 7
Scrollbar row 3
Content over the cover 8
Scrollbar row 4
Content over the cover 9
Scrollbar row 5
Content over the cover 10
H 1
H 2
H 3
H 4
H 5
H 6
H 7
H 8
H 9
H 10
H 11
H 12
H 13
H 14
Scrollbar row 6
Content over the cover 11
Scrollbar row 7
RefreshEnabled + RefreshIndicator (Tag="RefreshIndicator") · RefreshDistanceLimit 60, RefreshShowDistance 50 · pull down past 60 pt
Content over the cover 12
Scrollbar row 8
Pull down 1
Scrollbar row 9
Pull down 2
Scrollbar row 10
Pull down 3
Scrollbar row 11
Pull down 4
Scrollbar row 12
Pull down 5
Scrollbar row 13
Pull down 6
Scrollbar row 14
SnapToChildren="Center" + TrackIndexPosition="Center" (horizontal) · CurrentIndex -1
Pull down 7
Scrollbar row 15
Shaders
SkiaShaderCarousel — TransitionShader="shaders/transitions/cube.sksl" · IsLooped · LinearSpeedMs=750 · 0 → 1
Slide 1
Slide 2
Slide 4
Slides never move: a ShaderTransitionEffect blends the Image caches of the outgoing and incoming slides (iImage1 / iImage2, progress, ratio) through a gl-transitions style transition(uv) wrapped by the adapter template. Swipe, or drag slowly to scrub the transition; a swipe during a transition wraps it up first (InterruptedTransitionMs).
SkiaShaderEffect on a SkiaImage — ShaderSource="shaders/ripples.sksl" (Sandbox MultiRippleWithTouchEffect) · tap to ripple
The effect is an ISkiaGestureProcessor: every Down starts a ripple at the touch point, animated 0→1 over 4.5 s through Parent.AnimateRangeAsync and passed as the origins[10] / progresses[10] array uniforms; iImage1 is the image's own cache, iImage2 (SecondarySource) the reflection texture.
Inline ShaderCode + SetUniform("strength", 0.01) + iTime · animating
Generative shader — UseBackground="Never" on a SkiaLayer (no input texture)
ShaderSource="shaders/blit.sksl" (pass-through) toggled through VisualEffects · off
Sprites
SkiaSprite — Source="anims/BlueWarrior/Warrior_Idle.png" Columns={8} Rows={1} · 8 frames · 192×192 px · 533 ms
Frames are cut from the sheet by Columns × Rows, transparent borders trimmed per frame (C# SpriteFrameImage), nearest sampling; the animator runs 0..DurationMs and picks the frame by time.
SkiaSpriteSet warrior on a tile board — arrows / WASD move, Space attacks · tile 1,1 · IdleRight
WarriorSprite extends SkiaSpriteSet: Define(0 idle, 1 run, 2 attack) with the FastRepro sheets; WState maps to State and mirrors CurrentSprite.ScaleX; the move is a TranslateToAsync to the target tile while the walk state plays.
Transforms
Applied at render around the arranged box, so layout is untouched and caches stay valid. Same names as MAUI: TranslationX/Y, Rotation, RotationX/Y, ScaleX/Y, SkewX/Y, AnchorX/Y, Opacity, plus TranslationZ for 3D depth.
One property each
DrawnUI
DrawnUI
DrawnUI
DrawnUI
DrawnUI
none
Rotation={15}
RotationX={45}
RotationY={45}
Scale={1.3}
DrawnUI
DrawnUI
DrawnUI
DrawnUI
DrawnUI
ScaleX={-1}
SkewX={20}
TranslationY={10}
Opacity={0.35}
Rotation={15} AnchorX/Y={0}
3D — RotationX, RotationY, TranslationZ, with perspective
DrawnUI
Tap to flip
RotationX 0° · RotationY 0° · TranslationZ 0
RotationX
RotationY
TranslationZ
Perspective is built in: RotationX turns around the horizontal axis, RotationY around the vertical one, TranslationZ moves the control toward (+) or away from (−) the viewer. The card keeps its Image cache while it turns; it shows the back face once it faces away.
Gestures map through 3D — swipe and tap the tilted strip
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Tapped: none
RotationX 16, RotationY 20, Rotation −6. A sideways swipe scrolls the strip and a tap picks the tile under the finger: input goes through the inverse of the 3D matrix. An up or down swipe still scrolls the page.
Gestures map through transforms — tap the rotated, scaled button
The hit rect is the drawn one (inverse RenderTransformMatrix), not the layout box; the ripple lands under the finger.
Animations — FadeToAsync, ScaleToAsync, TranslateToAsync, RotateToAsync (Promises)
50 languages in order of preference · drag by the grip, hold at an edge to keep going
drag a language by its grip, or use the buttons
English (United States)
en-US
Español (España)
es-ES
Français (France)
fr-FR
Deutsch (Deutschland)
de-DE
Italiano (Italia)
it-IT
Português (Brasil)
pt-BR
Nederlands (Nederland)
nl-NL
Svenska (Sverige)
sv-SE
Norsk bokmål (Norge)
nb-NO
Dansk (Danmark)
da-DK
Suomi (Suomi)
fi-FI
Íslenska (Ísland)
is-IS
Polski (Polska)
pl-PL
Čeština (Česko)
cs-CZ
Slovenčina (Slovensko)
sk-SK
← → or drag to move, tap / Space to serve · first to 7
0 : 0
TAP TO SERVE
Press Tab to move between the drawn controls, Enter or Space to activate. Screen readers see the same overlay: roles, labels, hints, pressed state, live regions.
Nodes in the overlay: 24 · focused: none · last activated: -
This paragraph is drawn on the canvas, but the accessibility overlay also carries it as real, invisible text in the same font and line positions. Select it with the mouse or touch, copy it with Ctrl+C, let a screen reader read it word by word — like any HTML paragraph. Off by default: pointer input over selectable text goes to the selection, not to the drawn control, so it is never turned on for buttons, carousels or anything gesture-driven.
This one is a normal label: exposed to screen readers, not selectable.
The yellow circle is decorative: AccessibilityRole=Aria.RolePresentation keeps it out of the tree.
A container with a composite role (Aria.RoleList, RoleToolbar, RoleGrid...) is one Tab stop: the arrow keys move between its items, Home and End go to the first and the last, Enter or Space activates. Tab comes back to the item it left.
Fruits — a list: Up and Down
Numbers — a grid: all four arrows
This label is announced: SkiaLabel.DefaultAccessibilityRole = Aria.RoleText was set once at startup.
• <canvas> is aria-hidden; a DOM overlay mirrors accessible controls with role / aria-label / title / aria-pressed / aria-live / tabindex. • Snapshot rebuilt at most once per second from the arranged rects, so it follows scrolling. • Overlay has pointer-events:none — hover and gestures reach the canvas; keyboard and screen-reader activation are routed as a Tapped. • Same property names as DrawnUi (.NET): AccessibilityRole, AccessibilityLabel, AccessibilityHint, AccessibilityCanInteract, AccessibilityIsPressed, AccessibilityLive.