Skip to main content
🤖 URDF GuideUpdated June 2026

ROS 2 URDF & Xacro Guide 2026Robot Modeling · Inertia · Joints · Launch

Complete 2026 guide to URDF and Xacro for ROS 2. Covers writing a differential drive robot from scratch, all joint types, inertia calculations, Xacro properties and macros to eliminate boilerplate, include files for large robots, check_urdf validation, and launching robot_state_publisher with a Python launch file.

URDFXacrorobot_state_publisherinertiacheck_urdf

URDF Joint Types

TypeMotionUse Case
fixedNoneSensor mounts, static chassis parts, structural joints
continuousUnlimited rotationDrive wheels, spinning sensors, propellers
revoluteRotation with limitsRobot arm joints, grippers, pan-tilt heads
prismaticLinear slide with limitsLinear actuators, telescoping links, drawer joints
planarMotion in a planeRare — 2D sliding on a surface
floating6 DOF (no constraint)Base of free-floating robots, spacecraft

Write a URDF From Scratch

Minimal URDF — Two Links and One Joint

Start with the smallest valid URDF: a base_link, a child link, and a fixed joint connecting them.

xml
<?xml version="1.0"?>
<robot name="minimal_robot">

  <!-- Base link (root — must have no parent) -->
  <link name="base_link">
    <visual>
      <geometry>
        <box size="0.3 0.2 0.1"/>
      </geometry>
      <material name="gray">
        <color rgba="0.5 0.5 0.5 1.0"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <box size="0.3 0.2 0.1"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="5.0"/>
      <!-- Solid box inertia: m/12*(y²+z²), m/12*(x²+z²), m/12*(x²+y²) -->
      <inertia ixx="0.0108" ixy="0.0" ixz="0.0"
               iyy="0.0283" iyz="0.0"
               izz="0.0342"/>
    </inertial>
  </link>

  <!-- Lidar sensor link -->
  <link name="lidar_link">
    <visual>
      <geometry>
        <cylinder radius="0.04" length="0.07"/>
      </geometry>
      <material name="black">
        <color rgba="0.1 0.1 0.1 1.0"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="0.04" length="0.07"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="0.2"/>
      <inertia ixx="0.000108" ixy="0.0" ixz="0.0"
               iyy="0.000108" iyz="0.0"
               izz="0.000160"/>
    </inertial>
  </link>

  <!-- Fixed joint: lidar mounted 0.15 m forward, 0.08 m above base -->
  <joint name="base_to_lidar" type="fixed">
    <parent link="base_link"/>
    <child link="lidar_link"/>
    <origin xyz="0.15 0.0 0.08" rpy="0.0 0.0 0.0"/>
  </joint>

</robot>

💡 Joint types: fixed (no motion), continuous (unlimited rotation), revolute (rotation with limits), prismatic (linear slide).

Differential Drive Robot URDF

Full differential drive robot with two drive wheels, one caster, and a lidar sensor link.

