Guide

Parametric design in OpenSCAD: build models that resize cleanly

Parametric design means building a model from named dimensions and the rules between them, so changing one number regenerates a correct part. In OpenSCAD every size is already a variable; this guide shows the structure that keeps a model correct when you, a slider or the CAD Agent change those values.

8 min read Updated October 2026 Intermediate Design guides · 1 of 3
In short
  1. Every dimension is a named parameter at the top, with a label comment above it.
  2. Derive dependent values from parameters; never type the same number twice.
  3. One module per feature, one top-level call to build the part.
  4. Guard limits with assert(), then test every slider at its min and max.

Why parametric design pays off

  • A bracket with leg_length is both a 30 mm corner bracket and a 100 mm shelf bracket.
  • A cable clip with cable_d fits a USB-C lead and a Cat 6 cable without a rewrite.
  • A gear with teeth and gear_mod becomes a whole gear family.
  • When a test print is 0.3 mm too tight, you change one number instead of redrawing.

The parametric workflow, step by step

Click each step as you finish it. Every step ends with something you can check.

  1. Write the requirements as numbers. Measure what the part attaches to and what must fit in it, and note which sizes will change. Check: you have a list of sizes in millimetres, with the variable ones marked.
  2. Put the parameters at the top. One name = value; line per dimension, each with a label comment above it. Check: sliders appear in the editor's Adjust tab (Params in Advanced mode).
  3. Load libraries after the parameters. include and use lines go below the last parameter. Check: the sliders are still there.
  4. Derive everything else. Hole positions, gusset sizes and inner dimensions are expressions of the parameters. Check: no number in the geometry repeats a parameter's value.
  5. Give each feature its own module. Use descriptive names and put a // @feature: name tag above each call. Check: the editor shows one chip per feature.
  6. Build the part from one top-level call. Check: the file ends with a single call such as bracket();, so it exports as one solid.
  7. Add assert() guards. Protect minimum walls and impossible combinations. Check: a bad value stops the render with a clear message.
  8. Drag every slider to its minimum and maximum. Check: the part renders at every extreme and holes never break through an edge.
  9. Print a fit test. Print just the critical hole, slot or snap before the whole part. Check: the coupon fits, or you know which parameter to adjust.

Parameters that become sliders

// Box length (mm) [20:1:150]
box_length = 60; // [20:1:150]
// Box width (mm) [20:1:100]
box_width = 40; // [20:1:100]
// Box height (mm) [10:1:80]
box_height = 30; // [10:1:80]
// Wall thickness (mm) [1.2:0.2:4]
wall = 2; // [1.2:0.2:4]

difference() {
  cube([box_length, box_width, box_height]);
  translate([wall, wall, wall])
    cube([box_length - 2 * wall, box_width - 2 * wall, box_height]);
}

In the PrintPal editor these lines become sliders in the Adjust tab (Simple mode) or the Params tab (Advanced mode). Dragging one re-renders instantly without an AI call. Desktop OpenSCAD's Customizer reads a slightly different format, which is why each line carries the range twice:

