ui
ui copied to clipboard
Adding a reusable typography component
Following the discussion in #315 and with some inspiration from #11. The idea is to not only have the typography in the introduction of the docs as inspiration, but having a reusable typography component that implements these styles, can be used across the project and be installed with npx shadcn-ui typography
After having run in this issue multiple times myself, i created the code for typography component that exactly mimics the styles from https://ui.shadcn.com/docs/components/typography plus added 'text-foreground' or 'text-muted-foreground' to also enable dark mode.
Ps: this is my first ever contribution to an open source project, so be gentle :)
@nilaq is attempting to deploy a commit to the shadcn-pro Team on Vercel.
A member of the Team first needs to authorize it.
And maybe you should add the documentation about your component
The latest updates on your projects. Learn more about Vercel for Git ↗︎
1 Ignored Deployment
| Name | Status | Preview | Comments | Updated (UTC) |
|---|---|---|---|---|
| next-template | ⬜️ Ignored (Inspect) | May 18, 2023 0:30am |
@armandsalle thanks for the feedback, appreciate it a lot! Were you thinking something like this? Just added the changes you proposed. Will start working on the documentation.
Not sure though if this is the most elegant way to do it Typography element='h1' as='h1>Example</Typography> feels a lot more clunky than <H1>Example. Any thoughts on this?
Hey! I was reading the CVA documentation and I saw this example of polymorphic components
-- // A familiar `styled` button as a link
-- <Button as="a" href="#" variant="primary">Button as a link</Button>
++ // A `cva` button as a link
++ <a href="#" class={button({variant: "primary"})}>Button as a link</a>
in https://cva.style/docs/faqs
So why not just export classnames? No more need for the createElement() function
Any updates on this?
So far, it works like a charm. Thank you @nilaq !
Agree with @armandsalle
Exporting typographyVariants({...}) is more than enough
Component code with the fixes mentioned above
import * as React from "react";
import { VariantProps, cva } from "class-variance-authority";
import cn from "utils/cn";
const typographyVariants = cva("text-foreground", {
variants: {
variant: {
h1: "scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl",
h2: "scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0",
h3: "scroll-m-20 text-2xl font-semibold tracking-tight",
h4: "scroll-m-20 text-xl font-semibold tracking-tight",
h5: "scroll-m-20 text-lg font-semibold tracking-tight",
h6: "scroll-m-20 text-base font-semibold tracking-tight",
p: "leading-7 [&:not(:first-child)]:mt-6",
blockquote: "mt-6 border-l-2 pl-6 italic",
ul: "my-6 ml-6 list-disc [&>li]:mt-2",
inlineCode:
"relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold",
lead: "text-xl text-muted-foreground",
largeText: "text-lg font-semibold",
smallText: "text-sm font-medium leading-none",
mutedText: "text-sm text-muted-foreground",
},
},
});
type VariantPropType = VariantProps<typeof typographyVariants>;
const variantElementMap: Record<
NonNullable<VariantPropType["variant"]>,
string
> = {
h1: "h1",
h2: "h2",
h3: "h3",
h4: "h4",
h5: "h5",
h6: "h6",
p: "p",
blockquote: "blockquote",
inlineCode: "code",
largeText: "div",
smallText: "small",
lead: "p",
mutedText: "p",
ul: "ul",
};
type Element = keyof JSX.IntrinsicElements;
type TypographyProps<T extends Element> = {
as?: T;
} & VariantPropType &
React.HTMLAttributes<HTMLElement>;
const Typography = <T extends Element>({
className,
as,
variant,
...props
}: TypographyProps<T>) => {
const Component =
as ?? (variant ? variantElementMap[variant] : undefined) ?? "div";
const componentProps = {
className: cn(typographyVariants({ variant, className })),
...props,
};
return React.createElement(Component, componentProps);
};
export default React.forwardRef(Typography);
This one an update with ref
import * as React from "react";
import {VariantProps, cva} from "class-variance-authority";
import {cn} from "@/lib/utils"
import {Slot} from "@radix-ui/react-slot";
const typographyVariants = cva("text-foreground", {
variants: {
variant: {
h1: "text-6xl scroll-m-20 font-bold",
h2: "text-5xl scroll-m-20 font-bold",
h3: "text-4xl scroll-m-20 font-bold",
h4: "text-3xl scroll-m-20 font-bold",
h5: "text-2xl scroll-m-20 font-bold",
h6: "text-xl scroll-m-20 font-bold",
p: "leading-7 [&:not(:first-child)]:mt-6",
blockquote: "mt-6 border-l-2 pl-6 italic",
ul: "my-6 ml-6 list-disc [&>li]:mt-2",
inlineCode:
"relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold",
lead: "text-xl text-muted-foreground",
regularText: "text-base",
largeText: "text-lg font-semibold",
smallText: "text-sm leading-none",
mutedText: "text-sm text-muted-foreground",
},
weight: {
bold: '!font-bold',
semibold: '!font-semibold',
normal: '!font-normal',
medium: '!font-medium',
light: '!font-light'
}
},
defaultVariants: {
variant: "regularText",
weight: "normal",
},
});
type VariantPropType = VariantProps<typeof typographyVariants>;
const variantElementMap: Record<
NonNullable<VariantPropType["variant"]>,
string
> = {
h1: "h1",
h2: "h2",
h3: "h3",
h4: "h4",
h5: "h5",
h6: "h6",
p: "p",
blockquote: "blockquote",
inlineCode: "code",
largeText: "div",
regularText: "div",
smallText: "small",
lead: "p",
mutedText: "p",
ul: "ul",
};
export interface TypographyProps
extends React.HTMLAttributes<HTMLElement>,
VariantProps<typeof typographyVariants> {
asChild?: boolean
as?: string
}
const Typography = React.forwardRef<HTMLElement, TypographyProps>(
({className, variant, weight, as, asChild, ...props}, ref) => {
const Comp = asChild ? Slot : as ?? (variant ? variantElementMap[variant] : undefined) ?? "div"
return (
<Comp
className={cn(typographyVariants({variant, weight, className}))}
ref={ref}
{...props}
/>
)
}
)
Typography.displayName = "Typography"
export {Typography, typographyVariants}
Thanks for creating this component I was actually using this and sometimes it works and sometimes it doesn't for some reason, I also can see a warning about forwardRef in console:
Based on this comment: https://github.com/shadcn-ui/ui/pull/363#issuecomment-1597916990 and this comment: https://github.com/shadcn-ui/ui/pull/363#issuecomment-1636708882 I made this:
'use client';
import * as React from 'react';
import { VariantProps, cva } from 'class-variance-authority';
import { cn } from 'lib/utils';
import { Slot } from '@radix-ui/react-slot';
const typographyVariants = cva('text-foreground', {
variants: {
variant: {
h1: 'scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl',
h2: 'scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0',
h3: 'scroll-m-20 text-2xl font-semibold tracking-tight',
h4: 'scroll-m-20 text-xl font-semibold tracking-tight',
h5: 'scroll-m-20 text-lg font-semibold tracking-tight',
h6: 'scroll-m-20 text-base font-semibold tracking-tight',
p: 'leading-7 [&:not(:first-child)]:mt-6',
blockquote: 'mt-6 border-l-2 pl-6 italic',
ul: 'my-6 ml-6 list-disc [&>li]:mt-2',
inlineCode:
'relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold',
lead: 'text-xl text-muted-foreground',
largeText: 'text-lg font-semibold',
smallText: 'text-sm font-medium leading-none',
mutedText: 'text-sm text-muted-foreground',
},
},
defaultVariants: {
variant: "p",
},
});
type VariantPropType = VariantProps<typeof typographyVariants>;
const variantElementMap: Record<
NonNullable<VariantPropType['variant']>,
string
> = {
h1: 'h1',
h2: 'h2',
h3: 'h3',
h4: 'h4',
h5: 'h5',
h6: 'h6',
p: 'p',
blockquote: 'blockquote',
inlineCode: 'code',
largeText: 'div',
smallText: 'small',
lead: 'p',
mutedText: 'p',
ul: 'ul',
};
export interface TypographyProps
extends React.HTMLAttributes<HTMLElement>,
VariantProps<typeof typographyVariants> {
asChild?: boolean;
as?: string;
}
const Typography = React.forwardRef<HTMLElement, TypographyProps>(
({ className, variant, as, asChild, ...props }, ref) => {
const Comp = asChild
? Slot
: as ?? (variant ? variantElementMap[variant] : undefined) ?? 'div';
return (
<Comp
className={cn(typographyVariants({ variant, className }))}
ref={ref}
{...props}
/>
);
}
);
Typography.displayName = 'Typography';
export { Typography, typographyVariants };
The API like this feels really good to me also defaulting to p as would be one of the most used ones.
<Typography variant="h1" as="h4">
Test
</Typography>
any progress here? I'm available to finish this if @shadcn is interested in merge this.
I really like where this is going! I just spend the last couple evenings eventually getting to something very similar for my Typography component!
I additionally decided to create some semantic/wrapper components (Heading, Text, etc.) on top of the Typography component. I decided this after looking at what it looks like using the same "base" Typography component for everything (I didn't love it), as well as looking into what other UI component libraries were doing and seeing them be more semantic (e.g. Radix UI, etc.).
Semantic vs Non-Semantic
<div className="flex flex-row justify-center gap-6">
{/* Semantic */}
<div>
<Heading variant="h1">Heading 1</Heading>
<Heading variant="h2">Heading 2</Heading>
<Heading variant="h3">Heading 3</Heading>
<Heading variant="h4">Heading 4</Heading>
<Heading variant="h5">Heading 5</Heading>
<Heading variant="h6">Heading 6</Heading>
<Text variant="lead">This is a lead paragraph.</Text>
<Text variant="large">This is large text.</Text>
<Text variant="muted">This is muted text.</Text>
<Text variant="p">This is a paragraph.</Text>
<Text>This is a paragraph (without a variant prop).</Text>
<Text>
This is a paragraph <Em>with em</Em>.
</Text>
<Text>
This is a paragraph <Strong>with strong</Strong>.
</Text>
<Text>
This is a paragraph <Small>with small</Small>.
</Text>
<Blockquote>This is a blockquote.</Blockquote>
<Code>This is inline code.</Code>
</div>
{/* Non-semantic */}
<div>
<Typography variant="h1" gutterBottom>
Heading 1
</Typography>
<Typography variant="h2">Heading 2</Typography>
<Typography variant="h3">Heading 3</Typography>
<Typography variant="h4">Heading 4</Typography>
<Typography variant="h5">Heading 5</Typography>
<Typography variant="h6">Heading 6</Typography>
<Typography variant="p">This is a paragraph.</Typography>
<Typography>This is a paragraph (without a variant prop).</Typography>
<Typography variant="lead">This is a lead paragraph.</Typography>
<Typography variant="large">This is large text.</Typography>
<Typography variant="muted">This is muted text.</Typography>
<Typography variant="blockquote">This is a blockquote.</Typography>
<Typography variant="code">This is inline code.</Typography>
</div>
</div>
I adapted my semantic components to now use the "base" typography component that you guys have come up with, instead of my own -- specifically the one @joaopedrodcf posted.
To more easily create semantic components, I added a couple additions to the "base" typography component (could use some renaming):
// typography.tsx
...
type TypographyVariantType = NonNullable<
VariantProps<typeof typographyVariants>["variant"]
>
interface VariantPropsTypographyWithoutVariant
extends Omit<VariantProps<typeof typographyVariants>, "variant"> {
asChild?: boolean;
}
...
export type { VariantPropsTypographyWithoutVariant, TypographyVariantType };
I also created FilterUnionType to filter down the TypographyVariantType in individual semantic components based on what I wanted each to have available to them (while also keeping them connected to the "base" typography component):
// Helper type to filter union types
export type FilterUnionType<T, U> = T extends U ? T : never;
Some of my semantic components:
Heading (heading.tsx)
import React from "react";
import type { FilterUnionType } from "@/lib/types";
import {
Typography,
type VariantPropsTypographyWithoutVariant,
type TypographyVariantType,
} from "./typography";
// Specify the variants you want to allow (linting error will be thrown when using exported component with a variant (1) not specified here or (2) not within TypographyVariant)
type AllowedVariants = FilterUnionType<
TypographyVariantType,
"h1" | "h2" | "h3" | "h4" | "h5" | "h6"
>;
type HTMLTypographyElement = HTMLHeadingElement;
interface HeadingProps
extends React.HTMLAttributes<HTMLTypographyElement>,
VariantPropsTypographyWithoutVariant {
variant: AllowedVariants;
}
const Heading = React.forwardRef<HTMLTypographyElement, HeadingProps>(
({ variant, ...props }, ref) => {
return <Typography ref={ref} variant={variant} {...props} />;
},
);
export default Heading;
Text (text.tsx)
import React from "react";
import type { FilterUnionType } from "@/lib/types";
import {
Typography,
type VariantPropsTypographyWithoutVariant,
type TypographyVariantType,
} from "./typography";
// Specify the variants you want to allow (linting error will be thrown when using exported component with a variant (1) not specified here or (2) not within TypographyVariant)
type AllowedVariants = FilterUnionType<
TypographyVariantType,
"p" | "lead" | "largeText" | "mutedText" // ...smallText, etc.
>;
type HTMLTypographyElement = HTMLParagraphElement;
interface TextProps
extends React.HTMLAttributes<HTMLTypographyElement>,
VariantPropsTypographyWithoutVariant {
variant?: AllowedVariants;
}
const Text = React.forwardRef<HTMLTypographyElement, TextProps>(
({ variant = "p", ...props }, ref) => {
return (
<Typography
ref={ref}
variant={variant}
{...props}
/>
);
},
);
export default Text;
Blockquote (blockquote.tsx)
import React from "react";
import {
Typography,
type VariantPropsTypographyWithoutVariant,
type TypographyVariantType,
} from "./typography";
import type { FilterUnionType } from "@/lib/types";
// Specify the variants you want to allow (linting error will be thrown when using exported component with a variant (1) not specified here or (2) not within TypographyVariant)
type AllowedVariants = FilterUnionType<TypographyVariantType, "blockquote">;
type HTMLTypographyElement = HTMLQuoteElement;
export interface BlockquoteProps
extends React.HTMLAttributes<HTMLTypographyElement>,
VariantPropsTypographyWithoutVariant {
variant?: AllowedVariants;
}
const Blockquote = React.forwardRef<HTMLTypographyElement, BlockquoteProps>(
({ variant = "blockquote", ...props }, ref) => {
return <Typography ref={ref} variant={variant} {...props} />;
},
);
export default Blockquote;
Code (code.tsx)
import React from "react";
import {
Typography,
type VariantPropsTypographyWithoutVariant,
type TypographyVariantType,
} from "./typography";
import type { FilterUnionType } from "@/lib/types";
// Specify the variants you want to allow (linting error will be thrown when using exported component with a variant (1) not specified here or (2) not within TypographyVariant)
type AllowedVariants = FilterUnionType<TypographyVariantType, "inlineCode">;
type HTMLTypographyElement = React.ElementRef<"code">; // using React.ElementRef<"code"> instead of HTMLXElement because HTMLCodeElement is not a valid HTML element type
interface CodeProps
extends React.HTMLAttributes<HTMLTypographyElement>,
VariantPropsTypographyWithoutVariant {
variant?: AllowedVariants;
}
const Code = React.forwardRef<HTMLTypographyElement, CodeProps>(
({ variant = "inlineCode", ...props }, ref) => {
return <Typography ref={ref} variant={variant} {...props} />;
},
);
export default Code;
...I also started creating some "other" typography-related components (typography elements that are supposed to exist within other typography elements, etc.) (again, similarly to Radix UI). Structurally, they are a bit of a mix of shadcn and Radix UI:
Em (em.tsx)
import { cn } from "@/lib/utils";
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
const emVariants = cva("italic", {
variants: {
variant: {
default: "",
},
},
defaultVariants: {
variant: "default",
},
});
type EmElement = React.ElementRef<"em">;
export interface EmProps
extends React.ComponentPropsWithoutRef<"em">,
VariantProps<typeof emVariants> {
asChild?: boolean;
}
const Em = React.forwardRef<EmElement, EmProps>(
({ children, className, variant, ...props }, ref) => (
<em className={cn(emVariants({ variant, className }))} {...props} ref={ref}>
{children}
</em>
),
);
Em.displayName = "Em";
export default Em;
Strong (strong.tsx)
import { cn } from "@/lib/utils";
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
const strongVariants = cva("strong", {
variants: {
variant: {
default: "",
},
},
defaultVariants: {
variant: "default",
},
});
type StrongElement = React.ElementRef<"strong">;
export interface StrongProps
// extends React.HTMLAttributes<HTMLTypographyElement>,
extends React.ComponentPropsWithoutRef<"strong">,
VariantProps<typeof strongVariants> {
asChild?: boolean;
as?: string;
}
const Strong = React.forwardRef<StrongElement, StrongProps>(
({ children, className, variant, ...props }, ref) => (
<strong
className={cn(strongVariants({ variant, className }))}
{...props}
ref={ref}
>
{children}
</strong>
),
);
Strong.displayName = "Strong";
export default Strong;
Small (small.tsx)
import { cn } from "@/lib/utils";
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
const strongVariants = cva("small", {
variants: {
variant: {
default: "",
},
},
defaultVariants: {
variant: "default",
},
});
type SmallElement = React.ElementRef<"small">;
export interface SmallProps
// extends React.HTMLAttributes<HTMLTypographyElement>,
extends React.ComponentPropsWithoutRef<"small">,
VariantProps<typeof strongVariants> {
asChild?: boolean;
}
const Small = React.forwardRef<SmallElement, SmallProps>(
({ children, className, variant, ...props }, ref) => (
<small
className={cn(strongVariants({ variant, className }))}
{...props}
ref={ref}
>
{children}
</small>
),
);
Small.displayName = "Small";
export default Small;
I would love to hear your input!
Overall, I'm new to shadcn and I was just starting to go over the documentation and truly use it for the first time, but I got to the Typography section and went down this rabbit hole lol. I'm coming from using MUI for the last couple years, so... I may have gotten a bit excited about the possibility of semantic components (MUI = non-semantic Typography component for everything 😕)
import * as React from "react";
import Link, { LinkProps } from "next/link";
import { Slot } from "@radix-ui/react-slot";
import { VariantProps, cva } from "class-variance-authority";
import { cn } from 'lib/utils';
const typographyVariants = cva("text-foreground", {
variants: {
variant: {
a: "",
abbr: "",
b: "",
strong: "",
cite: "",
code: "",
em: "",
i: "",
sub: "",
sup: "",
u: "",
var: "",
p: "",
h1: "",
h2: "",
h3: "",
h4: "",
h5: "",
h6: "",
div: "",
span: "",
blockquote: "",
ul: "",
ol: "",
li: "",
large: "",
small: "",
lead: "",
muted: "",
},
},
});
type VariantPropType = VariantProps<typeof typographyVariants>;
const excludedVariants = ["a", "large", "lead", "muted"] as const;
type TypographyProps =
| (React.HTMLAttributes<HTMLElement> &
VariantPropType &
LinkProps & {
as: "a";
asChild?: boolean;
})
| (React.HTMLAttributes<HTMLElement> &
VariantPropType & {
as?: Exclude<
VariantPropType["variant"],
(typeof excludedVariants)[number]
>;
href?: never;
asChild?: boolean;
});
const Typography = React.forwardRef<HTMLElement, TypographyProps>(
({ className, variant, as, href, asChild, ...props }, ref) => {
const Component = asChild
? Slot
: as === "a"
? Link
: (as as React.ElementType) ||
(excludedVariants.includes(
variant as (typeof excludedVariants)[number],
)
? "p"
: variant) ||
"p";
return (
<Component
className={cn(typographyVariants({ variant, className }))}
ref={ref}
{...props}
href={as === "a" ? href : undefined}
/>
);
},
);
Typography.displayName = "Typography";
export { Typography, typographyVariants };
Add your styles. 👍
Adding my 2 cents on this which would allow for an api similar to MUI being
<Typography variant="h1">Hello World</Typography>; // renders as H1 by default
<Typography variant="h1" component='h2'>Hello World</Typography>; // renders as H2
While the code might seem like a-lot most if not all has been copied from MUI
import { cva, cx, VariantProps } from '@/cva.config';
type DistributiveOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
type OverrideProps<TProps, TComponent extends React.ElementType> = TProps &
DistributiveOmit<React.ComponentPropsWithRef<TComponent>, keyof TProps>;
interface OverrideAbleComponentFC<TDefaultComponent extends React.ElementType, TProps> {
<TComponent extends React.ElementType = TDefaultComponent>(
props: {
component?: TComponent;
} & OverrideProps<TProps, TComponent>
): JSX.Element | null;
displayName?: string;
}
export interface TypographyProps extends React.PropsWithChildren, VariantProps<typeof variants> {
className?: string;
}
const variants = cva({
variants: {
variant: {
h1: 'scroll-m-20 text-4xl font-bold text-primary-dark lg:text-5xl',
h2: 'mt-10 scroll-m-20 pb-2 text-3xl font-bold text-primary-dark first:mt-0 lg:text-4xl',
h3: 'mt-8 scroll-m-20 text-2xl font-bold text-primary-dark',
h4: 'mt-6 scroll-m-20 text-xl font-bold text-primary-dark',
p: 'leading-7 [&:not(:first-child)]:mt-4',
large: 'text-xl',
},
},
defaultVariants: {
variant: 'p',
},
});
const variantComponent = cva({
variants: {
variant: {
h1: 'h1',
h2: 'h2',
h3: 'h3',
h4: 'h4',
p: 'p',
large: 'div',
},
},
defaultVariants: {
variant: 'p',
},
});
export const Typography: OverrideAbleComponentFC<'p', TypographyProps> = ({
children,
className,
component,
variant = 'p',
}) => {
const Component = component || variantComponent({ variant });
return <Component className={cx(variants({ variant, className }))}>{children}</Component>;
};
@joaopedrodcf actually your component requires "use client" whenever used. Slot is causing this issue I guess.
Up :)
@amocarski updated to include the missing 'use client' thanks for noticing 🙏