Every game UI needs boxes: dialogue windows, inventory slots, buttons, tooltips. Drawing a new box for every size is a waste of time, and scaling one box up ruins it. The fix is 9-slice (Godot calls it nine-patch): draw one small panel, tell the engine where the borders are, and let it stretch only the parts that can stretch.
The problem with plain scaling
Take a 24x24 panel with a 6 px border and scale it to a wide dialogue box. The border scales with it, so it gets fat on one axis and thin on the other, and the bevel looks wrong.
How 9-slice works
Two vertical and two horizontal cut lines split the source into nine pieces:
- 4 corners: never stretched.
- 4 edges: stretched along one axis only (top and bottom horizontally, left and right vertically).
- 1 center: stretched both ways (or tiled).
The cut lines are just four numbers, the margins from each side. For the panel above they are all 6.
Drawing a panel that slices well
A few rules that save a round trip back to the editor:
- Keep the border the same width all round unless you really want different margins. Equal margins mean one number in Godot.
- Keep the edges uniform. Anything in the edge strip gets stretched, so a rivet or a notch in the middle of the top edge will smear. Put details in the corners.
- Keep the center flat (or a pattern that tiles cleanly). A gradient in the center will stretch into a blur.
- Draw at the size it will be used, not bigger. For a 320x180 game, a 24x24 or 32x32 panel is plenty.
I draw mine in a free browser pixel art editor and export a PNG; any editor works as long as it saves without anti-aliasing.
Making it crisp in Godot 4
Before slicing, make sure Godot isn't filtering your pixels:
- Project Settings → Rendering → Textures → Canvas Textures → Default Texture Filter = Nearest. Without this every UI texture is blurred.
-
Display → Window → Stretch → Mode = canvas_items (or
viewport) with your base resolution, and Scale Mode = integer so the whole UI scales by whole numbers. - If text or panels jitter when moving, enable Rendering → 2D → Snap 2D Transforms to Pixel.
Option 1: NinePatchRect
NinePatchRect is a Control node that draws a texture as nine pieces. It's the quickest way to get a resizable box.
- Add a
NinePatchRectto your UI scene and set Texture to your panel PNG. - Under Patch Margin, set Left, Top, Right and Bottom to 6 (your border width).
- Resize the node. Corners stay put, edges and center stretch.
var panel := NinePatchRect.new()
panel.texture = preload("res://ui/panel-24.png")
panel.patch_margin_left = 6
panel.patch_margin_top = 6
panel.patch_margin_right = 6
panel.patch_margin_bottom = 6
panel.size = Vector2(160, 48)
add_child(panel)
Axis Stretch controls how edges and center fill: Stretch (default), Tile, or Tile Fit. Use Tile when your edge has a repeating pattern (a rope, a chain, stitched leather); Tile Fit adjusts the tile slightly so it ends cleanly.
NinePatchRect doesn't hold children nicely by itself; put it behind a MarginContainer (margins equal to the patch margins) when you need content inside.
Option 2: StyleBoxTexture (for buttons and themes)
For anything built from Godot's Control nodes (Button, PanelContainer, LineEdit, Tooltip), use a StyleBoxTexture instead. It plugs into the theme, so every button and panel picks it up.
- Create a new
StyleBoxTextureresource and set Texture to the panel PNG. - Set Texture Margins (left, top, right, bottom) to 6. These are the 9-slice cuts.
- Set Content Margins to how much padding you want between the border and the content (often the same 6, sometimes a bit more).
- In a
Theme, assign it toPanelContainer/styles/panel,Button/styles/normal, and so on. Make a second texture (pressed, darker) forButton/styles/pressed.
var sb := StyleBoxTexture.new()
sb.texture = preload("res://ui/panel-24.png")
sb.texture_margin_left = 6
sb.texture_margin_top = 6
sb.texture_margin_right = 6
sb.texture_margin_bottom = 6
sb.content_margin_left = 8
sb.content_margin_right = 8
sb.content_margin_top = 6
sb.content_margin_bottom = 6
$PanelContainer.add_theme_stylebox_override("panel", sb)
Saved as a .tres theme, the whole UI changes style by swapping one file.
Common gotchas
-
Blurry edges: the texture filter is still Linear. Set it to Nearest project-wide or on the node's
Texture Filter. - Border looks 1 px off: the margins don't match your drawing. Count the border pixels again, including the dark outline.
-
Box smaller than its corners: at sizes below twice the margin, corners overlap. Give the node a
custom_minimum_sizeof at least2 × marginon each axis. - Odd scaling on high-DPI screens: use integer stretch scale, not fractional.
Summary
- Draw one small panel with uniform edges and a flat center.
- Turn off texture filtering.
-
NinePatchRectfor quick boxes,StyleBoxTexturein a theme for buttons and containers. - Patch/texture margins = your border width.
If you want a ready base to start from, sketch a panel in the pixel art editor at SimplePixelArt, export it at 1x, and drop it straight into the steps above.


Top comments (1)
Good checklist, the "put details in the corners" rule saves a lot of redraws.
One thing that bit me with Tile on pixel panels: if (width - left margin - right margin) isn't a multiple of the edge pattern's width, the last repeat gets cut off mid-pattern at the right and bottom. Tile Fit hides the seam, but it does it by slightly scaling the tiles, and with Nearest filtering that shows up as a doubled pixel column here and there. What worked for me was snapping the node size in code instead: round the inner width and height up to a multiple of the pattern, then plain Tile always ends on a whole repeat.
Also handy for focus/hover states: StyleBoxTexture has expand_margin_*, so you can draw a 1 px highlight ring outside the box without changing the layout or the content margins.