xml
<?xml version="1.0"?>
<robot name="diff_drive_robot">

  <link name="base_link">
    <visual>
      <geometry><box size="0.4 0.3 0.12"/></geometry>
      <material name="blue"><color rgba="0.2 0.4 0.8 1.0"/></material>
    </visual>
    <collision><geometry><box size="0.4 0.3 0.12"/></geometry></collision>
    <inertial>
      <mass value="10.0"/>
      <inertia ixx="0.0860" ixy="0.0" ixz="0.0"
               iyy="0.1367" iyz="0.0" izz="0.2083"/>
    </inertial>
  </link>

  <!-- Left wheel -->
  <link name="left_wheel">
    <visual>
      <geometry><cylinder radius="0.08" length="0.04"/></geometry>
      <material name="dark"><color rgba="0.1 0.1 0.1 1.0"/></material>
    </visual>
    <collision><geometry><cylinder radius="0.08" length="0.04"/></geometry></collision>
    <inertial>
      <mass value="0.5"/>
      <inertia ixx="0.001600" ixy="0.0" ixz="0.0"
               iyy="0.001600" iyz="0.0" izz="0.003200"/>
    </inertial>
  </link>

  <joint name="left_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="left_wheel"/>
    <!-- Wheel axle is along Y-axis → rotate wheel 90° around X -->
    <origin xyz="-0.1 0.17 -0.04" rpy="1.5708 0.0 0.0"/>
    <axis xyz="0 0 1"/>
  </joint>

  <!-- Right wheel (mirror of left) -->
  <link name="right_wheel">
    <visual>
      <geometry><cylinder radius="0.08" length="0.04"/></geometry>
      <material name="dark"><color rgba="0.1 0.1 0.1 1.0"/></material>
    </visual>
    <collision><geometry><cylinder radius="0.08" length="0.04"/></geometry></collision>
    <inertial>
      <mass value="0.5"/>
      <inertia ixx="0.001600" ixy="0.0" ixz="0.0"
               iyy="0.001600" iyz="0.0" izz="0.003200"/>
    </inertial>
  </link>

  <joint name="right_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="right_wheel"/>
    <origin xyz="-0.1 -0.17 -0.04" rpy="1.5708 0.0 0.0"/>
    <axis xyz="0 0 1"/>
  </joint>

  <!-- Caster wheel (ball caster — modeled as sphere with zero friction) -->
  <link name="caster_link">
    <visual>
      <geometry><sphere radius="0.04"/></geometry>
      <material name="gray"><color rgba="0.6 0.6 0.6 1.0"/></material>
    </visual>
    <collision><geometry><sphere radius="0.04"/></geometry></collision>
    <inertial>
      <mass value="0.1"/>
      <inertia ixx="0.000064" ixy="0.0" ixz="0.0"
               iyy="0.000064" iyz="0.0" izz="0.000064"/>
    </inertial>
  </link>

  <joint name="caster_joint" type="fixed">
    <parent link="base_link"/>
    <child link="caster_link"/>
    <origin xyz="0.15 0.0 -0.04" rpy="0.0 0.0 0.0"/>
  </joint>

  <!-- Lidar -->
  <link name="lidar_link">
    <visual>
      <geometry><cylinder radius="0.035" length="0.065"/></geometry>
      <material name="black"><color rgba="0.1 0.1 0.1 1.0"/></material>
    </visual>
    <collision><geometry><cylinder radius="0.035" length="0.065"/></geometry></collision>
    <inertial>
      <mass value="0.18"/>
      <inertia ixx="0.0000868" ixy="0.0" ixz="0.0"
               iyy="0.0000868" iyz="0.0" izz="0.0001103"/>
    </inertial>
  </link>

  <joint name="lidar_joint" type="fixed">
    <parent link="base_link"/>
    <child link="lidar_link"/>
    <origin xyz="0.0 0.0 0.095" rpy="0.0 0.0 0.0"/>
  </joint>

</robot>

Xacro — Parametric URDF

Xacro Basics — Properties, Macros, Math

Xacro eliminates URDF boilerplate: define constants once and compute inertias with math expressions.

xml
<?xml version="1.0"?>
<robot name="my_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">

  <!-- ===== Properties (constants) ===== -->
  <xacro:property name="base_length" value="0.4"/>
  <xacro:property name="base_width"  value="0.3"/>
  <xacro:property name="base_height" value="0.12"/>
  <xacro:property name="base_mass"   value="10.0"/>

  <xacro:property name="wheel_radius" value="0.08"/>
  <xacro:property name="wheel_width"  value="0.04"/>
  <xacro:property name="wheel_mass"   value="0.5"/>

  <xacro:property name="wheel_offset_x" value="-0.1"/>
  <xacro:property name="wheel_offset_y" value="0.17"/>
  <xacro:property name="wheel_offset_z" value="-0.04"/>

  <!-- ===== Inertia macros ===== -->
  <xacro:macro name="box_inertia" params="m x y z">
    <inertial>
      <mass value="${m}"/>
      <inertia
        ixx="${m/12.0*(y*y + z*z)}" ixy="0.0" ixz="0.0"
        iyy="${m/12.0*(x*x + z*z)}" iyz="0.0"
        izz="${m/12.0*(x*x + y*y)}"/>
    </inertial>
  </xacro:macro>

  <xacro:macro name="cylinder_inertia" params="m r l">
    <inertial>
      <mass value="${m}"/>
      <inertia
        ixx="${m/12.0*(3*r*r + l*l)}" ixy="0.0" ixz="0.0"
        iyy="${m/12.0*(3*r*r + l*l)}" iyz="0.0"
        izz="${m/2.0*r*r}"/>
    </inertial>
  </xacro:macro>

  <!-- ===== Wheel macro (reused for left and right) ===== -->
  <xacro:macro name="wheel" params="prefix y_reflect">
    <link name="${prefix}_wheel">
      <visual>
        <geometry>
          <cylinder radius="${wheel_radius}" length="${wheel_width}"/>
        </geometry>
        <material name="dark"><color rgba="0.1 0.1 0.1 1.0"/></material>
      </visual>
      <collision>
        <geometry>
          <cylinder radius="${wheel_radius}" length="${wheel_width}"/>
        </geometry>
      </collision>
      <xacro:cylinder_inertia m="${wheel_mass}" r="${wheel_radius}" l="${wheel_width}"/>
    </link>

    <joint name="${prefix}_wheel_joint" type="continuous">
      <parent link="base_link"/>
      <child link="${prefix}_wheel"/>
      <origin xyz="${wheel_offset_x} ${y_reflect*wheel_offset_y} ${wheel_offset_z}"
              rpy="1.5708 0.0 0.0"/>
      <axis xyz="0 0 1"/>
    </joint>
  </xacro:macro>

  <!-- ===== Base link ===== -->
  <link name="base_link">
    <visual>
      <geometry>
        <box size="${base_length} ${base_width} ${base_height}"/>
      </geometry>
      <material name="blue"><color rgba="0.2 0.4 0.8 1.0"/></material>
    </visual>
    <collision>
      <geometry>
        <box size="${base_length} ${base_width} ${base_height}"/>
      </geometry>
    </collision>
    <xacro:box_inertia m="${base_mass}"
                       x="${base_length}" y="${base_width}" z="${base_height}"/>
  </link>

  <!-- ===== Instantiate wheels ===== -->
  <xacro:wheel prefix="left"  y_reflect="1"/>
  <xacro:wheel prefix="right" y_reflect="-1"/>

