Quick starts
Single display model is one fixed look. Selected state switches with a condition on selected. Charge stages steps through cooldown with range_dispatch. Main-hand context uses select on the hand.
Build the new item definition files that live in assets/
Load a starter pattern, then swap the file paths and values to match your real pack.
Choose how the item picks its model, fill in the paths and branches, and copy one item definition file.
Single display model is one fixed look. Selected state switches with a condition on selected. Charge stages steps through cooldown with range_dispatch. Main-hand context uses select on the hand.
Namespace and path give assets/<namespace>/items/<path>.json. An item uses it through its item_model component; a file named after a vanilla item in the minecraft namespace replaces that item's look.
The model shown when no branch matches. Point it at something that exists, such as the vanilla model, so a missing case never renders as the purple and black error texture.
For custom_model_data the index picks the slot: the n-th string for select, the n-th float for range_dispatch.
For select, each branch is a value and a model. For range_dispatch, a threshold and a model: the highest threshold not above the value wins. For condition, the first branch is used when true, the second when false.
Every path in the file points to assets/<namespace>/models/<path>.json. A typo there shows the error texture, not an error message.
A select on custom_model_data compares text: give the item custom_model_data={strings:["novice"]} and write novice in the branch.
minecraft:damage is normalized by default, so 0.25 means a quarter of the durability is used up, not 25 points.
Item definitions exist from 1.21.4 and are current up to 26.3. Before 1.21.4, models chose variants through overrides inside models/item files instead, so this file does nothing there. A matching command: /give @p minecraft:book[item_model="cubeinsquare:book/formulary",custom_model_data={strings:["novice"]}].