PrintPal editorDesktop Customizer
LabelThe comment line above the variableThe comment line above the variable
Range[min:step:max] or [min:max] in the comment aboveA trailing comment on the same line, e.g. // [10:1:100]
Value typesNumbers, true/false, strings and lists of numbersLiterals, and vectors of up to 4 numbers
Stops reading atThe first include, use, module, function or shape lineThe first {
Hiding a valueWrite it as an expression, or define it after the first modulePut it after a /* [Hidden] */ comment

An include on line 1 means no sliders. The PrintPal editor stops reading parameters at the first include or use. Libraries like BOSL2 go below the parameter block; see OpenSCAD libraries.

Naming parameters

  • Spell names out: plate_width beats pw, and it is what the slider shows if there is no label comment.
  • Put the unit in the label: // Wall thickness (mm). OpenSCAD has no units; the convention for printing is millimetres.
  • Counts: hole_count, tooth_count. Switches: has_lid, add_gusset.
  • Hardware: name the standard, as in m3_clearance_d = 3.4.
  • Short names like d, r and h are fine as module arguments, where the context is obvious.

Derive values instead of typing them twice

If two values move together, write the dependent one as an expression of the other:

plate_w = 40;
hole_d = 5;
hole_x = plate_w / 2;    // always centred, even after plate_w changes
difference() {
  cube([plate_w, 20, 3]);
  translate([hole_x, 10, -1]) cylinder(h=5, d=hole_d, $fn=32);
}

hole_x = 20; is right today and wrong as soon as plate_w changes.

hole_x = plate_w / 2; states the intent: the hole is always centred.

A file has plate_w = 40; and hole_x = 20;. You drag plate_w to 60. Where is the hole now?

  1. Still centred, at x = 30
  2. At x = 20, now off-centre
  3. The hole disappears

A typed number never follows other parameters. Writing hole_x = plate_w / 2; keeps it centred, and as an expression it also stays out of the sliders.

One module per feature, tagged at the call

Wrap each feature in a named module, even ones used once, and assemble them in one place. You can then read, reuse or replace a feature without touching the rest. The modules and functions page covers the syntax.

In the PrintPal editor, a // @feature: name comment goes on the line above the module call, not above the definition. Each tag becomes a chip: click it to highlight that feature in 3D and add @feature[name] to your message, so the CAD Agent knows exactly which part you mean. The full example below shows the layout.

Defaults with overrides

Give reusable modules sensible defaults and let each call override only what differs:

module m3_hole(depth=10, d=3.4) {
  translate([0, 0, -1]) cylinder(h=depth + 2, d=d, $fn=32);
}

difference() {
  cube([50, 15, 12]);
  translate([10, 7.5, 0]) m3_hole();               // defaults
  translate([25, 7.5, 0]) m3_hole(depth=20);       // override one
  translate([40, 7.5, 0]) m3_hole(d=3.0, depth=6); // tighter pilot hole, override both
}

Guard limits with assert()

assert(condition, "message") stops the render when a combination makes no sense, instead of quietly producing a broken part. This is what a failing guard looks like:

thickness = 1;
assert(thickness >= 1.2, "Walls under 1.2 mm print poorly with a 0.4 mm nozzle.");
cube(10);
ERROR: Assertion '(thickness >= 1.2)' failed: "Walls under 1.2 mm print poorly with a 0.4 mm nozzle."

Good guards protect minimum walls, holes that would break through an edge, and features that collide. With a 0.4 mm nozzle, 1.2 mm (about three perimeters) is a safe minimum wall, and load-bearing walls are better at 2 mm or more; the 3D printing design guide has the details.

Echo a summary for yourself

An echo() at the end of the file prints key sizes on every render. In the PrintPal editor it appears in the Console tab next to the Code tab in Advanced mode. It is for you: the CAD Agent does not read echo output. It checks each render's measured size and a preview image instead.

Full example: a parametric L-bracket

Everything above in one file: labelled parameters, derived values, guards, one module per feature with @feature tags at the calls, a single entry point and an echo. Drag leg_length from 40 to 100 and the holes and gusset follow.

// L-bracket: parametric corner mount for M3 screws

// === Parameters (each literal below becomes a slider) =========
// Leg length (mm) [15:1:120]
leg_length = 40; // [15:1:120]
// Bracket width (mm) [10:1:60]
width = 20; // [10:1:60]
// Wall thickness (mm) [2:0.5:8]
thickness = 4; // [2:0.5:8]
// Screw clearance hole (mm) [2:0.1:8]
hole_d = 3.4; // [2:0.1:8]
// Hole distance from the leg end (mm) [4:0.5:30]
hole_inset = 8; // [4:0.5:30]
// Corner gusset?
add_gusset = true;
$fn = 48;

// === Derived values (expressions never become sliders) ========
gusset_size = (leg_length - thickness) / 2;
gusset_thickness = thickness;

// === Guards ===================================================
assert(thickness >= 1.2, "Walls under 1.2 mm print poorly with a 0.4 mm nozzle.");
assert(hole_inset * 2 < leg_length, "hole_inset is too large for this leg_length.");
assert(hole_d < width - 2, "The hole is wider than the leg.");

// === Features =================================================
module horizontal_leg() { cube([leg_length, width, thickness]); }

module vertical_leg() { cube([thickness, width, leg_length]); }

module gusset() {
  translate([thickness, (width + gusset_thickness) / 2, thickness])
    rotate([90, 0, 0])
      linear_extrude(height=gusset_thickness)
        polygon([[0, 0], [gusset_size, 0], [0, gusset_size]]);
}

module mounting_holes() {
  translate([leg_length - hole_inset, width / 2, -1])
    cylinder(h=thickness + 2, d=hole_d);
  translate([-1, width / 2, leg_length - hole_inset])
    rotate([0, 90, 0]) cylinder(h=thickness + 2, d=hole_d);
}

// === Assembly =================================================
module bracket() {
  difference() {
    union() {
      // @feature: horizontal_leg
      horizontal_leg();
      // @feature: vertical_leg
      vertical_leg();
      // @feature: gusset
      if (add_gusset) gusset();
    }
    // @feature: mounting_holes
    mounting_holes();
  }
}

bracket();
echo(str("Envelope: ", leg_length, " × ", width, " × ", leg_length, " mm"));

Test the extremes, then print a fit test

  • Every slider renders at its minimum and its maximum.
  • Walls stay at 1.2 mm or more at every setting, and assert() enforces it.
  • Holes and slots never break through an edge at the smallest size.
  • Mating parts have clearance: start around 0.2 mm per side for a snug fit and tune from a test print.
  • A small coupon of the critical fit has been printed and checked.

Anti-patterns

AvoidBecause
Magic numbers in the geometryYou cannot change a size you cannot find. Name it.
One 400-line difference()Hard to read, impossible to reuse, and there is nothing to tag with @feature.
Names like part1() or thing()Future you and the CAD Agent both need to know what a module makes.
include lines above the parametersThe PrintPal editor finds no sliders.
A range only in a trailing commentDesktop OpenSCAD reads it, but the PrintPal editor only reads the comment above.
Renaming features mid-conversationEarlier @feature[…] references in the chat stop matching. Add a new feature instead.

Working with the CAD Agent

The CAD Agent already writes parameters at the top with a label comment above each and puts @feature tags above module calls. To keep a model easy to edit over many turns:

  • Keep the @feature tags when you edit by hand; they are how you point at parts in chat.
  • Ask for the parameters you care about: "make the hole spacing and wall thickness sliders".
  • Comment intent, not mechanics: "rib must clear the M3 boss above" helps far more than "adds a 3 × 10 mm cube".

The AI CAD prompting guide has more ways to phrase requests.

Design a parametric L-bracket for M4 screws: 50 mm legs, 25 mm wide, 5 mm thick, with a triangular gusset and two 4.5 mm holes per leg. Put every dimension at the top as a labelled slider with a sensible range, add assert() checks for minimum wall thickness and hole edge distance, and tag each feature with @feature.

Make a parametric cable clip that screws to a desk edge. Parameters: cable diameter 4 to 12 mm, number of cable slots 1 to 6, clip width 10 mm, and one 3.5 mm countersunk screw hole. Keep 0.3 mm of clearance around each cable and walls at least 1.6 mm thick.

AI tool · free to tryCAD AgentDescribe a part and get parametric OpenSCAD with sliders for every dimension.Open tool Free PrintPal toolBin & Gridfinity GeneratorA ready-made parametric tool: set the size and get a printable bin or Gridfinity baseplate.Open tool

Frequently asked questions

What is parametric design?

Parametric design builds a model from named parameters and the relationships between them, so changing one value, such as a length or hole size, regenerates the whole part correctly. OpenSCAD is parametric by nature because every shape is defined in code.

How do I add sliders to an OpenSCAD model?

Put each parameter at the top of the file as a plain assignment, with a comment line above it holding a label and a [min:step:max] range. The PrintPal editor reads that comment. Desktop OpenSCAD's Customizer reads the range from a trailing comment on the same line, so write both to support both.

Why don't my parameters show up as sliders?

The PrintPal editor stops reading parameters at the first include, use, module or shape line, so anything below an include is missed. Values written as expressions, such as plate_width / 2, never become sliders either. Move plain value assignments to the very top of the file.

What minimum wall thickness should a parametric part allow?

With a 0.4 mm nozzle, 1.2 mm (about three perimeters) is a safe minimum for most walls, and load-bearing walls are better at 2 mm or more. Enforce the minimum with assert() so no slider setting can go below it.

How do I keep a variable out of the sliders?

In the PrintPal editor, only plain values at the top of the file become sliders, so write the variable as an expression or define it after the first module. Desktop OpenSCAD hides everything after a /* [Hidden] */ comment, which the PrintPal editor does not read.

Sources & further reading