diff --git a/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx b/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx
index 62b55a7da..0999a4012 100644
--- a/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx
+++ b/client/packages/lowcoder/src/comps/comps/layout/layoutMenuItemComp.tsx
@@ -38,15 +38,13 @@ export class LayoutMenuItemComp extends MultiBaseComp<ChildrenType> {
   }
 
   override getPropertyView(): ReactNode {
-    const isLeaf = this.children.items.getView().length === 0;
     return (
       <>
-        {isLeaf &&
-          this.children.action.propertyView({
-            onAppChange: (label) => {
-              label && this.children.label.dispatchChangeValueAction(label);
-            },
-          })}
+        {this.children.action.propertyView({
+          onAppChange: (label) => {
+            label && this.children.label.dispatchChangeValueAction(label);
+          },
+        })}
         {this.children.label.propertyView({ label: trans("label") })}
         {this.children.icon.propertyView({
           label: trans("icon"),
@@ -98,12 +96,17 @@ const LayoutMenuItemCompMigrate = migrateOldData(LayoutMenuItemComp, (oldData: a
 export class LayoutMenuItemListComp extends list(LayoutMenuItemCompMigrate) {
   addItem(value?: any) {
     const data = this.getView();
+
     this.dispatch(
       this.pushAction(
-        value || {
-          label: trans("menuItem") + " " + (data.length + 1),
-          itemKey: genRandomKey(),
-        }
+        value
+          ? {
+            ...value,
+            itemKey: value.itemKey || genRandomKey(),
+          } : {
+            label: trans("menuItem") + " " + (data.length + 1),
+            itemKey: genRandomKey(),
+          }
       )
     );
   }
diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx
index 5e8d47320..368e459a9 100644
--- a/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx
+++ b/client/packages/lowcoder/src/comps/comps/layout/navLayout.tsx
@@ -1,4 +1,4 @@
-import { Layout, Menu as AntdMenu, MenuProps } from "antd";
+import { Layout, Menu as AntdMenu, MenuProps, Segmented } from "antd";
 import MainContent from "components/layout/MainContent";
 import { LayoutMenuItemComp, LayoutMenuItemListComp } from "comps/comps/layout/layoutMenuItemComp";
 import { menuPropertyView } from "comps/comps/navComp/components/MenuItemList";
@@ -8,12 +8,38 @@ import { withDispatchHook } from "comps/generators/withDispatchHook";
 import { NameAndExposingInfo } from "comps/utils/exposingTypes";
 import { ALL_APPLICATIONS_URL } from "constants/routesURL";
 import { TopHeaderHeight } from "constants/style";
-import { Section } from "lowcoder-design";
+import { Section, controlItem, sectionNames } from "lowcoder-design";
 import { trans } from "i18n";
 import { EditorContainer, EmptyContent } from "pages/common/styledComponent";
 import { useCallback, useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
 import { isUserViewMode, useAppPathParam } from "util/hooks";
+import { StringControl, jsonControl } from "comps/controls/codeControl";
+import { styleControl } from "comps/controls/styleControl";
+import {
+  NavLayoutStyle,
+  NavLayoutItemStyle,
+  NavLayoutItemStyleType,
+  NavLayoutItemHoverStyle,
+  NavLayoutItemHoverStyleType,
+  NavLayoutItemActiveStyle,
+  NavLayoutItemActiveStyleType,
+} from "comps/controls/styleControlConstants";
+import { dropdownControl } from "comps/controls/dropdownControl";
+import _ from "lodash";
+import { check } from "util/convertUtils";
+import { genRandomKey } from "comps/utils/idGenerator";
+import history from "util/history";
+import {
+  DataOption,
+  DataOptionType,
+  ModeOptions,
+  jsonMenuItems,
+  menuItemStyleOptions
+} from "./navLayoutConstants";
+
+const DEFAULT_WIDTH = 240;
+type MenuItemStyleOptionValue = "normal" | "hover" | "active";
 
 const StyledSide = styled(Layout.Sider)`
   max-height: calc(100vh - ${TopHeaderHeight});
@@ -39,22 +65,192 @@ const ContentWrapper = styled.div`
   }
 `;
 
+const StyledMenu = styled(AntdMenu)<{
+  $navItemStyle?: NavLayoutItemStyleType & { width: string},
+  $navItemHoverStyle?: NavLayoutItemHoverStyleType,
+  $navItemActiveStyle?: NavLayoutItemActiveStyleType,
+}>`
+  .ant-menu-item {
+    height: auto;
+    width: ${(props) => props.$navItemStyle?.width};
+    background-color: ${(props) => props.$navItemStyle?.background};
+    color: ${(props) => props.$navItemStyle?.text};
+    border-radius: ${(props) => props.$navItemStyle?.radius} !important;
+    border: ${(props) => `1px solid ${props.$navItemStyle?.border}`};
+    margin: ${(props) => props.$navItemStyle?.margin};
+    padding: ${(props) => props.$navItemStyle?.padding};
+
+  }
+  .ant-menu-item-active {
+    background-color: ${(props) => props.$navItemHoverStyle?.background} !important;
+    color: ${(props) => props.$navItemHoverStyle?.text} !important;
+    border: ${(props) => `1px solid ${props.$navItemHoverStyle?.border}`};
+  }
+
+  .ant-menu-item-selected {
+    background-color: ${(props) => props.$navItemActiveStyle?.background} !important;
+    color: ${(props) => props.$navItemActiveStyle?.text} !important;
+    border: ${(props) => `1px solid ${props.$navItemActiveStyle?.border}`};
+  }
+
+  .ant-menu-submenu {
+    margin: ${(props) => props.$navItemStyle?.margin};
+    width: ${(props) => props.$navItemStyle?.width};
+
+    .ant-menu-submenu-title {
+      width: 100%;
+      height: auto !important;
+      background-color: ${(props) => props.$navItemStyle?.background};
+      color: ${(props) => props.$navItemStyle?.text};
+      border-radius: ${(props) => props.$navItemStyle?.radius} !important;
+      border: ${(props) => `1px solid ${props.$navItemStyle?.border}`};
+      margin: 0;
+      padding: ${(props) => props.$navItemStyle?.padding};
+
+    }
+
+    .ant-menu-item {
+      width: 100%;
+    }
+
+    &.ant-menu-submenu-active {
+      >.ant-menu-submenu-title {
+        width: 100%;
+        background-color: ${(props) => props.$navItemHoverStyle?.background} !important;
+        color: ${(props) => props.$navItemHoverStyle?.text} !important;
+        border: ${(props) => `1px solid ${props.$navItemHoverStyle?.border}`};
+      }
+    }
+    &.ant-menu-submenu-selected {
+      >.ant-menu-submenu-title {
+        width: 100%;
+        background-color: ${(props) => props.$navItemActiveStyle?.background} !important;
+        color: ${(props) => props.$navItemActiveStyle?.text} !important;
+        border: ${(props) => `1px solid ${props.$navItemActiveStyle?.border}`};
+      }
+    }
+  }
+
+`;
+
+const StyledImage = styled.img`
+  height: 1em;
+  color: currentColor;
+`;
+
+const defaultStyle = {
+  radius: '0px',
+  margin: '0px',
+  padding: '0px',
+}
+
+type UrlActionType = {
+  url?: string;
+  newTab?: boolean;
+}
+
+export type MenuItemNode = {
+  label: string;
+  key: string;
+  hidden?: boolean;
+  icon?: any;
+  action?: UrlActionType,
+  children?: MenuItemNode[];
+}
+
+function checkDataNodes(value: any, key?: string): MenuItemNode[] | undefined {
+  return check(value, ["array", "undefined"], key, (node, k) => {
+    check(node, ["object"], k);
+    check(node["label"], ["string"], "label");
+    check(node["hidden"], ["boolean", "undefined"], "hidden");
+    check(node["icon"], ["string", "undefined"], "icon");
+    check(node["action"], ["object", "undefined"], "action");
+    checkDataNodes(node["children"], "children");
+    return node;
+  });
+}
+
+function convertTreeData(data: any) {
+  return data === "" ? [] : checkDataNodes(data) ?? [];
+}
+
 let NavTmpLayout = (function () {
   const childrenMap = {
+    dataOptionType: dropdownControl(DataOptionType, DataOption.Manual),
     items: withDefault(LayoutMenuItemListComp, [
       {
         label: trans("menuItem") + " 1",
+        itemKey: genRandomKey(),
       },
     ]),
+    jsonItems: jsonControl(convertTreeData, jsonMenuItems),
+    width: withDefault(StringControl, DEFAULT_WIDTH),
+    backgroundImage: withDefault(StringControl, ""),
+    mode: dropdownControl(ModeOptions, "inline"),
+    navStyle: withDefault(styleControl(NavLayoutStyle), defaultStyle),
+    navItemStyle: withDefault(styleControl(NavLayoutItemStyle), defaultStyle),
+    navItemHoverStyle: withDefault(styleControl(NavLayoutItemHoverStyle), {}),
+    navItemActiveStyle: withDefault(styleControl(NavLayoutItemActiveStyle), {}),
   };
   return new MultiCompBuilder(childrenMap, (props) => {
     return null;
   })
     .setPropertyViewFn((children) => {
+      const [styleSegment, setStyleSegment] = useState('normal')
+
       return (
-        <>
-          <Section name={trans("menu")}>{menuPropertyView(children.items)}</Section>
-        </>
+        <div style={{overflowY: 'auto'}}>
+          <Section name={trans("menu")}>
+            {children.dataOptionType.propertyView({
+              radioButton: true,
+              type: "oneline",
+            })}
+            {
+              children.dataOptionType.getView() === DataOption.Manual
+                ? menuPropertyView(children.items)
+                : children.jsonItems.propertyView({
+                  label: "Json Data",
+                })
+            }
+          </Section>
+          <Section name={sectionNames.layout}>
+            { children.width.propertyView({
+                label: trans("navLayout.width"),
+                tooltip: trans("navLayout.widthTooltip"),
+                placeholder: DEFAULT_WIDTH + "",
+            })}
+            { children.mode.propertyView({
+              label: trans("labelProp.position"),
+              radioButton: true
+            })}
+            {children.backgroundImage.propertyView({
+              label: `Background Image`,
+              placeholder: 'https://temp.im/350x400',
+            })}
+          </Section>
+          <Section name={trans("navLayout.navStyle")}>
+            { children.navStyle.getPropertyView() }
+          </Section>
+          <Section name={trans("navLayout.navItemStyle")}>
+            {controlItem({}, (
+              <Segmented
+                block
+                options={menuItemStyleOptions}
+                value={styleSegment}
+                onChange={(k) => setStyleSegment(k as MenuItemStyleOptionValue)}
+              />
+            ))}
+            {styleSegment === 'normal' && (
+              children.navItemStyle.getPropertyView()
+            )}
+            {styleSegment === 'hover' && (
+              children.navItemHoverStyle.getPropertyView()
+            )}
+            {styleSegment === 'active' && (
+              children.navItemActiveStyle.getPropertyView()
+            )}
+          </Section>
+        </div>
       );
     })
     .build();
@@ -64,13 +260,98 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => {
   const pathParam = useAppPathParam();
   const isViewMode = isUserViewMode(pathParam);
   const [selectedKey, setSelectedKey] = useState("");
-  const items = useMemo(() => comp.children.items.getView(), [comp.children.items]);
-
+  const items = comp.children.items.getView();
+  const navWidth = comp.children.width.getView();
+  const navMode = comp.children.mode.getView();
+  const navStyle = comp.children.navStyle.getView();
+  const navItemStyle = comp.children.navItemStyle.getView();
+  const navItemHoverStyle = comp.children.navItemHoverStyle.getView();
+  const navItemActiveStyle = comp.children.navItemActiveStyle.getView();
+  const backgroundImage = comp.children.backgroundImage.getView();
+  const jsonItems = comp.children.jsonItems.getView();
+  const dataOptionType = comp.children.dataOptionType.getView();
+  
   // filter out hidden. unauthorised items filtered by server
   const filterItem = useCallback((item: LayoutMenuItemComp): boolean => {
     return !item.children.hidden.getView();
   }, []);
 
+  const generateItemKeyRecord = useCallback(
+    (items: LayoutMenuItemComp[] | MenuItemNode[]) => {
+      const result: Record<string, LayoutMenuItemComp | MenuItemNode> = {};
+      if(dataOptionType === DataOption.Manual) {
+        (items as LayoutMenuItemComp[])?.forEach((item) => {
+          const subItems = item.children.items.getView();
+          if (subItems.length > 0) {
+            Object.assign(result, generateItemKeyRecord(subItems))
+          }
+          result[item.getItemKey()] = item;
+        });
+      }
+      if(dataOptionType === DataOption.Json) {
+        (items as MenuItemNode[])?.forEach((item) => {
+          if (item.children?.length) {
+            Object.assign(result, generateItemKeyRecord(item.children))
+          }
+          result[item.key] = item;
+        })
+      }
+      return result;
+    }, [dataOptionType]
+  )
+
+  const itemKeyRecord = useMemo(() => {
+    if(dataOptionType === DataOption.Json) {
+      return generateItemKeyRecord(jsonItems)
+    }
+    return generateItemKeyRecord(items)
+  }, [dataOptionType, jsonItems, items, generateItemKeyRecord]);
+
+  const onMenuItemClick = useCallback(({key}: {key: string}) => {
+    const itemComp = itemKeyRecord[key]
+  
+    const url = [
+      ALL_APPLICATIONS_URL,
+      pathParam.applicationId,
+      pathParam.viewMode,
+      key,
+    ].join("/");
+
+    // handle manual menu item action
+    if(dataOptionType === DataOption.Manual) {
+      (itemComp as LayoutMenuItemComp).children.action.act(url);
+      return;
+    }
+    // handle json menu item action
+    if((itemComp as MenuItemNode).action?.newTab) {
+      return window.open((itemComp as MenuItemNode).action?.url, '_blank')
+    }
+    history.push(url);
+  }, [pathParam.applicationId, pathParam.viewMode, dataOptionType, itemKeyRecord])
+
+  const getJsonMenuItem = useCallback(
+    (items: MenuItemNode[]): MenuProps["items"] => {
+      return items?.map((item: MenuItemNode) => {
+        const {
+          label,
+          key,
+          hidden,
+          icon,
+          children,
+        } = item;
+        return {
+          label,
+          key,
+          hidden,
+          icon: <StyledImage src={icon} />,
+          onTitleClick: onMenuItemClick,
+          onClick: onMenuItemClick,
+          ...(children?.length && { children: getJsonMenuItem(children) }),
+        }
+      })
+    }, [onMenuItemClick]
+  )
+
   const getMenuItem = useCallback(
     (itemComps: LayoutMenuItemComp[]): MenuProps["items"] => {
       return itemComps.filter(filterItem).map((item) => {
@@ -81,14 +362,20 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => {
           title: label,
           key: item.getItemKey(),
           icon: <span>{item.children.icon.getView()}</span>,
+          onTitleClick: onMenuItemClick,
+          onClick: onMenuItemClick,
           ...(subItems.length > 0 && { children: getMenuItem(subItems) }),
         };
       });
     },
-    [filterItem]
+    [onMenuItemClick, filterItem]
   );
 
-  const menuItems = useMemo(() => getMenuItem(items), [items, getMenuItem]);
+  const menuItems = useMemo(() => {
+    if(dataOptionType === DataOption.Json) return getJsonMenuItem(jsonItems)
+
+    return getMenuItem(items)
+  }, [dataOptionType, jsonItems, getJsonMenuItem, items, getMenuItem]);
 
   // Find by path itemKey
   const findItemPathByKey = useCallback(
@@ -134,22 +421,60 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => {
     [filterItem]
   );
 
-  const itemKeyRecord = useMemo(() => {
-    const result: Record<string, LayoutMenuItemComp> = {};
-    items.forEach((item) => {
-      const subItems = item.children.items.getView();
-      if (subItems.length > 0) {
-        item.children.items
-          .getView()
-          .forEach((subItem) => (result[subItem.getItemKey()] = subItem));
-      } else {
-        result[item.getItemKey()] = item;
+  // Find by path itemKey
+  const findItemPathByKeyJson = useCallback(
+    (itemComps: MenuItemNode[], itemKey: string): string[] => {
+      for (let item of itemComps) {
+        const subItems = item.children;
+        if (subItems?.length) {
+          // have subMenus
+          const childPath = findItemPathByKeyJson(subItems, itemKey);
+          if (childPath.length > 0) {
+            return [item.key, ...childPath];
+          }
+        } else {
+          if (item.key === itemKey) {
+            return [item.key];
+          }
+        }
+      }
+      return [];
+    },
+    []
+  );
+
+  // Get the first visible menu
+  const findFirstItemPathJson = useCallback(
+    (itemComps: MenuItemNode[]): string[] => {
+      for (let item of itemComps) {
+        if (!item.hidden) {
+          const subItems = item.children;
+          if (subItems?.length) {
+            // have subMenus
+            const childPath = findFirstItemPathJson(subItems);
+            if (childPath.length > 0) {
+              return [item.key, ...childPath];
+            }
+          } else {
+            return [item.key];
+          }
+        }
       }
-    });
-    return result;
-  }, [items]);
+      return [];
+    }, []
+  );
 
   const defaultOpenKeys = useMemo(() => {
+    if(dataOptionType === DataOption.Json) {
+      let itemPath: string[];
+      if (pathParam.appPageId) {
+        itemPath = findItemPathByKeyJson(jsonItems, pathParam.appPageId);
+      } else {
+        itemPath = findFirstItemPathJson(jsonItems);
+      }
+      return itemPath.slice(0, itemPath.length - 1);
+    }
+
     let itemPath: string[];
     if (pathParam.appPageId) {
       itemPath = findItemPathByKey(items, pathParam.appPageId);
@@ -170,34 +495,79 @@ NavTmpLayout = withViewFn(NavTmpLayout, (comp) => {
     setSelectedKey(selectedKey);
   }, [pathParam.appPageId]);
 
-  let pageView = <EmptyContent text="" style={{ height: "100%" }} />;
-  const selectedItem = itemKeyRecord[selectedKey];
-  if (selectedItem && !selectedItem.children.hidden.getView()) {
-    const compView = selectedItem.children.action.getView();
-    if (compView) {
-      pageView = compView;
+  const pageView = useMemo(() => {
+    let pageView = <EmptyContent text="" style={{ height: "100%" }} />;
+    
+    if(dataOptionType === DataOption.Manual) {
+      const selectedItem = (itemKeyRecord[selectedKey] as LayoutMenuItemComp);
+      if (selectedItem && !selectedItem.children.hidden.getView()) {
+        const compView = selectedItem.children.action.getView();
+        if (compView) {
+          pageView = compView;
+        }
+      }
+    }
+    if(dataOptionType === DataOption.Json) {
+      const item = (itemKeyRecord[selectedKey] as MenuItemNode)
+      if(item?.action?.url) {
+        pageView =  <iframe
+          title={item?.action?.url}
+          src={item?.action?.url}
+          width="100%"
+          height="100%"
+          style={{ border: "none", marginBottom: "-6px" }}
+        />
+      }
     }
+    return pageView;
+  }, [dataOptionType, itemKeyRecord, selectedKey])
+
+  const getVerticalMargin = (margin: string[]) => {
+    if(margin.length === 1) return `${margin[0]}`;
+    if(margin.length === 2) return `(${margin[0]} + ${margin[0]})`;
+    if(margin.length === 3 || margin.length === 4)
+      return `(${margin[0]} + ${margin[2]})`;
+
+    return '0px';
+  }
+  const getHorizontalMargin = (margin: string[]) => {
+    if(margin.length === 1) return `(${margin[0]} + ${margin[0]})`;
+    if(margin.length === 2) return `(${margin[1]} + ${margin[1]})`;
+    if(margin.length === 3 || margin.length === 4)
+      return `(${margin[1]} + ${margin[3]})`;
+
+    return '0px';
+  }
+
+  let backgroundStyle = navStyle.background;
+  if(!_.isEmpty(backgroundImage))  {
+    backgroundStyle = `center / cover url('${backgroundImage}') no-repeat, ${backgroundStyle}`;
   }
 
   let content = (
     <Layout>
-      <StyledSide theme="light" width={240}>
-        <AntdMenu
+      <StyledSide theme="light" width={navWidth}>
+        <StyledMenu
           items={menuItems}
-          mode="inline"
-          style={{ height: "100%" }}
+          mode={navMode}
+          style={{
+            height: `calc(100% - ${getVerticalMargin(navStyle.margin.split(' '))})`,
+            width: `calc(100% - ${getHorizontalMargin(navStyle.margin.split(' '))})`,
+            borderRight: `1px solid ${navStyle.border}`,
+            borderRadius: navStyle.radius,
+            color: navStyle.text,
+            margin: navStyle.margin,
+            padding: navStyle.padding,
+            background: backgroundStyle,
+          }}
           defaultOpenKeys={defaultOpenKeys}
           selectedKeys={[selectedKey]}
-          onClick={(e) => {
-            const itemComp = itemKeyRecord[e.key];
-            const url = [
-              ALL_APPLICATIONS_URL,
-              pathParam.applicationId,
-              pathParam.viewMode,
-              itemComp.getItemKey(),
-            ].join("/");
-            itemComp.children.action.act(url);
+          $navItemStyle={{
+            width: `calc(100% - ${getHorizontalMargin(navItemStyle.margin.split(' '))})`,
+            ...navItemStyle,
           }}
+          $navItemHoverStyle={navItemHoverStyle}
+          $navItemActiveStyle={navItemActiveStyle}
         />
       </StyledSide>
       <MainContent>{pageView}</MainContent>
diff --git a/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts b/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts
new file mode 100644
index 000000000..e8fc23c0b
--- /dev/null
+++ b/client/packages/lowcoder/src/comps/comps/layout/navLayoutConstants.ts
@@ -0,0 +1,77 @@
+import { trans } from "i18n";
+
+export const ModeOptions = [
+  { label: trans("navLayout.modeInline"), value: "inline" },
+  { label: trans("navLayout.modeVertical"), value: "vertical" },
+] as const;
+
+export const DataOption = {
+  Manual: 'manual',
+  Json: 'json',
+}
+export const DataOptionType = [
+  {
+    label: trans("prop.manual"),
+    value: DataOption.Manual,
+  },
+  {
+    label: trans("prop.json"),
+    value: DataOption.Json,
+  },
+];
+
+export const menuItemStyleOptions = [
+  {
+    value: "normal",
+    label: "Normal",
+  },
+  {
+    value: "hover",
+    label: "Hover",
+  },
+  {
+    value: "active",
+    label: "Active",
+  }
+];
+
+export const jsonMenuItems = [
+  {
+    label: "Menu Item 1",
+    key: 'menu-item-1',
+    icon: "https://cdn-icons-png.flaticon.com/128/149/149338.png",
+    action: {
+      url: "https://www.lowcoder.cloud",
+      newTab: false,
+    },
+    children: [
+      {
+        label: "Submenu Item 1",
+        key: 'submenu-item-11',
+        icon: "",
+        action: {
+          url: "https://www.lowcoder.cloud",
+          newTab: false,
+        },
+      },
+      {
+        label: "Submenu Item 2",
+        key: 'submenu-item-12',
+        icon: "",
+        action: {
+          url: "https://www.lowcoder.cloud",
+          newTab: false,
+        },
+      },
+    ]
+  },
+  {
+    label: "Menu Item 2",
+    key: 'menu-item-2',
+    icon: "https://cdn-icons-png.flaticon.com/128/149/149206.png",
+    action: {
+      url: "https://www.lowcoder.cloud",
+      newTab: true,
+    },
+  }
+]
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx b/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx
index 44c342101..c4f22191a 100644
--- a/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx
+++ b/client/packages/lowcoder/src/comps/comps/navComp/components/DroppableMenuItem.tsx
@@ -1,10 +1,12 @@
 import { useDraggable, useDroppable } from "@dnd-kit/core";
 import { trans } from "i18n";
-import { Fragment } from "react";
+import { Fragment, useEffect } from "react";
 import styled from "styled-components";
 import DroppablePlaceholder from "./DroppablePlaceHolder";
 import MenuItem, { ICommonItemProps } from "./MenuItem";
 import { IDragData, IDropData } from "./types";
+import { LayoutMenuItemComp } from "comps/comps/layout/layoutMenuItemComp";
+import { genRandomKey } from "comps/utils/idGenerator";
 
 const DraggableMenuItemWrapper = styled.div`
   position: relative;
@@ -63,6 +65,22 @@ export default function DraggableMenuItem(props: IDraggableMenuItemProps) {
     disabled: isDragging || disabled || disableDropIn,
     data: dropData,
   });
+
+  // TODO: Remove this later.
+  // Set ItemKey for previously added sub-menus
+  useEffect(() => {
+    if(!items.length) return;
+    if(!(items[0] instanceof LayoutMenuItemComp)) return;
+
+    return items.forEach(item  => {
+      const subItem = item as LayoutMenuItemComp;
+      const itemKey = subItem.children.itemKey.getView();
+      if(itemKey === '') {
+        subItem.children.itemKey.dispatchChangeValueAction(genRandomKey())
+      }
+    })
+  }, [items])
+
   return (
     <>
       <DraggableMenuItemWrapper>
@@ -99,7 +117,7 @@ export default function DraggableMenuItem(props: IDraggableMenuItemProps) {
                 item={subItem}
                 level={0}
                 disabled={disabled || isDragging || disableDropIn}
-                // onAddSubMenu={onAddSubMenu}
+                onAddSubMenu={onAddSubMenu}
                 onDelete={onDelete}
                 parentDragging={isDragging}
               />
diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx
index 0344f21c9..58b706771 100644
--- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx
+++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx
@@ -821,7 +821,7 @@ export const TreeStyle = [
 
 export const TreeSelectStyle = [...multiSelectCommon, ...ACCENT_VALIDATE] as const;
 
-export const DrawerStyle = [getBackground()] as const;
+export const DrawerStyle = [getBackground()] as const
 
 export const JsonEditorStyle = [LABEL] as const;
 
@@ -928,6 +928,59 @@ export const ResponsiveLayoutColStyle = [
   PADDING,
 ] as const;
 
+export const NavLayoutStyle = [
+  ...getBgBorderRadiusByBg(),
+  {
+    name: "text",
+    label: trans("text"),
+    depName: "background",
+    // depTheme: "primary",
+    depType: DEP_TYPE.CONTRAST_TEXT,
+    transformer: contrastText,
+  },
+  MARGIN,
+  PADDING,
+] as const;
+
+export const NavLayoutItemStyle = [
+  getBackground("primarySurface"),
+  getStaticBorder('transparent'),
+  RADIUS,
+  {
+    name: "text",
+    label: trans("text"),
+    depName: "background",
+    depType: DEP_TYPE.CONTRAST_TEXT,
+    transformer: contrastText,
+  },
+  MARGIN,
+  PADDING,
+] as const;
+
+export const NavLayoutItemHoverStyle = [
+  getBackground("canvas"),
+  getStaticBorder('transparent'),
+  {
+    name: "text",
+    label: trans("text"),
+    depName: "background",
+    depType: DEP_TYPE.CONTRAST_TEXT,
+    transformer: contrastText,
+  },
+] as const;
+
+export const NavLayoutItemActiveStyle = [
+  getBackground("primary"),
+  getStaticBorder('transparent'),
+  {
+    name: "text",
+    label: trans("text"),
+    depName: "background",
+    depType: DEP_TYPE.CONTRAST_TEXT,
+    transformer: contrastText,
+  },
+] as const;
+
 export const CarouselStyle = [getBackground("canvas")] as const;
 
 export const RichTextEditorStyle = [getStaticBorder(), RADIUS] as const;
@@ -968,6 +1021,10 @@ export type CarouselStyleType = StyleConfigType<typeof CarouselStyle>;
 export type RichTextEditorStyleType = StyleConfigType<typeof RichTextEditorStyle>;
 export type ResponsiveLayoutRowStyleType = StyleConfigType<typeof ResponsiveLayoutRowStyle>;
 export type ResponsiveLayoutColStyleType = StyleConfigType<typeof ResponsiveLayoutColStyle>;
+export type NavLayoutStyleType = StyleConfigType<typeof NavLayoutStyle>;
+export type NavLayoutItemStyleType = StyleConfigType<typeof NavLayoutItemStyle>;
+export type NavLayoutItemHoverStyleType = StyleConfigType<typeof NavLayoutItemHoverStyle>;
+export type NavLayoutItemActiveStyleType = StyleConfigType<typeof NavLayoutItemActiveStyle>;
 
 export function widthCalculator(margin: string) {
   const marginArr = margin?.trim().replace(/\s+/g,' ').split(" ") || "";
diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts
index 4207f3ac6..ade1d8355 100644
--- a/client/packages/lowcoder/src/i18n/locales/en.ts
+++ b/client/packages/lowcoder/src/i18n/locales/en.ts
@@ -177,6 +177,7 @@ export const en = {
       "If the result is non-empty string, it is an error message. If empty or null, the validation passes. Example: ",
     manual: "Manual",
     map: "Mapped",
+    json: "JSON",
     use12Hours: "Use 12-hours",
     hourStep: "Hour step",
     minuteStep: "Minute step",
@@ -2713,4 +2714,13 @@ export const en = {
     rowLayout: "Row Layout",
     columnsLayout: "Columns Layout",
   },
+  navLayout: {
+    mode: "Mode",
+    modeInline: "Inline",
+    modeVertical: "Vertical",
+    width: "Width",
+    widthTooltip: "Number or percentage, e.g. 520, 60%",
+    navStyle: "Menu Style",
+    navItemStyle: "Menu Item Style",
+  }
 };
diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts
index 5d10e78f1..6b97f4805 100644
--- a/client/packages/lowcoder/src/i18n/locales/zh.ts
+++ b/client/packages/lowcoder/src/i18n/locales/zh.ts
@@ -167,6 +167,7 @@ prop: {
     customRuleTooltip: "如果结果是非空字符串,则为错误消息.如果为空或null,则验证通过.\n示例:",
     manual: "手动",
     map: "映射",
+    json: "JSON",
     use12Hours: "使用12小时制",
     hourStep: "小时步长",
     minuteStep: "分钟步长",
@@ -2559,6 +2560,15 @@ timeLine: {
         matchColumnsHeight: "匹配列高度",
         rowLayout: "行布局",
         columnsLayout: "栏目布局",
+    },
+    navLayout: {
+        mode: "模式",
+        modeInline: "排队",
+        modeVertical: "垂直的",
+        width: "宽度",
+        widthTooltip: "数字或百分比,例如 520,60%",
+        navStyle: "菜单风格",
+        navItemStyle: "菜单项样式",
     }
 };