Wrap list parent in div with wp_nav_menu menu

Since WordPress make use of Walkers you can extend the class Walker_Nav_Menu with your own and pass the instance of it to the function wp_nav_menu.

So your call will be:

<?php wp_nav_menu( array(
    'menu'   => 'Main Menu'
    'walker' => new Custom_Nav_Walker()
) ); ?>

Then in a separated file create the class Custom_Nav_Walker that will be your walker.

The class extends two methods of Walker_Nav_Menu that are start_el and end_el. The code within them is pretty the same of the main class, except that we introduce a little tweak to wrap the nav items with a div.

WordPress unfortunately mix up php and html code sometimes using string like this one $output .= $indent . '<li' . $id . $class_names . '>';

Within the Walker_Nav_Menu::start_el you’ll find that line of code that is the opened tag for all of the list items ( the method is called everytime a new item need to be added to the list ).

If we change it with this one $output .= $indent . (0 === $depth ? '<div ' : '<li ' ) . $id . $class_names . '>'; we’ll use the div tag for all of the items at the first level.

Then in the method end_el we change this one $output .= "</li>{$n}"; with $output .= (0 === $depth ? "</div>{$n}" : "</li>{$n}"); so we can close the previously opened div tag.

Unfortunately unless you deep into complicated regexp this is the best and rapid solution to achive your goal.

The problem with this solution is that you must extend the Walker_Nav_Menu class and override the two methods above, meaning unnecessary code to maintain in case of WordPress change something.

Here the entire subclass used as walker for the menu.

class Custom_Nav_Walker extends Walker_Nav_Menu {

public function start_el( &$output, $item, $depth = 0, $args = [], $id = 0 ) {

    if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
        $t="";
        $n = '';
    } else {
        $t = "\t";
        $n = "\n";
    }
    $indent = ( $depth ) ? str_repeat( $t, $depth ) : '';

    $classes   = empty( $item->classes ) ? array() : (array) $item->classes;
    $classes[] = 'menu-item-' . $item->ID;

    /**
     * Filters the arguments for a single nav menu item.
     *
     * @since 4.4.0
     *
     * @param stdClass $args  An object of wp_nav_menu() arguments.
     * @param WP_Post  $item  Menu item data object.
     * @param int      $depth Depth of menu item. Used for padding.
     */
    $args = apply_filters( 'nav_menu_item_args', $args, $item, $depth );

    /**
     * Filters the CSS class(es) applied to a menu item's list item element.
     *
     * @since 3.0.0
     * @since 4.1.0 The `$depth` parameter was added.
     *
     * @param array    $classes The CSS classes that are applied to the menu item's `<li>` element.
     * @param WP_Post  $item    The current menu item.
     * @param stdClass $args    An object of wp_nav_menu() arguments.
     * @param int      $depth   Depth of menu item. Used for padding.
     */
    $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
    $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';

    /**
     * Filters the ID applied to a menu item's list item element.
     *
     * @since 3.0.1
     * @since 4.1.0 The `$depth` parameter was added.
     *
     * @param string   $menu_id The ID that is applied to the menu item's `<li>` element.
     * @param WP_Post  $item    The current menu item.
     * @param stdClass $args    An object of wp_nav_menu() arguments.
     * @param int      $depth   Depth of menu item. Used for padding.
     */
    $id = apply_filters( 'nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth );
    $id = $id ? ' id="' . esc_attr( $id ) . '"' : '';

    $output .= $indent . (0 === $depth ? '<div ' : '<li ' ) . $id . $class_names . '>';

    $atts           = array();
    $atts['title']  = ! empty( $item->attr_title ) ? $item->attr_title : '';
    $atts['target'] = ! empty( $item->target ) ? $item->target : '';
    $atts['rel']    = ! empty( $item->xfn ) ? $item->xfn : '';
    $atts['href']   = ! empty( $item->url ) ? $item->url : '';

    /**
     * Filters the HTML attributes applied to a menu item's anchor element.
     *
     * @since 3.6.0
     * @since 4.1.0 The `$depth` parameter was added.
     *
     * @param array $atts {
     *     The HTML attributes applied to the menu item's `<a>` element, empty strings are ignored.
     *
     *     @type string $title  Title attribute.
     *     @type string $target Target attribute.
     *     @type string $rel    The rel attribute.
     *     @type string $href   The href attribute.
     * }
     * @param WP_Post  $item  The current menu item.
     * @param stdClass $args  An object of wp_nav_menu() arguments.
     * @param int      $depth Depth of menu item. Used for padding.
     */
    $atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth );

    $attributes="";
    foreach ( $atts as $attr => $value ) {
        if ( ! empty( $value ) ) {
            $value       = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
            $attributes .= ' ' . $attr . '="' . $value . '"';
        }
    }

    /** This filter is documented in wp-includes/post-template.php */
    $title = apply_filters( 'the_title', $item->title, $item->ID );

    /**
     * Filters a menu item's title.
     *
     * @since 4.4.0
     *
     * @param string   $title The menu item's title.
     * @param WP_Post  $item  The current menu item.
     * @param stdClass $args  An object of wp_nav_menu() arguments.
     * @param int      $depth Depth of menu item. Used for padding.
     */
    $title = apply_filters( 'nav_menu_item_title', $title, $item, $args, $depth );

    $item_output  = $args->before;
    $item_output .= '<a' . $attributes . '>';
    $item_output .= $args->link_before . $title . $args->link_after;
    $item_output .= '</a>';
    $item_output .= $args->after;

    /**
     * Filters a menu item's starting output.
     *
     * The menu item's starting output only includes `$args->before`, the opening `<a>`,
     * the menu item's title, the closing `</a>`, and `$args->after`. Currently, there is
     * no filter for modifying the opening and closing `<li>` for a menu item.
     *
     * @since 3.0.0
     *
     * @param string   $item_output The menu item's starting HTML output.
     * @param WP_Post  $item        Menu item data object.
     * @param int      $depth       Depth of menu item. Used for padding.
     * @param stdClass $args        An object of wp_nav_menu() arguments.
     */
    $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
}

public function end_el( &$output, $item, $depth = 0, $args = [] ) {

    if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
        $t="";
        $n = '';
    } else {
        $t = "\t";
        $n = "\n";
    }
    $output .= (0 === $depth ? "</div>{$n}" : "</li>{$n}");
}

}

I leave it the comments blocks from the original methods, but you get rid of them if you don’t want to have info about the filters.

The class Walker_Nav_Menu is located into wp-includes/class-walker-nav-menu.php.