Skip to content

9-Slice Editor — Nine-Patch Borders for Godot and Unity

Borders are four numbers in source pixels. The editor suggests them from the longest run of identical columns and rows, lets you drag, type or nudge them, and renders every target size with corners that never change.

A nine-slice (9-patch) border is four numbers in source pixels — left, right, top, bottom — that cut an image into a 3 × 3 grid. The four corners are always drawn at their own size, the top and bottom edges stretch or repeat horizontally, the left and right edges vertically, and the centre both ways. Set the borders once here (suggested from identical columns and rows), preview any size, then enter the same numbers in Godot 4 (NinePatchRect.patch_margin_*), Unity (Sprite Editor border, Image Type Sliced) or Phaser (add.nineslice). The download is PNG + nine-slice.json + SETUP.md; rendering inside the engines was not run.

Drop a UI panel, button, glyph sheet or UI sheet (PNG)
Open the UI Lab
Runs in your browser. Files are not uploaded.
UI Lab: a CC0 UI panel with nine-slice guides and previews at several sizes
The real UI Lab with a CC0 asset (Kenney).
Outputs
  • Nine-sliceMeasured· Pillow
  • Bitmap fontMeasured· independent .fnt parser
  • Button statesMeasured· Pillow
  • Engine importUNVERIFIED

The nine regions and what each one does

For a W × H image with borders L, R, T and B the cuts give four corners (L × T, R × T, L × B, R × B), a top and a bottom edge (W − L − R wide), a left and a right edge (H − T − B tall) and the centre. When the panel is resized, corners are copied unscaled, horizontal edges change only their width, vertical edges only their height and the centre both. That is why a rounded or bevelled corner survives any size, and why the smallest size that shows every corner whole is (L + R) × (T + B).

Stretch resamples an edge or the centre to the new length, which is right for flat colour or a gradient along the other axis. Tile repeats it at its natural size, which keeps a pattern (rivets, stitches, a brick edge) undistorted; Nerulio clips the last partial tile. Godot 4 also has Tile Fit, which stretches the tiles slightly so each one is shown whole; Nerulio's preview does not draw that mode.

Every engine stores the same four numbers under different names and in a different order. Godot 4 uses patch_margin_left/right/top/bottom on a NinePatchRect (integers, in pixels) and texture_margin_* on a StyleBoxTexture. Unity's Sprite Editor has L, R, T and B fields, while the scripting value Sprite.border is a Vector4 in the order left, bottom, right, top. Phaser's NineSlice takes leftWidth, rightWidth, topHeight, bottomHeight after the size, and becomes a 3-slice when only the left and right widths are given.

Border (patch margin)
The distance in source pixels from one edge of the image to the cut line.
Stretch
An edge or the centre is scaled to fill the new length.
Tile
An edge or the centre is repeated at its original size.
Minimum size
(L + R) × (T + B): below it the corners cannot be drawn whole and are squashed.

Example: a 48 × 48 panel drawn at 300 × 80

Borders 12 px on every side. These are the rectangles the editor's draw plan produces, the same plan the exported previews use:

source 48 × 48   borders L 12 · R 12 · T 12 · B 12   target 300 × 80   stretch
corners     4 × (12×12 → 12×12)     copied 1:1, never resampled
top/bottom  24×12 → 276×12          276 = 300 − 12 − 12   (×11.5 horizontally only)
left/right  12×24 → 12×56           56 = 80 − 12 − 12     (×2.33 vertically only)
centre      24×24 → 276×56
tile mode   top edge = 11 tiles of 24 px + 1 clipped tile of 12 px   (11 × 24 + 12 = 276)
too small   target 20 × 80: 20 < 12 + 12 → corners squashed to 10 + 10 (warning "narrow")
asymmetric  L 10 · R 6 · T 8 · B 14   →  Unity Sprite.border (x, y, z, w) = (L, B, R, T) = (10, 14, 6, 8)
Godot       patch_margin_left 12 · right 12 · top 12 · bottom 12
Phaser      this.add.nineslice(x, y, "panel", null, 300, 80, 12, 12, 12, 12)

With an integer scale of 2 the corners are drawn at exactly 24 × 24 (each source pixel becomes 2 × 2), so a pixel-art frame stays crisp at double size.

