Repository: gregberge/svgr (ts)

Issue:
Add desc prop to svg
<!-- Love SVGR? Please consider supporting our collective: 👉  https://opencollective.com/svgr/donate -->

## 🚀 Feature Proposal

Just as titleProp allows the addition of a custom title via props, let's also make a custom desc (and its id) available as well! This will make it easier to build in accessibility into our svg components.

## Motivation

I was looking at the w3 guidelines for svgs and it was recommended to have a title and description so that users can understand the meaning behind an svg.

## Example

In code, something like:

Take this svg (taken from [here](https://react-svgr.com/docs/what-is-svgr/)):
```
<?xml version="1.0" encoding="UTF-8"?>
<svg
  width="48px"
  height="1px"
  viewBox="0 0 48 1"
  version="1.1"
  xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink"
>
  <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
  <title>Rectangle 5</title>
  <desc>Created with Sketch.</desc>
  <defs></defs>
  <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
    <g
      id="19-Separator"
      transform="translate(-129.000000, -156.000000)"
      fill="#063855"
    >
      <g id="Controls/Settings" transform="translate(80.000000, 0.000000)">
        <g id="Content" transform="translate(0.000000, 64.000000)">
          <g id="Group" transform="translate(24.000000, 56.000000)">
            <g id="Group-2">
              <rect id="Rectangle-5" x="25" y="36" width="48" height="1"></rect>
            </g>
          </g>
        </g>
      </g>
    </g>
  </g>
</svg>
```

And do something like:
```
import * as React from 'react'

const SvgComponent = (props) => (
  <svg width="1em" height="1em" viewBox="0 0 48 1" {...props}>
    <desc id={props.descId}>{props.desc}</desc>
    <path d="M0 0h48v1H0z" fill="currentColor" fillRule="evenodd" />
  </svg>
)

export default SvgComponent
```

## Pitch

Making SVGs accessible benefits everyone and this will make it a lot easier to create accessible svgs.

Pull request:
feat(descProp): add descProp option
<!-- Thanks for submitting a pull request! Please provide enough information so that others can review your pull request. The two fields below are mandatory. -->

## Summary

Fixes #714 

- Updated README.md and src files in `babel-plugin-svg-dynamic-title` to add a descProp
  - the plugin now takes in a `tag` option indicating the attribute/element to add to the svg ('title' for title, or 'desc' for desc)
- Updated `babel-plugin-transform-svg-component` to add additional desc and descId attributes to the svg if descProp is true
- Updated `babel-preset` to add the `aria-describedby` attribute
- Added descProp to various options, cli-config, etc.
- Updated documentation to include a Description section

## Test plan

<!-- Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. -->

Simply `npm run test`! Existing tests have been slightly modified. New tests have been added to test the new feature.


Patch:
diff --git a/packages/babel-plugin-svg-dynamic-title/README.md b/packages/babel-plugin-svg-dynamic-title/README.md
index efe47ac..904db85 100644
--- a/packages/babel-plugin-svg-dynamic-title/README.md
+++ b/packages/babel-plugin-svg-dynamic-title/README.md
@@ -16,6 +16,10 @@ npm install --save-dev @svgr/babel-plugin-svg-dynamic-title
 }
 ```
 
+## Note
+
+This plugin handles both the titleProp and descProp options. By default, it will handle titleProp only.
+
 ## License
 
 MIT
diff --git a/packages/babel-plugin-svg-dynamic-title/src/index.ts b/packages/babel-plugin-svg-dynamic-title/src/index.ts
index 6673f51..220c596 100644
--- a/packages/babel-plugin-svg-dynamic-title/src/index.ts
+++ b/packages/babel-plugin-svg-dynamic-title/src/index.ts
@@ -3,25 +3,37 @@ import { NodePath, types as t } from '@babel/core'
 
 const elements = ['svg', 'Svg']
 
-const createTitleElement = (
+type tag = 'title' | 'desc'
+
+export interface Options {
+  tag: tag | null
+}
+
+interface State {
+  opts: Options
+}
+
+const createTagElement = (
+  tag: tag,
   children: t.JSXExpressionContainer[] = [],
   attributes: (t.JSXAttribute | t.JSXSpreadAttribute)[] = [],
 ) => {
-  const title = t.jsxIdentifier('title')
+  const eleName = t.jsxIdentifier(tag)
   return t.jsxElement(
-    t.jsxOpeningElement(title, attributes),
-    t.jsxClosingElement(title),
+    t.jsxOpeningElement(eleName, attributes),
+    t.jsxClosingElement(eleName),
     children,
   )
 }
 
-const createTitleIdAttribute = () =>
+const createTagIdAttribute = (tag: tag) =>
   t.jsxAttribute(
     t.jsxIdentifier('id'),
-    t.jsxExpressionContainer(t.identifier('titleId')),
+    t.jsxExpressionContainer(t.identifier(`${tag}Id`)),
   )
 
-const addTitleIdAttribute = (
+const addTagIdAttribute = (
+  tag: tag,
   attributes: (t.JSXAttribute | t.JSXSpreadAttribute)[],
 ) => {
   const existingId = attributes.find(
@@ -29,19 +41,20 @@ const addTitleIdAttribute = (
   ) as t.JSXAttribute | undefined
 
   if (!existingId) {
-    return [...attributes, createTitleIdAttribute()]
+    return [...attributes, createTagIdAttribute(tag)]
   }
   existingId.value = t.jsxExpressionContainer(
     t.isStringLiteral(existingId.value)
-      ? t.logicalExpression('||', t.identifier('titleId'), existingId.value)
-      : t.identifier('titleId'),
+      ? t.logicalExpression('||', t.identifier(`${tag}Id`), existingId.value)
+      : t.identifier(`${tag}Id`),
   )
   return attributes
 }
 
 const plugin = () => ({
   visitor: {
-    JSXElement(path: NodePath<t.JSXElement>) {
+    JSXElement(path: NodePath<t.JSXElement>, state: State) {
+      const tag = state.opts.tag || 'title'
       if (!elements.length) return
 
       const openingElement = path.get('openingElement')
@@ -54,22 +67,24 @@ const plugin = () => ({
         return
       }
 
-      const getTitleElement = (
+      const getTagElement = (
         existingTitle?: t.JSXElement,
       ): t.JSXExpressionContainer => {
-        const titleExpression = t.identifier('title')
+        const tagExpression = t.identifier(tag)
         if (existingTitle) {
-          existingTitle.openingElement.attributes = addTitleIdAttribute(
+          existingTitle.openingElement.attributes = addTagIdAttribute(
+            tag,
             existingTitle.openingElement.attributes,
           )
         }
         const conditionalTitle = t.conditionalExpression(
-          titleExpression,
-          createTitleElement(
-            [t.jsxExpressionContainer(titleExpression)],
+          tagExpression,
+          createTagElement(
+            tag,
+            [t.jsxExpressionContainer(tagExpression)],
             existingTitle
               ? existingTitle.openingElement.attributes
-              : [createTitleIdAttribute()],
+              : [createTagIdAttribute(tag)],
           ),
           t.nullLiteral(),
         )
@@ -80,7 +95,7 @@ const plugin = () => ({
             t.conditionalExpression(
               t.binaryExpression(
                 '===',
-                titleExpression,
+                tagExpression,
                 t.identifier('undefined'),
               ),
               existingTitle,
@@ -92,25 +107,25 @@ const plugin = () => ({
       }
 
       // store the title element
-      let titleElement: t.JSXExpressionContainer | null = null
+      let tagElement: t.JSXExpressionContainer | null = null
 
       const hasTitle = path.get('children').some((childPath) => {
-        if (childPath.node === titleElement) return false
+        if (childPath.node === tagElement) return false
         if (!childPath.isJSXElement()) return false
         const name = childPath.get('openingElement').get('name')
         if (!name.isJSXIdentifier()) return false
-        if (name.node.name !== 'title') return false
-        titleElement = getTitleElement(childPath.node)
-        childPath.replaceWith(titleElement)
+        if (name.node.name !== tag) return false
+        tagElement = getTagElement(childPath.node)
+        childPath.replaceWith(tagElement)
         return true
       })
 
       // create a title element if not already create
-      titleElement = titleElement || getTitleElement()
+      tagElement = tagElement || getTagElement()
       if (!hasTitle) {
         // path.unshiftContainer is not working well :(
         // path.unshiftContainer('children', titleElement)
-        path.node.children.unshift(titleElement)
+        path.node.children.unshift(tagElement)
         path.replaceWith(path.node)
       }
     },
diff --git a/packages/babel-plugin-transform-svg-component/src/types.ts b/packages/babel-plugin-transform-svg-component/src/types.ts
index 8c1c665..c171a5b 100644
--- a/packages/babel-plugin-transform-svg-component/src/types.ts
+++ b/packages/babel-plugin-transform-svg-component/src/types.ts
@@ -35,6 +35,7 @@ export interface JSXRuntimeImport {
 export interface Options {
   typescript?: boolean
   titleProp?: boolean
+  descProp?: boolean
   expandProps?: boolean | 'start' | 'end'
   ref?: boolean
   template?: Template
diff --git a/packages/babel-plugin-transform-svg-component/src/variables.ts b/packages/babel-plugin-transform-svg-component/src/variables.ts
index 844e638..64a1481 100644
--- a/packages/babel-plugin-transform-svg-component/src/variables.ts
+++ b/packages/babel-plugin-transform-svg-component/src/variables.ts
@@ -122,21 +122,47 @@ export const getVariables = ({
     )
   }
 
-  if (opts.titleProp) {
-    const prop = t.objectPattern([
-      t.objectProperty(
-        t.identifier('title'),
-        t.identifier('title'),
+  if (opts.titleProp || opts.descProp) {
+    const properties = []
+    const propertySignatures = []
+    const createProperty = (attr: string) => {
+      return t.objectProperty(
+        t.identifier(attr),
+        t.identifier(attr),
         false,
         true,
-      ),
-      t.objectProperty(
-        t.identifier('titleId'),
-        t.identifier('titleId'),
-        false,
-        true,
-      ),
-    ])
+      )
+    }
+    const createSignature = (attr: string) => {
+      return tsOptionalPropertySignature(
+        t.identifier(attr),
+        t.tsTypeAnnotation(t.tsStringKeyword()),
+      )
+    }
+
+    if (opts.titleProp) {
+      properties.push(createProperty('title'), createProperty('titleId'))
+
+      if (opts.typescript) {
+        propertySignatures.push(
+          createSignature('title'),
+          createSignature('titleId'),
+        )
+      }
+    }
+
+    if (opts.descProp) {
+      properties.push(createProperty('desc'), createProperty('descId'))
+
+      if (opts.typescript) {
+        propertySignatures.push(
+          createSignature('desc'),
+          createSignature('descId'),
+        )
+      }
+    }
+
+    const prop = t.objectPattern(properties)
     props.push(prop)
     if (opts.typescript) {
       interfaces.push(
@@ -144,16 +170,7 @@ export const getVariables = ({
           t.identifier('SVGRProps'),
           null,
           null,
-          t.tSInterfaceBody([
-            tsOptionalPropertySignature(
-              t.identifier('title'),
-              t.tsTypeAnnotation(t.tsStringKeyword()),
-            ),
-            tsOptionalPropertySignature(
-              t.identifier('titleId'),
-              t.tsTypeAnnotation(t.tsStringKeyword()),
-            ),
-          ]),
+          t.tSInterfaceBody(propertySignatures),
         ),
       )
       prop.typeAnnotation = t.tsTypeAnnotation(
diff --git a/packages/babel-preset/src/index.ts b/packages/babel-preset/src/index.ts
index 5405a03..aa8faf5 100644
--- a/packages/babel-preset/src/index.ts
+++ b/packages/babel-preset/src/index.ts
@@ -18,6 +18,7 @@ import transformSvgComponent, {
 export interface Options extends TransformOptions {
   ref?: boolean
   titleProp?: boolean
+  descProp?: boolean
   expandProps?: boolean | 'start' | 'end'
   dimensions?: boolean
   icon?: boolean | string | number
@@ -76,6 +77,17 @@ const plugin = (_: ConfigAPI, opts: Options) => {
     ]
   }
 
+  if (opts.descProp) {
+    toAddAttributes = [
+      ...toAddAttributes,
+      {
+        name: 'aria-describedby',
+        value: 'descId',
+        literal: true,
+      },
+    ]
+  }
+
   if (opts.expandProps) {
     toAddAttributes = [
       ...toAddAttributes,
@@ -130,6 +142,10 @@ const plugin = (_: ConfigAPI, opts: Options) => {
     plugins.push(svgDynamicTitle)
   }
 
+  if (opts.descProp) {
+    plugins.push([svgDynamicTitle, { tag: 'desc' }])
+  }
+
   if (opts.native) {
     plugins.push(transformReactNativeSVG)
   }
diff --git a/packages/cli/README.md b/packages/cli/README.md
index 6cc42ff..be7b1bb 100644
--- a/packages/cli/README.md
+++ b/packages/cli/README.md
@@ -35,6 +35,7 @@ Options:
   --index-template <file>          specify a custom index.js template to use
   --no-index                       disable index file generation
   --title-prop                     create a title element linked with props
+  --desc-prop                      create a desc element linked with props
   --prettier-config <fileOrJson>   Prettier config
   --no-prettier                    disable Prettier
   --svgo-config <fileOrJson>       SVGO config
diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
index bee4a0b..bbdb464 100644
--- a/packages/cli/src/index.ts
+++ b/packages/cli/src/index.ts
@@ -146,6 +146,7 @@ program
   )
   .option('--no-index', 'disable index file generation')
   .option('--title-prop', 'create a title element linked with props')
+  .option('--desc-prop', 'create a desc element linked with props')
   .option(
     '--prettier-config <fileOrJson>',
     'Prettier config',
diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts
index 028f9e6..9468ca8 100644
--- a/packages/core/src/config.ts
+++ b/packages/core/src/config.ts
@@ -9,6 +9,7 @@ import type { State } from './state'
 export interface Config {
   ref?: boolean
   titleProp?: boolean
+  descProp?: boolean
   expandProps?: boolean | 'start' | 'end'
   dimensions?: boolean
   icon?: boolean | string | number
@@ -59,6 +60,7 @@ export const DEFAULT_CONFIG: Config = {
   template: undefined,
   index: false,
   titleProp: false,
+  descProp: false,
   runtimeConfig: true,
   namedExport: 'ReactComponent',
   exportType: 'default',
diff --git a/packages/plugin-jsx/src/index.ts b/packages/plugin-jsx/src/index.ts
index 57e9c84..9d198df 100644
--- a/packages/plugin-jsx/src/index.ts
+++ b/packages/plugin-jsx/src/index.ts
@@ -38,6 +38,7 @@ const jsxPlugin: Plugin = (code, config, state) => {
   const svgPresetOptions: SvgrPresetOptions = {
     ref: config.ref,
     titleProp: config.titleProp,
+    descProp: config.descProp,
     expandProps: config.expandProps,
     dimensions: config.dimensions,
     icon: config.icon,
diff --git a/website/pages/docs/options.mdx b/website/pages/docs/options.mdx
index 825e1a2..f59cb5a 100644
--- a/website/pages/docs/options.mdx
+++ b/website/pages/docs/options.mdx
@@ -178,6 +178,14 @@ Add title tag via title property. If titleProp is set to true and no title is pr
 | ------- | -------------- | -------------------- |
 | `false` | `--title-prop` | `titleProp: boolean` |
 
+## Description
+
+Add desc tag via desc property. If descProp is set to true and no description is provided (`desc={undefined}`) at render time, this will fallback to an existing desc element in the svg if exists.
+
+| Default | CLI Override  | API Override        |
+| ------- | ------------- | ------------------- |
+| `false` | `--desc-prop` | `descProp: boolean` |
+
 ## Template
 
 Specify a template file (CLI) or a template function (API) to use. For an example of template, see [the default one](https://github.com/gregberge/svgr/blob/main/packages/babel-plugin-transform-svg-component/src/defaultTemplate.ts).
diff --git a/website/src/components/playground/config/settings.js b/website/src/components/playground/config/settings.js
index 75f3f39..5192fbf 100644
--- a/website/src/components/playground/config/settings.js
+++ b/website/src/components/playground/config/settings.js
@@ -67,6 +67,13 @@ export const settings = [
     group: 'global',
     default: false,
   },
+  {
+    label: 'Desc prop',
+    name: 'descProp',
+    type: 'boolean',
+    group: 'global',
+    default: false,
+  },
   {
     label: 'Expand props',
     name: 'expandProps',


Test patch:
diff --git a/packages/babel-plugin-svg-dynamic-title/src/index.test.ts b/packages/babel-plugin-svg-dynamic-title/src/index.test.ts
index 32cb9db..4753c5d 100644
--- a/packages/babel-plugin-svg-dynamic-title/src/index.test.ts
+++ b/packages/babel-plugin-svg-dynamic-title/src/index.test.ts
@@ -1,16 +1,16 @@
 import { transform } from '@babel/core'
-import plugin from '.'
+import plugin, { Options } from '.'
 
-const testPlugin = (code: string) => {
+const testPlugin = (code: string, options: Options = {tag: 'title'}) => {
   const result = transform(code, {
-    plugins: ['@babel/plugin-syntax-jsx', plugin],
+    plugins: ['@babel/plugin-syntax-jsx', [plugin, options]],
     configFile: false,
   })
 
   return result?.code
 }
 
-describe('plugin', () => {
+describe('title plugin', () => {
   it('should add title attribute if not present', () => {
     expect(testPlugin('<svg></svg>')).toMatchInlineSnapshot(
       `"<svg>{title ? <title id={titleId}>{title}</title> : null}</svg>;"`,
@@ -19,7 +19,9 @@ describe('plugin', () => {
 
   it('should add title element and fallback to existing title', () => {
     // testing when the existing title contains a simple string
-    expect(testPlugin(`<svg><title>Hello</title></svg>`)).toMatchInlineSnapshot(
+    expect(
+      testPlugin(`<svg><title>Hello</title></svg>`),
+    ).toMatchInlineSnapshot(
       `"<svg>{title === undefined ? <title id={titleId}>Hello</title> : title ? <title id={titleId}>{title}</title> : null}</svg>;"`,
     )
     // testing when the existing title contains an JSXExpression
@@ -38,19 +40,78 @@ describe('plugin', () => {
     )
   })
   it('should support empty title', () => {
-    expect(testPlugin('<svg><title></title></svg>')).toMatchInlineSnapshot(
+    expect(
+      testPlugin('<svg><title></title></svg>'),
+    ).toMatchInlineSnapshot(
       `"<svg>{title ? <title id={titleId}>{title}</title> : null}</svg>;"`,
     )
   })
   it('should support self closing title', () => {
-    expect(testPlugin('<svg><title /></svg>')).toMatchInlineSnapshot(
+    expect(
+      testPlugin('<svg><title /></svg>'),
+    ).toMatchInlineSnapshot(
       `"<svg>{title ? <title id={titleId}>{title}</title> : null}</svg>;"`,
     )
   })
 
   it('should work if an attribute is already present', () => {
-    expect(testPlugin('<svg><foo /></svg>')).toMatchInlineSnapshot(
+    expect(
+      testPlugin('<svg><foo /></svg>'),
+    ).toMatchInlineSnapshot(
       `"<svg>{title ? <title id={titleId}>{title}</title> : null}<foo /></svg>;"`,
     )
   })
 })
+
+describe('desc plugin', () => {
+  it('should add desc attribute if not present', () => {
+    expect(testPlugin('<svg></svg>', { tag: 'desc' })).toMatchInlineSnapshot(
+      `"<svg>{desc ? <desc id={descId}>{desc}</desc> : null}</svg>;"`,
+    )
+  })
+
+  it('should add desc element and fallback to existing desc', () => {
+    // testing when the existing desc contains a simple string
+    expect(
+      testPlugin(`<svg><desc>Hello</desc></svg>`, { tag: 'desc' }),
+    ).toMatchInlineSnapshot(
+      `"<svg>{desc === undefined ? <desc id={descId}>Hello</desc> : desc ? <desc id={descId}>{desc}</desc> : null}</svg>;"`,
+    )
+    // testing when the existing desc contains an JSXExpression
+    expect(
+      testPlugin(`<svg><desc>{"Hello"}</desc></svg>`, { tag: 'desc' }),
+    ).toMatchInlineSnapshot(
+      `"<svg>{desc === undefined ? <desc id={descId}>{\\"Hello\\"}</desc> : desc ? <desc id={descId}>{desc}</desc> : null}</svg>;"`,
+    )
+  })
+  it('should preserve any existing desc attributes', () => {
+    // testing when the existing desc contains a simple string
+    expect(
+      testPlugin(`<svg><desc id='a'>Hello</desc></svg>`, { tag: 'desc' }),
+    ).toMatchInlineSnapshot(
+      `"<svg>{desc === undefined ? <desc id={descId || 'a'}>Hello</desc> : desc ? <desc id={descId || 'a'}>{desc}</desc> : null}</svg>;"`,
+    )
+  })
+  it('should support empty desc', () => {
+    expect(
+      testPlugin('<svg><desc></desc></svg>', { tag: 'desc' }),
+    ).toMatchInlineSnapshot(
+      `"<svg>{desc ? <desc id={descId}>{desc}</desc> : null}</svg>;"`,
+    )
+  })
+  it('should support self closing desc', () => {
+    expect(
+      testPlugin('<svg><desc /></svg>', { tag: 'desc' }),
+    ).toMatchInlineSnapshot(
+      `"<svg>{desc ? <desc id={descId}>{desc}</desc> : null}</svg>;"`,
+    )
+  })
+
+  it('should work if an attribute is already present', () => {
+    expect(
+      testPlugin('<svg><foo /></svg>', { tag: 'desc' }),
+    ).toMatchInlineSnapshot(
+      `"<svg>{desc ? <desc id={descId}>{desc}</desc> : null}<foo /></svg>;"`,
+    )
+  })
+})
diff --git a/packages/babel-plugin-transform-svg-component/src/__snapshots__/index.test.ts.snap b/packages/babel-plugin-transform-svg-component/src/__snapshots__/index.test.ts.snap
index 587508f..7029e26 100644
--- a/packages/babel-plugin-transform-svg-component/src/__snapshots__/index.test.ts.snap
+++ b/packages/babel-plugin-transform-svg-component/src/__snapshots__/index.test.ts.snap
@@ -97,6 +97,29 @@ const SvgComponent = () => <svg><g /></svg>;
 export default SvgComponent;"
 `;
 
+exports[`plugin javascript with "descProp" adds "desc" and "descId" prop 1`] = `
+"import * as React from \\"react\\";
+
+const SvgComponent = ({
+  desc,
+  descId
+}) => <svg><g /></svg>;
+
+export default SvgComponent;"
+`;
+
+exports[`plugin javascript with "descProp" and "expandProps" adds "desc", "descId" props and expands props 1`] = `
+"import * as React from \\"react\\";
+
+const SvgComponent = ({
+  desc,
+  descId,
+  ...props
+}) => <svg><g /></svg>;
+
+export default SvgComponent;"
+`;
+
 exports[`plugin javascript with "expandProps" add props 1`] = `
 "import * as React from \\"react\\";
 
@@ -194,7 +217,21 @@ const ForwardRef = forwardRef(SvgComponent);
 export default ForwardRef;"
 `;
 
-exports[`plugin javascript with "titleProp" adds "titleProp" and "titleId" prop 1`] = `
+exports[`plugin javascript with "titleProp" "descProp" and "expandProps" adds "title", "titleId", "desc", "descId" props and expands props 1`] = `
+"import * as React from \\"react\\";
+
+const SvgComponent = ({
+  title,
+  titleId,
+  desc,
+  descId,
+  ...props
+}) => <svg><g /></svg>;
+
+export default SvgComponent;"
+`;
+
+exports[`plugin javascript with "titleProp" adds "title" and "titleId" prop 1`] = `
 "import * as React from \\"react\\";
 
 const SvgComponent = ({
@@ -205,7 +242,20 @@ const SvgComponent = ({
 export default SvgComponent;"
 `;
 
-exports[`plugin javascript with "titleProp" and "expandProps" adds "titleProp", "titleId" props and expands props 1`] = `
+exports[`plugin javascript with "titleProp" and "descProp" adds "title", "titleId", "desc", and "descId prop 1`] = `
+"import * as React from \\"react\\";
+
+const SvgComponent = ({
+  title,
+  titleId,
+  desc,
+  descId
+}) => <svg><g /></svg>;
+
+export default SvgComponent;"
+`;
+
+exports[`plugin javascript with "titleProp" and "expandProps" adds "title", "titleId" props and expands props 1`] = `
 "import * as React from \\"react\\";
 
 const SvgComponent = ({
@@ -325,6 +375,38 @@ const SvgComponent = () => <svg><g /></svg>;
 export default SvgComponent;"
 `;
 
+exports[`plugin typescript with "descProp" adds "desc" and "descId" prop 1`] = `
+"import * as React from \\"react\\";
+interface SVGRProps {
+  desc?: string;
+  descId?: string;
+}
+
+const SvgComponent = ({
+  desc,
+  descId
+}: SVGRProps) => <svg><g /></svg>;
+
+export default SvgComponent;"
+`;
+
+exports[`plugin typescript with "descProp" and "expandProps" adds "desc", "descId" props and expands props 1`] = `
+"import * as React from \\"react\\";
+import { SVGProps } from \\"react\\";
+interface SVGRProps {
+  desc?: string;
+  descId?: string;
+}
+
+const SvgComponent = ({
+  desc,
+  descId,
+  ...props
+}: SVGProps<SVGSVGElement> & SVGRProps) => <svg><g /></svg>;
+
+export default SvgComponent;"
+`;
+
 exports[`plugin typescript with "expandProps" add props 1`] = `
 "import * as React from \\"react\\";
 import { SVGProps } from \\"react\\";
@@ -423,7 +505,28 @@ const ForwardRef = forwardRef(SvgComponent);
 export default ForwardRef;"
 `;
 
-exports[`plugin typescript with "titleProp" adds "titleProp" and "titleId" prop 1`] = `
+exports[`plugin typescript with "titleProp" "descProp" and "expandProps" adds "title", "titleId", "desc", "descId" props and expands props 1`] = `
+"import * as React from \\"react\\";
+import { SVGProps } from \\"react\\";
+interface SVGRProps {
+  title?: string;
+  titleId?: string;
+  desc?: string;
+  descId?: string;
+}
+
+const SvgComponent = ({
+  title,
+  titleId,
+  desc,
+  descId,
+  ...props
+}: SVGProps<SVGSVGElement> & SVGRProps) => <svg><g /></svg>;
+
+export default SvgComponent;"
+`;
+
+exports[`plugin typescript with "titleProp" adds "title" and "titleId" prop 1`] = `
 "import * as React from \\"react\\";
 interface SVGRProps {
   title?: string;
@@ -438,7 +541,26 @@ const SvgComponent = ({
 export default SvgComponent;"
 `;
 
-exports[`plugin typescript with "titleProp" and "expandProps" adds "titleProp", "titleId" props and expands props 1`] = `
+exports[`plugin typescript with "titleProp" and "descProp" adds "title", "titleId", "desc", and "descId prop 1`] = `
+"import * as React from \\"react\\";
+interface SVGRProps {
+  title?: string;
+  titleId?: string;
+  desc?: string;
+  descId?: string;
+}
+
+const SvgComponent = ({
+  title,
+  titleId,
+  desc,
+  descId
+}: SVGRProps) => <svg><g /></svg>;
+
+export default SvgComponent;"
+`;
+
+exports[`plugin typescript with "titleProp" and "expandProps" adds "title", "titleId" props and expands props 1`] = `
 "import * as React from \\"react\\";
 import { SVGProps } from \\"react\\";
 interface SVGRProps {
diff --git a/packages/babel-plugin-transform-svg-component/src/index.test.ts b/packages/babel-plugin-transform-svg-component/src/index.test.ts
index cce4d49..7272ff0 100644
--- a/packages/babel-plugin-transform-svg-component/src/index.test.ts
+++ b/packages/babel-plugin-transform-svg-component/src/index.test.ts
@@ -57,7 +57,7 @@ describe('plugin', () => {
     })
 
     describe('with "titleProp"', () => {
-      it('adds "titleProp" and "titleId" prop', () => {
+      it('adds "title" and "titleId" prop', () => {
         const { code } = testPlugin(language)('<svg><g /></svg>', {
           titleProp: true,
         })
@@ -66,7 +66,7 @@ describe('plugin', () => {
     })
 
     describe('with "titleProp" and "expandProps"', () => {
-      it('adds "titleProp", "titleId" props and expands props', () => {
+      it('adds "title", "titleId" props and expands props', () => {
         const { code } = testPlugin(language)('<svg><g /></svg>', {
           ...defaultOptions,
           expandProps: true,
@@ -76,6 +76,48 @@ describe('plugin', () => {
       })
     })
 
+    describe('with "descProp"', () => {
+      it('adds "desc" and "descId" prop', () => {
+        const { code } = testPlugin(language)('<svg><g /></svg>', {
+          descProp: true,
+        })
+        expect(code).toMatchSnapshot()
+      })
+    })
+
+    describe('with "descProp" and "expandProps"', () => {
+      it('adds "desc", "descId" props and expands props', () => {
+        const { code } = testPlugin(language)('<svg><g /></svg>', {
+          ...defaultOptions,
+          expandProps: true,
+          descProp: true,
+        })
+        expect(code).toMatchSnapshot()
+      })
+    })
+
+    describe('with "titleProp" and "descProp"', () => {
+      it('adds "title", "titleId", "desc", and "descId prop', () => {
+        const { code } = testPlugin(language)('<svg><g /></svg>', {
+          titleProp: true,
+          descProp: true,
+        })
+        expect(code).toMatchSnapshot()
+      })
+    })
+
+    describe('with "titleProp" "descProp" and "expandProps"', () => {
+      it('adds "title", "titleId", "desc", "descId" props and expands props', () => {
+        const { code } = testPlugin(language)('<svg><g /></svg>', {
+          ...defaultOptions,
+          expandProps: true,
+          titleProp: true,
+          descProp: true,
+        })
+        expect(code).toMatchSnapshot()
+      })
+    })
+
     describe('with "expandProps"', () => {
       it('add props', () => {
         const { code } = testPlugin(language)('<svg><g /></svg>', {
diff --git a/packages/babel-preset/src/index.test.ts b/packages/babel-preset/src/index.test.ts
index 90073f5..bf09b86 100644
--- a/packages/babel-preset/src/index.test.ts
+++ b/packages/babel-preset/src/index.test.ts
@@ -83,6 +83,55 @@ describe('preset', () => {
     `)
   })
 
+  it('handles descProp', () => {
+    expect(
+      testPreset('<svg></svg>', {
+        descProp: true,
+      }),
+    ).toMatchInlineSnapshot(`
+      "import * as React from \\"react\\";
+
+      const SvgComponent = ({
+        desc,
+        descId
+      }) => <svg aria-describedby={descId}>{desc ? <desc id={descId}>{desc}</desc> : null}</svg>;
+
+      export default SvgComponent;"
+    `)
+  })
+  it('handles descProp and fallback on existing desc', () => {
+    // testing when existing desc has string as chilren
+    expect(
+      testPreset(`<svg><desc>Hello</desc></svg>`, {
+        descProp: true,
+      }),
+    ).toMatchInlineSnapshot(`
+      "import * as React from \\"react\\";
+
+      const SvgComponent = ({
+        desc,
+        descId
+      }) => <svg aria-describedby={descId}>{desc === undefined ? <desc id={descId}>Hello</desc> : desc ? <desc id={descId}>{desc}</desc> : null}</svg>;
+
+      export default SvgComponent;"
+    `)
+    // testing when existing desc has JSXExpression as children
+    expect(
+      testPreset(`<svg><desc>{"Hello"}</desc></svg>`, {
+        descProp: true,
+      }),
+    ).toMatchInlineSnapshot(`
+      "import * as React from \\"react\\";
+
+      const SvgComponent = ({
+        desc,
+        descId
+      }) => <svg aria-describedby={descId}>{desc === undefined ? <desc id={descId}>{\\"Hello\\"}</desc> : desc ? <desc id={descId}>{desc}</desc> : null}</svg>;
+
+      export default SvgComponent;"
+    `)
+  })
+
   it('handles replaceAttrValues', () => {
     expect(
       testPreset('<svg a="#000" b="#fff" />', {
diff --git a/packages/cli/src/__snapshots__/index.test.ts.snap b/packages/cli/src/__snapshots__/index.test.ts.snap
index a2c6a58..fb97ba8 100644
--- a/packages/cli/src/__snapshots__/index.test.ts.snap
+++ b/packages/cli/src/__snapshots__/index.test.ts.snap
@@ -155,6 +155,27 @@ export default SvgFile
 "
 `;
 
+exports[`cli should support various args: --desc-prop 1`] = `
+"import * as React from 'react'
+
+const SvgFile = ({ desc, descId, ...props }) => (
+  <svg
+    width={48}
+    height={1}
+    xmlns=\\"http://www.w3.org/2000/svg\\"
+    aria-describedby={descId}
+    {...props}
+  >
+    {desc ? <desc id={descId}>{desc}</desc> : null}
+    <path d=\\"M0 0h48v1H0z\\" fill=\\"#063855\\" fillRule=\\"evenodd\\" />
+  </svg>
+)
+
+export default SvgFile
+
+"
+`;
+
 exports[`cli should support various args: --expand-props none 1`] = `
 "import * as React from 'react'
 
@@ -484,6 +505,37 @@ export default SvgFile
 "
 `;
 
+exports[`cli should support various args: --typescript --ref --desc-prop 1`] = `
+"import * as React from 'react'
+import { SVGProps, Ref, forwardRef } from 'react'
+interface SVGRProps {
+  desc?: string;
+  descId?: string;
+}
+
+const SvgFile = (
+  { desc, descId, ...props }: SVGProps<SVGSVGElement> & SVGRProps,
+  ref: Ref<SVGSVGElement>,
+) => (
+  <svg
+    width={48}
+    height={1}
+    xmlns=\\"http://www.w3.org/2000/svg\\"
+    ref={ref}
+    aria-describedby={descId}
+    {...props}
+  >
+    {desc ? <desc id={descId}>{desc}</desc> : null}
+    <path d=\\"M0 0h48v1H0z\\" fill=\\"#063855\\" fillRule=\\"evenodd\\" />
+  </svg>
+)
+
+const ForwardRef = forwardRef(SvgFile)
+export default ForwardRef
+
+"
+`;
+
 exports[`cli should support various args: --typescript --ref --title-prop 1`] = `
 "import * as React from 'react'
 import { SVGProps, Ref, forwardRef } from 'react'
diff --git a/packages/cli/src/index.test.ts b/packages/cli/src/index.test.ts
index 9575084..5de680f 100644
--- a/packages/cli/src/index.test.ts
+++ b/packages/cli/src/index.test.ts
@@ -131,9 +131,11 @@ describe('cli', () => {
     ['--no-svgo'],
     ['--no-prettier'],
     ['--title-prop'],
+    ['--desc-prop'],
     ['--typescript'],
     ['--typescript --ref'],
     ['--typescript --ref --title-prop'],
+    ['--typescript --ref --desc-prop'],
   ])(
     'should support various args',
     async (args) => {
diff --git a/packages/core/src/__snapshots__/config.test.ts.snap b/packages/core/src/__snapshots__/config.test.ts.snap
index 4f14623..d5886aa 100644
--- a/packages/core/src/__snapshots__/config.test.ts.snap
+++ b/packages/core/src/__snapshots__/config.test.ts.snap
@@ -2,6 +2,7 @@
 
 exports[`svgo async #loadConfig [async] should load config using filePath 1`] = `
 Object {
+  "descProp": false,
   "dimensions": true,
   "expandProps": "end",
   "exportType": "default",
@@ -32,6 +33,7 @@ Object {
 
 exports[`svgo async #loadConfig [async] should not load config with "runtimeConfig: false 1`] = `
 Object {
+  "descProp": false,
   "dimensions": true,
   "expandProps": "end",
   "exportType": "default",
@@ -63,6 +65,7 @@ Object {
 
 exports[`svgo async #loadConfig [async] should use default config without state.filePath 1`] = `
 Object {
+  "descProp": false,
   "dimensions": false,
   "expandProps": "end",
   "exportType": "default",
@@ -87,6 +90,7 @@ Object {
 
 exports[`svgo async #loadConfig [async] should work with custom config path 1`] = `
 Object {
+  "descProp": false,
   "dimensions": true,
   "expandProps": "end",
   "exportType": "default",
@@ -117,6 +121,7 @@ Object {
 
 exports[`svgo sync #loadConfig [sync] should load config using filePath 1`] = `
 Object {
+  "descProp": false,
   "dimensions": true,
   "expandProps": "end",
   "exportType": "default",
@@ -147,6 +152,7 @@ Object {
 
 exports[`svgo sync #loadConfig [sync] should not load config with "runtimeConfig: false 1`] = `
 Object {
+  "descProp": false,
   "dimensions": true,
   "expandProps": "end",
   "exportType": "default",
@@ -178,6 +184,7 @@ Object {
 
 exports[`svgo sync #loadConfig [sync] should use default config without state.filePath 1`] = `
 Object {
+  "descProp": false,
   "dimensions": false,
   "expandProps": "end",
   "exportType": "default",
@@ -202,6 +209,7 @@ Object {
 
 exports[`svgo sync #loadConfig [sync] should work with custom config path 1`] = `
 Object {
+  "descProp": false,
   "dimensions": true,
   "expandProps": "end",
   "exportType": "default",
diff --git a/packages/core/src/__snapshots__/transform.test.ts.snap b/packages/core/src/__snapshots__/transform.test.ts.snap
index c85ba40..1c7355a 100644
--- a/packages/core/src/__snapshots__/transform.test.ts.snap
+++ b/packages/core/src/__snapshots__/transform.test.ts.snap
@@ -1,5 +1,33 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`convert config accepts options {"descProp":true} 1`] = `
+"import * as React from 'react'
+
+const SvgComponent = ({ desc, descId, ...props }) => (
+  <svg
+    width={88}
+    height={88}
+    xmlns=\\"http://www.w3.org/2000/svg\\"
+    aria-describedby={descId}
+    {...props}
+  >
+    {desc ? <desc id={descId}>{desc}</desc> : null}
+    <g
+      stroke=\\"#063855\\"
+      strokeWidth={2}
+      fill=\\"none\\"
+      fillRule=\\"evenodd\\"
+      strokeLinecap=\\"square\\"
+    >
+      <path d=\\"M51 37 37 51M51 51 37 37\\" />
+    </g>
+  </svg>
+)
+
+export default SvgComponent
+"
+`;
+
 exports[`convert config accepts options {"dimensions":false} 1`] = `
 "import * as React from 'react'
 
@@ -489,6 +517,28 @@ export default noop
 "
 `;
 
+exports[`convert config descProp: without desc added 1`] = `
+"import * as React from 'react'
+
+const SvgComponent = ({ desc, descId, ...props }) => (
+  <svg
+    width={0}
+    height={0}
+    style={{
+      position: 'absolute',
+    }}
+    aria-describedby={descId}
+    {...props}
+  >
+    {desc ? <desc id={descId}>{desc}</desc> : null}
+    <path d=\\"M0 0h24v24H0z\\" fill=\\"none\\" />
+  </svg>
+)
+
+export default SvgComponent
+"
+`;
+
 exports[`convert config titleProp: without title added 1`] = `
 "import * as React from 'react'
 
diff --git a/packages/core/src/transform.test.ts b/packages/core/src/transform.test.ts
index bfea9b3..387a202 100644
--- a/packages/core/src/transform.test.ts
+++ b/packages/core/src/transform.test.ts
@@ -317,6 +317,7 @@ describe('convert', () => {
           tpl`const noop = () => null; export default noop;`,
       },
       { titleProp: true },
+      { descProp: true },
       { memo: true },
       {
         namedExport: 'Component',
@@ -340,5 +341,16 @@ describe('convert', () => {
         await convertWithAllPlugins(svg, { titleProp: true }),
       ).toMatchSnapshot()
     })
+
+    it('descProp: without desc added', async () => {
+      const svg = `
+      <svg width="0" height="0" style="position:absolute">
+    <path d="M0 0h24v24H0z" fill="none" />
+</svg>
+`
+      expect(
+        await convertWithAllPlugins(svg, { descProp: true }),
+      ).toMatchSnapshot()
+    })
   })
 })