</robot>

Convert a .xacro file to plain URDF: `xacro robot.urdf.xacro > robot.urdf`. Pass parameters with `xacro robot.urdf.xacro wheel_radius:=0.1`.

Xacro Include Files

Split a large robot description into per-component xacro files included in the main file.

xml
<?xml version="1.0"?>
<!-- main.urdf.xacro -->
<robot name="full_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">

  <!-- Include shared macros (inertia helpers, materials, etc.) -->
  <xacro:include filename="$(find my_robot_description)/urdf/macros.xacro"/>

  <!-- Include sub-assemblies -->
  <xacro:include filename="$(find my_robot_description)/urdf/base.xacro"/>
  <xacro:include filename="$(find my_robot_description)/urdf/arm.xacro"/>
  <xacro:include filename="$(find my_robot_description)/urdf/sensors.xacro"/>

  <!-- Pass parameters to included macros -->
  <xacro:arg name="use_gpu_lidar" default="false"/>

</robot>

Launch robot_state_publisher

Launch robot_state_publisher with Xacro

Python launch file that processes the Xacro file and passes robot_description to robot_state_publisher.

python
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import Command, LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
import os


def generate_launch_description():
    pkg_share = FindPackageShare("my_robot_description")

    urdf_file = PathJoinSubstitution([pkg_share, "urdf", "robot.urdf.xacro"])

    robot_description_content = Command([
        "xacro ", urdf_file,
        " use_gpu_lidar:=", LaunchConfiguration("use_gpu_lidar"),
    ])

    return LaunchDescription([
        DeclareLaunchArgument(
            "use_gpu_lidar", default_value="false",
            description="Use GPU-accelerated lidar plugin"
        ),

        Node(
            package="robot_state_publisher",
            executable="robot_state_publisher",
            name="robot_state_publisher",
            output="screen",
            parameters=[{
                "robot_description": robot_description_content,
                "use_sim_time": False,
            }],
        ),

        # Publish dummy joint states for visualization (no controller)
        Node(
            package="joint_state_publisher_gui",
            executable="joint_state_publisher_gui",
            name="joint_state_publisher_gui",
        ),
    ])

Validation & Visualization

URDF Check & Visualization Tools

Validate your URDF and visualize it in RViz2 from the command line.

bash
# Install URDF tools
sudo apt install ros-jazzy-urdf-tutorial ros-jazzy-joint-state-publisher-gui
sudo apt install ros-jazzy-xacro ros-jazzy-robot-state-publisher

# Check URDF for syntax errors
check_urdf robot.urdf
# Output: robot name is: my_robot
#         ---------- Successfully Parsed XML ---------------
#         root Link: base_link has 3 child(ren)

# Convert Xacro to URDF and check in one step
xacro robot.urdf.xacro | check_urdf /dev/stdin

# Visualize in RViz2 (one command with joint slider GUI)
ros2 launch urdf_tutorial display.launch.py model:=$(pwd)/robot.urdf.xacro

# View TF tree generated from the URDF
ros2 run tf2_tools view_frames
# → Saves frames.pdf in current directory

Related Guides