What it does

  • Border suggestion from identical columns/rows, shown with the run it came from (never applied silently).
  • Stretch or tile edges; tiles repeat at their natural size and the last tile is clipped.
  • Corners are never scaled (checked at 100×40, 300×80, 800×200, 420×120): the same bytes in Chromium; in Firefox semi-transparent corner pixels can shift by one alpha-rounding step.
  • Export: previews, nine-slice.json with pixels, normalised values, Godot patch_margin_* and Unity border (L, B, R, T), and SETUP.md.

How to do it

  1. Drop the panel or button PNG.
  2. Accept the suggested borders or drag/type them (arrow keys nudge 1 px, Shift 10 px).
  3. Preview at your target sizes, stretch or tile.
  4. Export the ZIP.

Files you get

FileWhat it is for
panel.pngThe source image, unchanged, named after your file.
previews/100x40.png, 300x80.png, 800x200.png + custom sizeThe panel rendered at each preview size with your mode and scale (the custom size defaults to 420 × 120).
nine-slice.jsonThe borders as pixels, normalized (border ÷ width or height), godot4 (patch_margin_*) and unity (border in L, B, R, T order, named), plus mode, scale and the preview sizes.
SETUP.mdShort Godot 4 and Unity steps with your numbers filled in, marked as not run inside the engines.

Enter the borders in Godot, Unity or Phaser

  1. Godot 4: add a NinePatchRect, set its Texture to the PNG and patch_margin_left, patch_margin_right, patch_margin_top, patch_margin_bottom to the four numbers from nine-slice.json.
  2. Godot 4, tile mode: set axis_stretch_horizontal and axis_stretch_vertical to Tile (or Tile Fit). For a Button or Panel theme, use a StyleBoxTexture with the same numbers in texture_margin_* and the same axis stretch settings.
  3. Unity: select the texture, set Mesh Type to Full Rect and click Apply, open the Sprite Editor and drag the green handles or type the L, R, T and B fields (the pixels values).
  4. Unity: on a Sprite Renderer set Draw Mode to Sliced (or Tiled, with Tile Mode Continuous or Adaptive); on a UI Image set Image Type to Sliced (or Tiled).
  5. Phaser 3.60 or later: this.add.nineslice(x, y, key, frame, width, height, leftWidth, rightWidth, topHeight, bottomHeight). The Phaser 4.1 API lists tileX and tileY to repeat instead of stretch.
  6. For pixel art keep Nearest filtering and resize by whole numbers of source pixels, so the corners stay sharp.

Check the result

  • Resize the engine node to 300 × 80 and compare it with previews/300x80.png: the four 12 × 12 corners should match pixel for pixel.
  • Shrink it below L + R wide: Nerulio warns "narrow" for the same size, and Phaser documents L + R as the minimum width.
  • In Unity, Debug.Log(sprite.border) should print the unity.border numbers from the JSON (left, bottom, right, top).

Troubleshooting

