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.

Plain scale vs 9-slice

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).

9-slice cut lines and result

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:

  1. Project Settings → Rendering → Textures → Canvas Textures → Default Texture Filter = Nearest. Without this every UI texture is blurred.
  2. Display → Window → Stretch → Mode = canvas_items (or viewport) with your base resolution, and Scale Mode = integer so the whole UI scales by whole numbers.
  3. 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.

  1. Add a NinePatchRect to your UI scene and set Texture to your panel PNG.
  2. Under Patch Margin, set Left, Top, Right and Bottom to 6 (your border width).
  3. 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.

  1. Create a new StyleBoxTexture resource and set Texture to the panel PNG.
  2. Set Texture Margins (left, top, right, bottom) to 6. These are the 9-slice cuts.
  3. Set Content Margins to how much padding you want between the border and the content (often the same 6, sometimes a bit more).
  4. In a Theme, assign it to PanelContainer/styles/panel, Button/styles/normal, and so on. Make a second texture (pressed, darker) for Button/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_size of at least 2 × margin on 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.
  • NinePatchRect for quick boxes, StyleBoxTexture in 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.