diff --git a/components/_util/canUseDom.ts b/components/_util/canUseDom.ts
new file mode 100644
index 000000000..39705dc74
--- /dev/null
+++ b/components/_util/canUseDom.ts
@@ -0,0 +1,5 @@
+function canUseDom() {
+  return !!(typeof window !== 'undefined' && window.document && window.document.createElement);
+}
+
+export default canUseDom;
diff --git a/components/_util/hooks/useFlexGapSupport.ts b/components/_util/hooks/useFlexGapSupport.ts
new file mode 100644
index 000000000..eb3c100ec
--- /dev/null
+++ b/components/_util/hooks/useFlexGapSupport.ts
@@ -0,0 +1,11 @@
+import { onMounted, ref } from 'vue';
+import { detectFlexGapSupported } from '../styleChecker';
+
+export default () => {
+  const flexible = ref(false);
+  onMounted(() => {
+    flexible.value = detectFlexGapSupported();
+  });
+
+  return flexible;
+};
diff --git a/components/_util/styleChecker.ts b/components/_util/styleChecker.ts
index 6554faea8..ae845c67e 100644
--- a/components/_util/styleChecker.ts
+++ b/components/_util/styleChecker.ts
@@ -1,5 +1,9 @@
-const isStyleSupport = (styleName: string | Array<string>): boolean => {
-  if (typeof window !== 'undefined' && window.document && window.document.documentElement) {
+import canUseDom from './canUseDom';
+
+export const canUseDocElement = () => canUseDom() && window.document.documentElement;
+
+export const isStyleSupport = (styleName: string | Array<string>): boolean => {
+  if (canUseDocElement()) {
     const styleNameList = Array.isArray(styleName) ? styleName : [styleName];
     const { documentElement } = window.document;
 
@@ -8,6 +12,32 @@ const isStyleSupport = (styleName: string | Array<string>): boolean => {
   return false;
 };
 
-export const isFlexSupported = isStyleSupport(['flex', 'webkitFlex', 'Flex', 'msFlex']);
+let flexGapSupported: boolean | undefined;
+export const detectFlexGapSupported = () => {
+  if (!canUseDocElement()) {
+    return false;
+  }
+
+  if (flexGapSupported !== undefined) {
+    return flexGapSupported;
+  }
+
+  // create flex container with row-gap set
+  const flex = document.createElement('div');
+  flex.style.display = 'flex';
+  flex.style.flexDirection = 'column';
+  flex.style.rowGap = '1px';
+
+  // create two, elements inside it
+  flex.appendChild(document.createElement('div'));
+  flex.appendChild(document.createElement('div'));
+
+  // append to the DOM (needed to obtain scrollHeight)
+  document.body.appendChild(flex);
+  flexGapSupported = flex.scrollHeight === 1; // flex container should be 1px high from the row-gap
+  document.body.removeChild(flex);
+
+  return flexGapSupported;
+};
 
 export default isStyleSupport;
diff --git a/components/grid/Col.tsx b/components/grid/Col.tsx
index d1b0ee783..35ee71d82 100644
--- a/components/grid/Col.tsx
+++ b/components/grid/Col.tsx
@@ -1,8 +1,9 @@
-import { inject, defineComponent, HTMLAttributes, CSSProperties } from 'vue';
+import { inject, defineComponent, CSSProperties, ExtractPropTypes, computed } from 'vue';
 import classNames from '../_util/classNames';
 import PropTypes from '../_util/vue-types';
-import { defaultConfigProvider } from '../config-provider';
 import { rowContextState } from './Row';
+import useConfigInject from '../_util/hooks/useConfigInject';
+import { useInjectRow } from './context';
 
 type ColSpanType = number | string;
 
@@ -16,22 +17,6 @@ export interface ColSize {
   pull?: ColSpanType;
 }
 
-export interface ColProps extends HTMLAttributes {
-  span?: ColSpanType;
-  order?: ColSpanType;
-  offset?: ColSpanType;
-  push?: ColSpanType;
-  pull?: ColSpanType;
-  xs?: ColSpanType | ColSize;
-  sm?: ColSpanType | ColSize;
-  md?: ColSpanType | ColSize;
-  lg?: ColSpanType | ColSize;
-  xl?: ColSpanType | ColSize;
-  xxl?: ColSpanType | ColSize;
-  prefixCls?: string;
-  flex?: FlexType;
-}
-
 function parseFlex(flex: FlexType): string {
   if (typeof flex === 'number') {
     return `${flex} ${flex} auto`;
@@ -44,92 +29,17 @@ function parseFlex(flex: FlexType): string {
   return flex;
 }
 
-const ACol = defineComponent<ColProps>({
-  name: 'ACol',
-  setup(props, { slots }) {
-    const configProvider = inject('configProvider', defaultConfigProvider);
-    const rowContext = inject<rowContextState>('rowContext', {});
-
-    return () => {
-      const { gutter } = rowContext;
-      const { prefixCls: customizePrefixCls, span, order, offset, push, pull, flex } = props;
-      const prefixCls = configProvider.getPrefixCls('col', customizePrefixCls);
-      let sizeClassObj = {};
-      ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'].forEach(size => {
-        let sizeProps: ColSize = {};
-        const propSize = props[size];
-        if (typeof propSize === 'number') {
-          sizeProps.span = propSize;
-        } else if (typeof propSize === 'object') {
-          sizeProps = propSize || {};
-        }
-
-        sizeClassObj = {
-          ...sizeClassObj,
-          [`${prefixCls}-${size}-${sizeProps.span}`]: sizeProps.span !== undefined,
-          [`${prefixCls}-${size}-order-${sizeProps.order}`]:
-            sizeProps.order || sizeProps.order === 0,
-          [`${prefixCls}-${size}-offset-${sizeProps.offset}`]:
-            sizeProps.offset || sizeProps.offset === 0,
-          [`${prefixCls}-${size}-push-${sizeProps.push}`]: sizeProps.push || sizeProps.push === 0,
-          [`${prefixCls}-${size}-pull-${sizeProps.pull}`]: sizeProps.pull || sizeProps.pull === 0,
-        };
-      });
-      const classes = classNames(
-        prefixCls,
-        {
-          [`${prefixCls}-${span}`]: span !== undefined,
-          [`${prefixCls}-order-${order}`]: order,
-          [`${prefixCls}-offset-${offset}`]: offset,
-          [`${prefixCls}-push-${push}`]: push,
-          [`${prefixCls}-pull-${pull}`]: pull,
-        },
-        sizeClassObj,
-      );
-      let mergedStyle: CSSProperties = {};
-      if (gutter) {
-        mergedStyle = {
-          ...(gutter[0] > 0
-            ? {
-                paddingLeft: `${gutter[0] / 2}px`,
-                paddingRight: `${gutter[0] / 2}px`,
-              }
-            : {}),
-          ...(gutter[1] > 0
-            ? {
-                paddingTop: `${gutter[1] / 2}px`,
-                paddingBottom: `${gutter[1] / 2}px`,
-              }
-            : {}),
-          ...mergedStyle,
-        };
-      }
-      if (flex) {
-        mergedStyle.flex = parseFlex(flex);
-      }
-
-      return (
-        <div class={classes} style={mergedStyle}>
-          {slots.default?.()}
-        </div>
-      );
-    };
-  },
-});
-
 const stringOrNumber = PropTypes.oneOfType([PropTypes.string, PropTypes.number]);
-
-export const ColSize = PropTypes.shape({
+export const colSize = PropTypes.shape({
   span: stringOrNumber,
   order: stringOrNumber,
   offset: stringOrNumber,
   push: stringOrNumber,
   pull: stringOrNumber,
 }).loose;
+const objectOrNumber = PropTypes.oneOfType([PropTypes.string, PropTypes.number, colSize]);
 
-const objectOrNumber = PropTypes.oneOfType([PropTypes.string, PropTypes.number, ColSize]);
-
-ACol.props = {
+const colProps = {
   span: stringOrNumber,
   order: stringOrNumber,
   offset: stringOrNumber,
@@ -145,4 +55,85 @@ ACol.props = {
   flex: stringOrNumber,
 };
 
-export default ACol;
+export type ColProps = Partial<ExtractPropTypes<typeof colProps>>;
+
+export default defineComponent({
+  name: 'ACol',
+  props: colProps,
+  setup(props, { slots }) {
+    const { gutter, supportFlexGap, wrap } = useInjectRow();
+    const { prefixCls, direction } = useConfigInject('col', props);
+    const classes = computed(() => {
+      const { span, order, offset, push, pull } = props;
+      const pre = prefixCls.value;
+      let sizeClassObj = {};
+      ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'].forEach(size => {
+        let sizeProps: ColSize = {};
+        const propSize = props[size];
+        if (typeof propSize === 'number') {
+          sizeProps.span = propSize;
+        } else if (typeof propSize === 'object') {
+          sizeProps = propSize || {};
+        }
+
+        sizeClassObj = {
+          ...sizeClassObj,
+          [`${pre}-${size}-${sizeProps.span}`]: sizeProps.span !== undefined,
+          [`${pre}-${size}-order-${sizeProps.order}`]: sizeProps.order || sizeProps.order === 0,
+          [`${pre}-${size}-offset-${sizeProps.offset}`]: sizeProps.offset || sizeProps.offset === 0,
+          [`${pre}-${size}-push-${sizeProps.push}`]: sizeProps.push || sizeProps.push === 0,
+          [`${pre}-${size}-pull-${sizeProps.pull}`]: sizeProps.pull || sizeProps.pull === 0,
+          [`${pre}-rtl`]: direction.value === 'rtl',
+        };
+      });
+      return classNames(
+        pre,
+        {
+          [`${pre}-${span}`]: span !== undefined,
+          [`${pre}-order-${order}`]: order,
+          [`${pre}-offset-${offset}`]: offset,
+          [`${pre}-push-${push}`]: push,
+          [`${pre}-pull-${pull}`]: pull,
+        },
+        sizeClassObj,
+      );
+    });
+
+    const mergedStyle = computed(() => {
+      const { flex } = props;
+      const gutterVal = gutter.value;
+      let style: CSSProperties = {};
+      // Horizontal gutter use padding
+      if (gutterVal && gutterVal[0] > 0) {
+        const horizontalGutter = `${gutterVal[0] / 2}px`;
+        style.paddingLeft = horizontalGutter;
+        style.paddingRight = horizontalGutter;
+      }
+
+      // Vertical gutter use padding when gap not support
+      if (gutterVal && gutterVal[1] > 0 && !supportFlexGap.value) {
+        const verticalGutter = `${gutterVal[1] / 2}px`;
+        style.paddingTop = verticalGutter;
+        style.paddingBottom = verticalGutter;
+      }
+
+      if (flex) {
+        style.flex = parseFlex(flex);
+
+        // Hack for Firefox to avoid size issue
+        // https://github.com/ant-design/ant-design/pull/20023#issuecomment-564389553
+        if (flex === 'auto' && wrap.value === false && !style.minWidth) {
+          style.minWidth = 0;
+        }
+      }
+      return style;
+    });
+    return () => {
+      return (
+        <div class={classes.value} style={mergedStyle.value}>
+          {slots.default?.()}
+        </div>
+      );
+    };
+  },
+});
diff --git a/components/grid/Row.tsx b/components/grid/Row.tsx
index e038db608..3b38b66c1 100644
--- a/components/grid/Row.tsx
+++ b/components/grid/Row.tsx
@@ -1,22 +1,23 @@
 import {
-  inject,
-  provide,
-  reactive,
   defineComponent,
-  HTMLAttributes,
   ref,
   onMounted,
   onBeforeUnmount,
+  ExtractPropTypes,
+  computed,
+  CSSProperties,
 } from 'vue';
 import classNames from '../_util/classNames';
 import { tuple } from '../_util/type';
 import PropTypes from '../_util/vue-types';
-import { defaultConfigProvider } from '../config-provider';
 import ResponsiveObserve, {
   Breakpoint,
   ScreenMap,
   responsiveArray,
 } from '../_util/responsiveObserve';
+import useConfigInject from '../_util/hooks/useConfigInject';
+import useFlexGapSupport from '../_util/hooks/useFlexGapSupport';
+import useProvideRow from './context';
 
 const RowAligns = tuple('top', 'middle', 'bottom', 'stretch');
 const RowJustify = tuple('start', 'end', 'center', 'space-around', 'space-between');
@@ -27,24 +28,36 @@ export interface rowContextState {
   gutter?: [number, number];
 }
 
-export interface RowProps extends HTMLAttributes {
-  type?: 'flex';
-  gutter?: Gutter | [Gutter, Gutter];
-  align?: typeof RowAligns[number];
-  justify?: typeof RowJustify[number];
-  prefixCls?: string;
-}
+const rowProps = {
+  type: PropTypes.oneOf(['flex']),
+  align: PropTypes.oneOf(RowAligns),
+  justify: PropTypes.oneOf(RowJustify),
+  prefixCls: PropTypes.string,
+  gutter: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]).def(0),
+  wrap: PropTypes.looseBool,
+};
 
-const ARow = defineComponent<RowProps>({
+export type RowProps = Partial<ExtractPropTypes<typeof rowProps>>;
+
+const ARow = defineComponent({
   name: 'ARow',
+  props: rowProps,
   setup(props, { slots }) {
-    const rowContext = reactive<rowContextState>({
-      gutter: undefined,
-    });
-    provide('rowContext', rowContext);
+    const { prefixCls, direction } = useConfigInject('row', props);
 
     let token: number;
 
+    const screens = ref<ScreenMap>({
+      xs: true,
+      sm: true,
+      md: true,
+      lg: true,
+      xl: true,
+      xxl: true,
+    });
+
+    const supportFlexGap = useFlexGapSupport();
+
     onMounted(() => {
       token = ResponsiveObserve.subscribe(screen => {
         const currentGutter = props.gutter || 0;
@@ -62,18 +75,7 @@ const ARow = defineComponent<RowProps>({
       ResponsiveObserve.unsubscribe(token);
     });
 
-    const screens = ref<ScreenMap>({
-      xs: true,
-      sm: true,
-      md: true,
-      lg: true,
-      xl: true,
-      xxl: true,
-    });
-
-    const { getPrefixCls } = inject('configProvider', defaultConfigProvider);
-
-    const getGutter = (): [number, number] => {
+    const gutter = computed(() => {
       const results: [number, number] = [0, 0];
       const { gutter = 0 } = props;
       const normalizedGutter = Array.isArray(gutter) ? gutter : [gutter, 0];
@@ -91,34 +93,48 @@ const ARow = defineComponent<RowProps>({
         }
       });
       return results;
-    };
+    });
+
+    useProvideRow({
+      gutter,
+      supportFlexGap,
+      wrap: computed(() => props.wrap),
+    });
+
+    const classes = computed(() =>
+      classNames(prefixCls.value, {
+        [`${prefixCls.value}-no-wrap`]: props.wrap === false,
+        [`${prefixCls.value}-${props.justify}`]: props.justify,
+        [`${prefixCls.value}-${props.align}`]: props.align,
+        [`${prefixCls.value}-rtl`]: direction.value === 'rtl',
+      }),
+    );
+
+    const rowStyle = computed(() => {
+      const gt = gutter.value;
+      // Add gutter related style
+      const style: CSSProperties = {};
+      const horizontalGutter = gt[0] > 0 ? `${gt[0] / -2}px` : undefined;
+      const verticalGutter = gt[1] > 0 ? `${gt[1] / -2}px` : undefined;
+
+      if (horizontalGutter) {
+        style.marginLeft = horizontalGutter;
+        style.marginRight = horizontalGutter;
+      }
+
+      if (supportFlexGap.value) {
+        // Set gap direct if flex gap support
+        style.rowGap = `${gt[1]}px`;
+      } else if (verticalGutter) {
+        style.marginTop = verticalGutter;
+        style.marginBottom = verticalGutter;
+      }
+      return style;
+    });
 
     return () => {
-      const { prefixCls: customizePrefixCls, justify, align } = props;
-      const prefixCls = getPrefixCls('row', customizePrefixCls);
-      const gutter = getGutter();
-      const classes = classNames(prefixCls, {
-        [`${prefixCls}-${justify}`]: justify,
-        [`${prefixCls}-${align}`]: align,
-      });
-      const rowStyle = {
-        ...(gutter[0] > 0
-          ? {
-              marginLeft: `${gutter[0] / -2}px`,
-              marginRight: `${gutter[0] / -2}px`,
-            }
-          : {}),
-        ...(gutter[1] > 0
-          ? {
-              marginTop: `${gutter[1] / -2}px`,
-              marginBottom: `${gutter[1] / -2}px`,
-            }
-          : {}),
-      };
-
-      rowContext.gutter = gutter;
       return (
-        <div class={classes} style={rowStyle}>
+        <div class={classes.value} style={rowStyle.value}>
           {slots.default?.()}
         </div>
       );
@@ -126,12 +142,4 @@ const ARow = defineComponent<RowProps>({
   },
 });
 
-ARow.props = {
-  type: PropTypes.oneOf(['flex']),
-  align: PropTypes.oneOf(RowAligns),
-  justify: PropTypes.oneOf(RowJustify),
-  prefixCls: PropTypes.string,
-  gutter: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]).def(0),
-};
-
 export default ARow;
diff --git a/components/grid/context.ts b/components/grid/context.ts
new file mode 100644
index 000000000..38fb8a174
--- /dev/null
+++ b/components/grid/context.ts
@@ -0,0 +1,20 @@
+import { Ref, inject, InjectionKey, provide, ComputedRef } from 'vue';
+
+export interface RowContext {
+  gutter: ComputedRef<[number, number]>;
+  wrap: ComputedRef<boolean>;
+  supportFlexGap: Ref<boolean>;
+}
+
+export const RowContextKey: InjectionKey<RowContext> = Symbol('rowContextKey');
+
+const useProvideRow = (state: RowContext) => {
+  provide(RowContextKey, state);
+};
+
+const useInjectRow = () => {
+  return inject(RowContextKey);
+};
+
+export { useInjectRow, useProvideRow };
+export default useProvideRow;
diff --git a/components/grid/index.ts b/components/grid/index.ts
index 8b2900f88..d8eddbf16 100644
--- a/components/grid/index.ts
+++ b/components/grid/index.ts
@@ -1,4 +1,11 @@
 import Row from './Row';
 import Col from './Col';
+import useBreakpoint from '../_util/hooks/useBreakpoint';
+
+export { RowProps } from './Row';
+
+export { ColProps, ColSize } from './Col';
 
 export { Row, Col };
+
+export default { useBreakpoint };
diff --git a/components/grid/style/index.less b/components/grid/style/index.less
index 490d959b5..67b091844 100644
--- a/components/grid/style/index.less
+++ b/components/grid/style/index.less
@@ -11,6 +11,11 @@
   &::after {
     display: flex;
   }
+
+  // No wrap of flex
+  &-no-wrap {
+    flex-wrap: nowrap;
+  }
 }
 
 // x轴原点
diff --git a/v2-doc b/v2-doc
index a7013ae87..0f6d531d0 160000
--- a/v2-doc
+++ b/v2-doc
@@ -1 +1 @@
-Subproject commit a7013ae87f69dcbcf547f4b023255b8a7a775557
+Subproject commit 0f6d531d088d5283250c8cec1c7e8be0e0d36a36
diff --git a/v3-changelog.md b/v3-changelog.md
index e69de29bb..cbdd025c7 100644
--- a/v3-changelog.md
+++ b/v3-changelog.md
@@ -0,0 +1,3 @@
+## grid
+
+破坏性更新:row gutter 支持 row-wrap, 无需使用多个 row 划分 col