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.


