309

I am trying to write a React component for HTML heading tags (h1, h2, h3, etc.), where the heading level is specified via a prop.

I tried to do it like this:

<h{this.props.level}>Hello</h{this.props.level}>

And I expected output like:

<h1>Hello</h1>

But this is not working.

Is there any way to do this?

Aryan Beezadhur
  • 4,503
  • 4
  • 21
  • 42
Eranga Kapukotuwa
  • 4,542
  • 5
  • 25
  • 30

9 Answers9

557

No way to do that in-place, just put it in a variable (with first letter capitalised):

const CustomTag = `h${this.props.level}`;

<CustomTag>Hello</CustomTag>
Mark Amery
  • 143,130
  • 81
  • 406
  • 459
zerkms
  • 249,484
  • 69
  • 436
  • 539
  • 7
    Definitely easier than `React.createClass`, I prefer this way. Thanks. – Vadorequest Jan 06 '17 at 09:19
  • 1
    @zerkms Do you have any idea how to add attributes to CustomTag? thanks – Sabrina Luo May 19 '17 at 08:04
  • 1
    @Sabrina `` – zerkms May 19 '17 at 08:08
  • Huh. How does this work? If the variable name is lowercase, it just inserts that as a tag (eg. if it was customtag, I would get Hello). Is this documented anywhere? – Ibrahim Sep 13 '17 at 02:35
  • 1
    I guess it's because with a capital first letter, [it gets interpreted as a React component](https://facebook.github.io/react/docs/jsx-in-depth.html#html-tags-vs.-react-components), and html tag names are also considered valid React components so it works. Seems a little fishy, but I'll take it. – Ibrahim Sep 13 '17 at 02:38
  • I would like to mention that, if you are going to use a react class here, dont wrap its name in quotes, Don't do this: `'ClassName'`, instead use as `ClassName`, quite intuitive yet sometime we make this mistake! – demonofthemist Feb 01 '18 at 05:47
  • 7
    If the component is stored in an object's property, a capital first letter isn't necessary. `var foo = { bar: CustomTag }; return ` works fine. – jdunning May 19 '18 at 21:59
  • This should be in the docs. Choosing Types at Runtime is there but this is a very useful variation on what they currently have. – Ynot Sep 12 '18 at 19:12
  • @GalAbra https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals – zerkms Oct 16 '19 at 11:20
  • As @Ibrahim says, this seems "fishy". Since `CustomTag` is capitalized, per the docs Ibrahim cites, we're technically telling React to render the *component* stored in variable `CustomTag` when write ``. But `CustomTag` is not a component, it's just a string (with a value like `"h1"`)! So it seems to me like we're doing something technically illegal in this answer, that happens to work in the current version of React but has at least a modest risk of breaking at some point in future. I'd thus favour [using `React.createElement`](https://stackoverflow.com/a/45788598/1709587) instead. – Mark Amery Jan 03 '22 at 23:00
162

If you're using TypeScript, you'll have seen an error like this:

Type '{ children: string; }' has no properties in common with type 'IntrinsicAttributes'.ts(2559)

TypeScript does not know that CustomTag is a valid HTML tag name and throws an unhelpful error.

To fix, cast CustomTag as keyof JSX.IntrinsicElements!

// var name must start with a capital letter
const CustomTag = `h${this.props.level}` as keyof JSX.IntrinsicElements;
// or to let TypeScript check if the tag is valid
// const CustomTag : keyof JSX.IntrinsicElements = `h${this.props.level}`;

<CustomTag>Hello</CustomTag>
Steven Clontz
  • 945
  • 1
  • 11
  • 20
Jack Steam
  • 4,500
  • 1
  • 24
  • 39
  • 2
    I'm on TypeScript but casting it gives this error: `Types of property 'crossOrigin' are incompatible. Type 'string | undefined' is not assignable to type '"" | "anonymous" | "use-credentials" | undefined'. Type 'string' is not assignable to type '"" | "anonymous" | "use-credentials" | undefined'.` – Can Poyrazoğlu Mar 09 '20 at 09:25
  • 9
    Just wanted to say thanks for this. I probably would have spent hours trying to type this if this were not here. – Kelly Copley Feb 17 '21 at 16:24
  • How can you be able to do this with Flow? – Farid Shokri Jul 13 '21 at 18:10
  • 5
    I think ```const Tag: keyof JSX.IntrinsicElements = `h${level}`;``` would be slightly better because if you now use an invalid tag e.g. `headline${level}` TypeScript will complain. (assuming that `level` is typed correctly as literal type) – Martin Böttcher Jan 10 '22 at 08:49
  • 1
    Note that the variable apparently must be PascalCased for this to work. I tried `customTag` and still got the same error, but changing it to `CustomTag` fixed everything. I guess typescript probably assumes lower cased tags must be native html elements and validates them differently – Jemar Jones Jan 17 '22 at 23:35
  • Error : ` is using incorrect casing. Use PascalCase for React components` – Akshay K Nair May 09 '22 at 05:57
  • I get the error `Expression produces a union type that is too complex to represent` on my`className`, `ref` and a spread operator (i.e.``). – Michael Lynch Dec 01 '22 at 17:11
  • 1
    It's better to use `ElementType>` from react: https://www.aleksandrhovhannisyan.com/blog/dynamic-tag-name-props-in-react/ Typical names: - `as`: https://react-bootstrap.netlify.app/components/buttons/#button-props, https://chakra-ui.com/docs/styled-system/style-props#the-as-prop - `component`: https://mui.com/material-ui/api/button/#props - `tagName`: https://formatjs.io/docs/react-intl/components/#formattedmessage – tanguy_k Feb 08 '23 at 22:15
  • `as const` works too if you restrict level to be 1-6 – Xitang Jun 11 '23 at 21:38
  • Now that JSX namespace is deprecated I recommend: `const CustomTag = \`h${this.props.level}\` as keyof React.JSX.IntrinsicElements;` – theannouncer Aug 03 '23 at 17:09
49

For completeness, if you want to use a dynamic name, you can also directly call React.createElement instead of using JSX:

React.createElement(`h${this.props.level}`, null, 'Hello')

This avoids having to create a new variable or component.

With props:

React.createElement(
  `h${this.props.level}`,
  {
    foo: 'bar',
  },
  'Hello'
)

From the docs:

Create and return a new React element of the given type. The type argument can be either a tag name string (such as 'div' or 'span'), or a React component type (a class or a function).

Code written with JSX will be converted to use React.createElement(). You will not typically invoke React.createElement() directly if you are using JSX. See React Without JSX to learn more.

Mark Amery
  • 143,130
  • 81
  • 406
  • 459
Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
24

All the other answers are working fine but I would add some extra, because by doing this:

  1. It is a bit safer. Even if your type-checking is failing you still return a proper component.
  2. It is more declarative. Anybody by looking at this component can see what it could return.
  3. Its is more flexible for example instead of 'h1', 'h2', ... for type of your Heading you can have some other abstract concepts 'sm', 'lg' or 'primary', 'secondary'

The Heading component:

import React from 'react';

const elements = {
  h1: 'h1',
  h2: 'h2',
  h3: 'h3',
  h4: 'h4',
  h5: 'h5',
  h6: 'h6',
};

function Heading({ type, children, ...props }) {    
  return React.createElement(
    elements[type] || elements.h1, 
    props, 
    children
  );
}

Heading.defaultProps = {
  type: 'h1',
};

export default Heading;

Which you can use it like

<Heading type="h1">Some Heading</Heading>

or you can have a different abstract concept, for example you can define a size props like:

import React from 'react';

const elements = {
  xl: 'h1',
  lg: 'h2',
  rg: 'h3',
  sm: 'h4',
  xs: 'h5',
  xxs: 'h6',
};

function Heading({ size, children }) {
  return React.createElement(
    elements[size] || elements.rg, 
    props, 
    children
  );
}

Heading.defaultProps = {
  size: 'rg',
};

export default Heading;

Which you can use it like

<Heading size="sm">Some Heading</Heading>
Saman
  • 5,044
  • 3
  • 28
  • 27
10

In the instance of dynamic headings (h1, h2...), a component could return React.createElement (mentioned above by Felix) like so.

const Heading = ({level, children, ...props}) => {
    return React.createElement('h'.concat(level), props , children)
}

For composability, both props and children are passed.

See Example

robstarbuck
  • 6,893
  • 2
  • 41
  • 40
5

This is how I set it up for my project.

TypographyType.ts

import { HTMLAttributes } from 'react';

export type TagType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span';

export type HeadingType = HTMLAttributes<HTMLHeadingElement>;
export type ParagraphType = HTMLAttributes<HTMLParagraphElement>;
export type SpanType = HTMLAttributes<HTMLSpanElement>;

export type TypographyProps = (HeadingType | ParagraphType | SpanType) & {
  variant?:
    | 'h1'
    | 'h2'
    | 'h3'
    | 'h4'
    | 'h5'
    | 'h6'
    | 'body1'
    | 'body2'
    | 'subtitle1'
    | 'subtitle2'
    | 'caption'
    | 'overline'
    | 'button';
};

Typography.tsx

    import { FC } from 'react';
    import cn from 'classnames';
    import { typography } from '@/theme';
    
    import { TagType, TypographyProps } from './TypographyType';
    
    const headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
    const paragraphs = ['body1', 'body2', 'subtitle1', 'subtitle2'];
    const spans = ['button', 'caption', 'overline'];
    
    const Typography: FC<TypographyProps> = ({
      children,
      variant = 'body1',
      className,
      ...props
    }) => {
      const { variants } = typography;
    
      const Tag = cn({
        [`${variant}`]: headings.includes(variant),
        [`p`]: paragraphs.includes(variant),
        [`span`]: spans.includes(variant)
      }) as TagType;
    
      return (
        <Tag
          {...props}
          className={cn(
            {
              [`${variants[variant]}`]: variant,
            },
            className
          )}
        >
          {children}
        </Tag>
      );
    };
    
    export default Typography;
Mike
  • 384
  • 7
  • 16
3
//for Typescript
interface ComponentProps {
    containerTag: keyof JSX.IntrinsicElements;
}

export const Component = ({ containerTag: CustomTag }: ComponentProps) => {
    return <CustomTag>Hello</CustomTag>;
}
2

You can give this a try. I implement like this.

import { memo, ReactNode } from "react";
import cx from "classnames";

import classes from "./Title.module.scss";

export interface TitleProps {
  children?: ReactNode;
  className?: string;
  text?: string;
  variant: Sizes;
}

type Sizes = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
const Title = ({
  className,
  variant = "h1",
  text,
  children,
}: TitleProps): JSX.Element => {
  const Tag = `${variant}` as keyof JSX.IntrinsicElements;
  return (
    <Tag
      className={cx(`${classes.title} ${classes[variant]}`, {
        [`${className}`]: className,
      })}
    >
      {text || children}
    </Tag>
  );
};

export default memo(Title);
ashwin1014
  • 351
  • 1
  • 5
  • 18
1

Generalising robstarbuck's answer you can create a completely dynamic tag component like this:

const Tag = ({ tagName, children, ...props }) => (
  React.createElement(tagName, props , children)
)

which you can use like:

const App = ({ myTagName = 'h1' }) => {
  return (
    <Tag tagName={myTagName} className="foo">
     Hello Tag!
    </Tag>
  )
}
Mark Amery
  • 143,130
  • 81
  • 406
  • 459
gazdagergo
  • 6,187
  • 1
  • 31
  • 45