SymptomLikely causeHow to checkFix
Corners stretch in UnityMesh Type is Tight, or the Image or Sprite Renderer still draws SimpleTexture import settings; Image Type or Draw ModeMesh Type Full Rect + Apply, then Image Type Sliced or Draw Mode Sliced
Top and bottom borders look swapped in UnityThe Vector4 order (left, bottom, right, top) was typed into the L, R, T, B fields, or the other way roundCompare the Sprite Editor fields with pixels in the JSONType pixels into the Sprite Editor; use unity.border only for Sprite.border in scripts
Corners look squashed at small sizesThe control is smaller than L + R or T + BNerulio shows the "narrow" or "short" warning for that sizeMake the control larger or the borders smaller
A patterned edge looks smearedThe edge is stretched, so a repeating pattern is resampledSwitch the preview to Tile and compareGodot: axis stretch Tile or Tile Fit; Unity: Tiled; Phaser 4: tileX / tileY
Pixel-art corners are blurryLinear filtering, or a scale that is not a whole numberZoom in: soft steps on the cornerNearest filtering and integer scales (the editor's scale is 1–4)
Phaser NineSlice from a TexturePacker atlas looks wrongThe frame was trimmed; the Phaser docs say NineSlice does not support trimmed TexturePacker texturesCheck the atlas JSON for trimmed framesPack the panel untrimmed, or load it as its own image
"Suggest borders" finds no clear runThe middle is a gradient or noise, so no columns or rows are identicalThe suggestion says there is no repeating column or rowPlace the guides by hand where the corner art ends

Other ways to do it

OptionWhen it is the better choice
Set the borders directly in Godot's NinePatchRect inspector or Unity's Sprite EditorOne or two panels, already in the engine: you see the real engine rendering while you drag.
The UI Lab atlas stageMany panels on one sheet: each element gets its own borders and they all travel in one ui-atlas.json.

Outputs and how each was checked

"Measured" means the downloaded file was opened again outside the page (Pillow, numpy or an independent parser) and the property was measured. Loading these files in a game engine was not tested.

TargetFilesCheck
Nine-slicenine-slice.json · previews · SETUP.mdMeasured PillowEvery resized preview keeps all four corner blocks unscaled: the same bytes in Chromium; in Firefox semi-transparent pixels can shift by one alpha-rounding step. 0 gaps, 0 overlaps.
Bitmap fontfont.png · font.fnt (BMFont text) · font.jsonMeasured independent .fnt parserThe .fnt was read back by a parser written separately in the test: header and every glyph rect match.
Button statesstates/*.png · strip · states.jsonMeasured PillowNormal equals the source (byte for byte in Chromium; in Firefox semi-transparent colour within one alpha-rounding step); every rect in states.json cuts exactly its state out of the strip.
Engine importGodot NinePatchRect · Unity Sprite border · BMFont importUNVERIFIEDNot run in Godot or Unity.

Evidence

Nine-slice corners unscaled at 100×40, 300×80, 800×200 and 420×120; border suggestion 6·6·6·6 from a run of 12 identical columns; button states: normal equal to the source, disabled grey at alpha 128, focus ring exact; .fnt glyph records equal the JSON; missing glyphs from a .po listed exactly; black on white measured 21:1. Images pass through a browser canvas: Chromium returns the same bytes; Firefox rounds the colour of semi-transparent pixels to one premultiplied step (measured on a Kenney button: up to 4 levels at alpha 63), while alpha and opaque pixels stay exact.

Limits

  • Rendering inside Godot, Unity or Phaser was not run: the numbers match their documentation, not an engine test.
  • No engine files are written (no .tres, .meta or Android .9.png).
  • Tile mode at an integer scale above 1 squashes the final partial tile by up to (scale − 1) ÷ scale of a source pixel.
  • No kerning pairs, one font page, no MSDF; the SDF output is Beta.
  • A TTF's cmap is not parsed, so the missing-glyph check needs a .fnt or the Lab's own font.
  • One atlas page; touching elements count as one.
  • Rendering inside Godot or Unity was not run: the engine numbers (patch margins, L-B-R-T border) are documented, not engine-tested.

Versions and sources

Measured in Chromium for docs/UI-LAB.md: the 6 × 6 corner blocks of every exported preview are byte-identical to the source at 100 × 40, 300 × 80, 800 × 200 and 420 × 120; a plan has 0 gaps and 0 overlaps at 300 × 80, at scale 3 and when squashed to 8 × 4; a striped 12 × 12 panel tiles with its 4 px period; the suggestion finds 6 · 6 · 6 · 6 from runs of 12 identical columns and rows. In Firefox semi-transparent corner pixels can shift by one alpha-rounding step. Godot and Unity import is UNVERIFIED. Engine settings above follow Godot 4.7, Unity 6 and the Phaser API documentation.

Official documentation used

Questions

What order does Unity use?

Unity's scripting value Sprite.border is a Vector4 in the order Left, Bottom, Right, Top; the JSON writes that order and names it. The Sprite Editor itself shows the same numbers as L, R, T and B fields, so type the pixel values there.

Was it tested inside Godot or Unity?

No. The numbers are the ones those engines document; rendering in the engines was not run.

Is it free, and do I need an account?

Everything on these pages is free and works without an account. There is no watermark and no export that is held back.

Are my sprites uploaded?

No. The Studio decodes, packs and exports in this browser tab. Projects are autosaved in the browser (IndexedDB) and can be saved as a .nerulio file.

Does it need Aseprite or an engine plugin installed?

No. .aseprite files are read and written in the browser, and each engine bundle carries what that engine needs (for Godot, the import settings and scene; for Unity and the Godot TileSet, a small editor script).