diff --git a/apps/tester/tests/79-typography.spec.tsx b/apps/tester/tests/79-typography.spec.tsx
new file mode 100644
index 0000000..8dd03a3
--- /dev/null
+++ b/apps/tester/tests/79-typography.spec.tsx
@@ -0,0 +1,436 @@
+import InfoOutlined from '@mui/icons-material/InfoOutlined';
+import { MdInfoOutline } from 'react-icons/md';
+import { Typography as JoyTypography, Chip as JoyChip } from '@mui/joy';
+import { Typography as TJTypography } from 'tailwind-joy/components';
+
+import type { Fixture } from '@/settings';
+import { testEach } from '@/settings';
+
+const containerClassName =
+ 'flex h-[400px] w-[400px] items-center justify-center p-2';
+
+const fixtures: Fixture[] = [
+ {
+ title: 'levels',
+ alterSizes: ['md'],
+ alterVariants: [
+ // @ts-expect-error
+ undefined,
+ 'solid',
+ 'soft',
+ 'outlined',
+ 'plain',
+ ],
+ alterColors: [
+ // @ts-expect-error
+ undefined,
+ 'primary',
+ 'neutral',
+ 'danger',
+ 'success',
+ 'warning',
+ ],
+ renderJoyElement({ testId, size, variant, color }) {
+ return (
+
>`
+- Default:
+ ```tsx
+ {
+ h1: 'h1',
+ h2: 'h2',
+ h3: 'h3',
+ h4: 'h4',
+ 'title-lg': 'p',
+ 'title-md': 'p',
+ 'title-sm': 'p',
+ 'body-lg': 'p',
+ 'body-md': 'p',
+ 'body-sm': 'p',
+ 'body-xs': 'span',
+ inherit: 'p',
+ }
+ ```
+
+### `noWrap`
+
+If `true`, the text will not wrap, but instead will truncate with a text overflow ellipsis.
+
+- Type: `boolean`
+- Default: `false`
+
+### `slotProps`
+
+The props used for each slot inside.
+
+- Type:
+ ```tsx
+ {
+ root?: ComponentProps<'a'>;
+ startDecorator?: ComponentProps<'span'>;
+ endDecorator?: ComponentProps<'span'>;
+ }
+ ```
+- Default: `{}`
+
+### `startDecorator`
+
+Element placed before the children.
+
+- Type: `ReactNode`
+
+### `textColor`
+
+Arbitrary text color.
+
+- Type: `string`
+
+### `variant`
+
+The variant of the component.
+
+- Type: `'solid' | 'soft' | 'outlined' | 'plain'`
diff --git a/apps/website/docs/components/02-data-display/09-typography.mdx b/apps/website/docs/components/02-data-display/09-typography.mdx
new file mode 100644
index 0000000..f5e139c
--- /dev/null
+++ b/apps/website/docs/components/02-data-display/09-typography.mdx
@@ -0,0 +1,340 @@
+---
+sidebar_position: 9
+slug: /components/typography
+---
+
+import {
+ TypographyBasics,
+ TypographyHeading,
+ TypographyTitleAndBody,
+ TypographyNestedTypography,
+ TypographyLevels,
+ TypographySemanticElements,
+ TypographyDecorators,
+} from '@site/src/demos/typography';
+
+# Typography
+
+
+
+The Typography component helps present design and content clearly and efficiently.
+
+## Basics
+
+```tsx
+import { Typography } from 'tailwind-joy/components';
+```
+
+The Typography component wraps around its content, and displays text with specific typographic styles and properties.
+
+
+
+```tsx
+import { Sheet, Typography } from 'tailwind-joy/components';
+
+export function TypographyBasics() {
+ return (
+
+ National Parks
+
+ Yosemite National Park
+
+
+ Yosemite National Park is a national park spanning 747,956 acres
+ (1,169.4 sq mi; 3,025.2 km2) in the western Sierra Nevada of Central
+ California.
+
+
+ );
+}
+```
+
+### Heading
+
+Use `h1` through `h4` to render a headline.
+The produced HTML element will match the semantic [headings](https://www.w3.org/WAI/tutorials/page-structure/headings/) of the page structure.
+
+
+
+```tsx
+import { Box, Typography } from 'tailwind-joy/components';
+
+export function TypographyHeading() {
+ return (
+
+ h1: Lorem ipsum
+ h2: What is Lorem Ipsum?
+ h3: The standard Lorem Ipsum passage.
+ h4: The smallest headline of the page
+
+ );
+}
+```
+
+### Title and body
+
+Aside from the heading typographic levels, the Typography component also provides the `title-*` and `body-*` type levels.
+
+To ensure proper information hierarchy, we recommend combining them using either the same size or a lower one.
+For example, using `title-lg` with `body-lg` or `title-md` with `body-sm`.
+
+
+
+```tsx
+import { Box, Sheet, Typography } from 'tailwind-joy/components';
+
+export function TypographyTitleAndBody() {
+ return (
+
+
+
+ Title of the component{' '}
+
+ title-lg
+
+
+
+ This is the description of the component that contain some information
+ of it.{' '}
+
+ body-md
+
+
+
+
+
+ Title of the component{' '}
+
+ title-md
+
+
+
+ This is the description of the component that contain some information
+ of it.{' '}
+
+ body-md
+
+
+
+ Metadata, for example a date.{' '}
+
+ body-sm
+
+
+
+
+
+ Title of the component{' '}
+
+ title-sm
+
+
+
+ This is the description of the component that contain some information
+ of it.{' '}
+
+ body-sm
+
+
+
+ Metadata, for example a date.{' '}
+
+ body-xs
+
+
+
+
+ );
+}
+```
+
+### Nested Typography
+
+The Typography component renders as a `` by default.
+Nested Typography components are rendered as `` elements (unless customized by [the `component` prop](#semantic-elements)).
+
+
+
+```tsx
+import { Typography } from 'tailwind-joy/components';
+
+export function TypographyNestedTypography() {
+ return (
+
+ Typography lets you create nested{' '}
+ typography. Use your{' '}
+
+ imagination
+ {' '}
+ to build wonderful{' '}
+
+ user interface
+
+ .
+
+ );
+}
+```
+
+## Customization
+
+### Levels
+
+The `level` prop gives access to a pre-defined scale of typographic values defined in the theme.
+These values include various heading levels (h1, h2, h3, etc.) as well as body text levels (body-md, body-sm, etc) and can be used to apply consistent typography throughout your application.
+Additionally, you can also use the level prop to control the font size, weight, line height, and other typographic properties.
+
+
+
+```tsx
+import { Typography } from 'tailwind-joy/components';
+
+export function TypographyLevels() {
+ return (
+
+ h1
+ h2
+ h3
+ h4
+ title-lg
+ title-md
+ title-sm
+ body-lg
+ body-md
+ body-sm
+ body-xs
+
+ );
+}
+```
+
+### Semantic elements
+
+To customize the semantic element used, you can use the `component` prop.
+This can be useful in situations where you want to use a different semantic element than the one assigned by the `level` prop.
+The component will render as the HTML element defined by `component`, but with the styles assigned to its respective `level`.
+
+
+
+```tsx
+import { Typography } from 'tailwind-joy/components';
+
+export function TypographySemanticElements() {
+ return (
+
+ I render as an h2, but I have h1 styles
+
+ );
+}
+```
+
+### Decorators
+
+Use the `startDecorator` and `endDecorator` props to add supporting icons or elements to the Typography.
+
+
+
+```tsx
+import { MdInfoOutline } from 'react-icons/md';
+import { Typography } from 'tailwind-joy/components';
+import { iconClass } from 'tailwind-joy/utils';
+
+export function TypographyDecorators() {
+ return (
+
+ }
+ className="mb-4"
+ >
+ The icon automatically adjusts to the scale
+
+
+
+ 123
+
+
+ }
+ className="justify-center"
+ >
+ The display also changes to flexbox
+
+
+ );
+}
+```
+
+## Anatomy
+
+The Typography component is composed of a single root `` that's assigned the `body-md` class, unless these defaults are overridden by the [`level`](#levels) and/or [`component`](#semantic-elements) props.
+
+When one Typography component is nested within another, the nested component renders as a `` (unless customized as described above).
+
+```html
+
+
+
+
+
+
+```
+
+## API
+
+See the documentation below for a complete reference to all of the props available to the components mentioned here.
+
+- [``](../apis/box)
+- [``](../apis/sheet)
+- [``](../apis/typography)
diff --git a/apps/website/src/components/docs/DisplayStand.tsx b/apps/website/src/components/docs/DisplayStand.tsx
index e2da2f6..343abb8 100644
--- a/apps/website/src/components/docs/DisplayStand.tsx
+++ b/apps/website/src/components/docs/DisplayStand.tsx
@@ -8,6 +8,7 @@ export function DisplayStand({ children, className }: ComponentProps<'div'>) {
return (
+ {/* TODO: Replace Sheet with Card. */}
+
+ National Parks
+
+ Yosemite National Park
+
+
+ Yosemite National Park is a national park spanning 747,956 acres
+ (1,169.4 sq mi; 3,025.2 km2) in the western Sierra Nevada of Central
+ California.
+
+
+
+ );
+}
diff --git a/apps/website/src/demos/typography/Decorators.tsx b/apps/website/src/demos/typography/Decorators.tsx
new file mode 100644
index 0000000..66a5d57
--- /dev/null
+++ b/apps/website/src/demos/typography/Decorators.tsx
@@ -0,0 +1,39 @@
+import { MdInfoOutline } from 'react-icons/md';
+import { Typography } from 'tailwind-joy/components';
+import { iconClass } from 'tailwind-joy/utils';
+import { DisplayStand } from '@site/src/components/docs/DisplayStand';
+
+export function TypographyDecorators() {
+ return (
+
+
+ }
+ className="mb-4"
+ >
+ The icon automatically adjusts to the scale
+
+
+
+ 123
+
+
+ }
+ className="justify-center"
+ >
+ The display also changes to flexbox
+
+
+
+ );
+}
diff --git a/apps/website/src/demos/typography/Heading.tsx b/apps/website/src/demos/typography/Heading.tsx
new file mode 100644
index 0000000..159f7c0
--- /dev/null
+++ b/apps/website/src/demos/typography/Heading.tsx
@@ -0,0 +1,20 @@
+import { Box, Typography } from 'tailwind-joy/components';
+import { DisplayStand } from '@site/src/components/docs/DisplayStand';
+
+export function TypographyHeading() {
+ return (
+
+ {/* TODO: Replace Box with Stack. */}
+
+ h1: Lorem ipsum
+ h2: What is Lorem Ipsum?
+
+ h3: The standard Lorem Ipsum passage.
+
+
+ h4: The smallest headline of the page
+
+
+
+ );
+}
diff --git a/apps/website/src/demos/typography/Levels.tsx b/apps/website/src/demos/typography/Levels.tsx
new file mode 100644
index 0000000..b2a5665
--- /dev/null
+++ b/apps/website/src/demos/typography/Levels.tsx
@@ -0,0 +1,22 @@
+import { Typography } from 'tailwind-joy/components';
+import { DisplayStand } from '@site/src/components/docs/DisplayStand';
+
+export function TypographyLevels() {
+ return (
+
+
+ h1
+ h2
+ h3
+ h4
+ title-lg
+ title-md
+ title-sm
+ body-lg
+ body-md
+ body-sm
+ body-xs
+
+
+ );
+}
diff --git a/apps/website/src/demos/typography/NestedTypography.tsx b/apps/website/src/demos/typography/NestedTypography.tsx
new file mode 100644
index 0000000..c6e7d21
--- /dev/null
+++ b/apps/website/src/demos/typography/NestedTypography.tsx
@@ -0,0 +1,21 @@
+import { Typography } from 'tailwind-joy/components';
+import { DisplayStand } from '@site/src/components/docs/DisplayStand';
+
+export function TypographyNestedTypography() {
+ return (
+
+
+ Typography lets you create{' '}
+ nested typography. Use your{' '}
+
+ imagination
+ {' '}
+ to build wonderful{' '}
+
+ user interface
+
+ .
+
+
+ );
+}
diff --git a/apps/website/src/demos/typography/SemanticElements.tsx b/apps/website/src/demos/typography/SemanticElements.tsx
new file mode 100644
index 0000000..c31dc96
--- /dev/null
+++ b/apps/website/src/demos/typography/SemanticElements.tsx
@@ -0,0 +1,12 @@
+import { Typography } from 'tailwind-joy/components';
+import { DisplayStand } from '@site/src/components/docs/DisplayStand';
+
+export function TypographySemanticElements() {
+ return (
+
+
+ I render as an h2, but I have h1 styles
+
+
+ );
+}
diff --git a/apps/website/src/demos/typography/TitleAndBody.tsx b/apps/website/src/demos/typography/TitleAndBody.tsx
new file mode 100644
index 0000000..0773649
--- /dev/null
+++ b/apps/website/src/demos/typography/TitleAndBody.tsx
@@ -0,0 +1,113 @@
+import { Box, Sheet, Typography } from 'tailwind-joy/components';
+import { DisplayStand } from '@site/src/components/docs/DisplayStand';
+
+export function TypographyTitleAndBody() {
+ return (
+
+ {/* TODO: Replace Box with Stack. */}
+
+ {/* TODO: Replace Sheet with Card. */}
+
+
+ Title of the component{' '}
+
+ title-lg
+
+
+
+ This is the description of the component that contain some
+ information of it.{' '}
+
+ body-md
+
+
+
+ {/* TODO: Replace Sheet with Card. */}
+
+
+ Title of the component{' '}
+
+ title-md
+
+
+
+ This is the description of the component that contain some
+ information of it.{' '}
+
+ body-md
+
+
+
+ Metadata, for example a date.{' '}
+
+ body-sm
+
+
+
+ {/* TODO: Replace Sheet with Card. */}
+
+
+ Title of the component{' '}
+
+ title-sm
+
+
+
+ This is the description of the component that contain some
+ information of it.{' '}
+
+ body-sm
+
+
+
+ Metadata, for example a date.{' '}
+
+ body-xs
+
+
+
+
+
+ );
+}
diff --git a/apps/website/src/demos/typography/index.ts b/apps/website/src/demos/typography/index.ts
new file mode 100644
index 0000000..20ea74b
--- /dev/null
+++ b/apps/website/src/demos/typography/index.ts
@@ -0,0 +1,7 @@
+export { TypographyBasics } from './Basics';
+export { TypographyHeading } from './Heading';
+export { TypographyTitleAndBody } from './TitleAndBody';
+export { TypographyNestedTypography } from './NestedTypography';
+export { TypographyLevels } from './Levels';
+export { TypographySemanticElements } from './SemanticElements';
+export { TypographyDecorators } from './Decorators';
diff --git a/packages/tailwind-joy/README.md b/packages/tailwind-joy/README.md
index 5906dd5..3cc3a0b 100644
--- a/packages/tailwind-joy/README.md
+++ b/packages/tailwind-joy/README.md
@@ -22,6 +22,7 @@ https://tailwind-joy.vercel.app
### Data display
- Divider
+- Typography
### Feedback
diff --git a/packages/tailwind-joy/src/base/theme.ts b/packages/tailwind-joy/src/base/theme.ts
index 70f419f..13b939a 100644
--- a/packages/tailwind-joy/src/base/theme.ts
+++ b/packages/tailwind-joy/src/base/theme.ts
@@ -1081,4 +1081,194 @@ export const theme = {
},
},
},
+ typography: {
+ h1: {
+ className: [
+ 'font-[var(--joy-fontWeight-xl,700)]',
+ 'text-[length:var(--joy-fontSize-xl4,2.25rem)]',
+ '[line-height:var(--joy-lineHeight-xs,1.33334)]',
+ 'tracking-[-0.025em]',
+ colorTokens.text.primary,
+ ],
+ values: {
+ fontWeight: 'var(--joy-fontWeight-xl,700)',
+ fontSize: 'var(--joy-fontSize-xl4,2.25rem)',
+ lineHeight: 'var(--joy-lineHeight-xs,1.33334)',
+ letterSpacing: '-0.025em',
+ },
+ tokens: {
+ color: baseTokens.text.primary,
+ },
+ },
+ h2: {
+ className: [
+ 'font-[var(--joy-fontWeight-xl,700)]',
+ 'text-[length:var(--joy-fontSize-xl3,1.875rem)]',
+ '[line-height:var(--joy-lineHeight-xs,1.33334)]',
+ 'tracking-[-0.025em]',
+ colorTokens.text.primary,
+ ],
+ values: {
+ fontWeight: 'var(--joy-fontWeight-xl,700)',
+ fontSize: 'var(--joy-fontSize-xl3,1.875rem)',
+ lineHeight: 'var(--joy-lineHeight-xs,1.33334)',
+ letterSpacing: '-0.025em',
+ },
+ tokens: {
+ color: baseTokens.text.primary,
+ },
+ },
+ h3: {
+ className: [
+ 'font-[var(--joy-fontWeight-lg,600)]',
+ 'text-[length:var(--joy-fontSize-xl2,1.5rem)]',
+ '[line-height:var(--joy-lineHeight-xs,1.33334)]',
+ 'tracking-[-0.025em]',
+ colorTokens.text.primary,
+ ],
+ values: {
+ fontWeight: 'var(--joy-fontWeight-lg,600)',
+ fontSize: 'var(--joy-fontSize-xl2,1.5rem)',
+ lineHeight: 'var(--joy-lineHeight-xs,1.33334)',
+ letterSpacing: '-0.025em',
+ },
+ tokens: {
+ color: baseTokens.text.primary,
+ },
+ },
+ h4: {
+ className: [
+ 'font-[var(--joy-fontWeight-lg,600)]',
+ 'text-[length:var(--joy-fontSize-xl,1.25rem)]',
+ '[line-height:var(--joy-lineHeight-md,1.5)]',
+ 'tracking-[-0.025em]',
+ colorTokens.text.primary,
+ ],
+ values: {
+ fontWeight: 'var(--joy-fontWeight-lg,600)',
+ fontSize: 'var(--joy-fontSize-xl,1.25rem)',
+ lineHeight: 'var(--joy-lineHeight-md,1.5)',
+ letterSpacing: '-0.025em',
+ },
+ tokens: {
+ color: baseTokens.text.primary,
+ },
+ },
+ 'title-lg': {
+ className: [
+ 'font-[var(--joy-fontWeight-lg,600)]',
+ 'text-[length:var(--joy-fontSize-lg,1.125rem)]',
+ '[line-height:var(--joy-lineHeight-xs,1.33334)]',
+ colorTokens.text.primary,
+ ],
+ values: {
+ fontWeight: 'var(--joy-fontWeight-lg,600)',
+ fontSize: 'var(--joy-fontSize-lg,1.125rem)',
+ lineHeight: 'var(--joy-lineHeight-xs,1.33334)',
+ letterSpacing: undefined,
+ },
+ tokens: {
+ color: baseTokens.text.primary,
+ },
+ },
+ 'title-md': {
+ className: [
+ 'font-[var(--joy-fontWeight-md,500)]',
+ 'text-[length:var(--joy-fontSize-md,1rem)]',
+ '[line-height:var(--joy-lineHeight-md,1.5)]',
+ colorTokens.text.primary,
+ ],
+ values: {
+ fontWeight: 'var(--joy-fontWeight-md,500)',
+ fontSize: 'var(--joy-fontSize-md,1rem)',
+ lineHeight: 'var(--joy-lineHeight-md,1.5)',
+ letterSpacing: undefined,
+ },
+ tokens: {
+ color: baseTokens.text.primary,
+ },
+ },
+ 'title-sm': {
+ className: [
+ 'font-[var(--joy-fontWeight-md,500)]',
+ 'text-[length:var(--joy-fontSize-sm,0.875rem)]',
+ '[line-height:var(--joy-lineHeight-sm,1.42858)]',
+ colorTokens.text.primary,
+ ],
+ values: {
+ fontWeight: 'var(--joy-fontWeight-md,500)',
+ fontSize: 'var(--joy-fontSize-sm,0.875rem)',
+ lineHeight: 'var(--joy-lineHeight-sm,1.42858)',
+ letterSpacing: undefined,
+ },
+ tokens: {
+ color: baseTokens.text.primary,
+ },
+ },
+ 'body-lg': {
+ className: [
+ 'text-[length:var(--joy-fontSize-lg,1.125rem)]',
+ '[line-height:var(--joy-lineHeight-md,1.5)]',
+ colorTokens.text.secondary,
+ ],
+ values: {
+ fontWeight: undefined,
+ fontSize: 'var(--joy-fontSize-lg,1.125rem)',
+ lineHeight: 'var(--joy-lineHeight-md,1.5)',
+ letterSpacing: undefined,
+ },
+ tokens: {
+ color: baseTokens.text.secondary,
+ },
+ },
+ 'body-md': {
+ className: [
+ 'text-[length:var(--joy-fontSize-md,1rem)]',
+ '[line-height:var(--joy-lineHeight-md,1.5)]',
+ colorTokens.text.secondary,
+ ],
+ values: {
+ fontWeight: undefined,
+ fontSize: 'var(--joy-fontSize-md,1rem)',
+ lineHeight: 'var(--joy-lineHeight-md,1.5)',
+ letterSpacing: undefined,
+ },
+ tokens: {
+ color: baseTokens.text.secondary,
+ },
+ },
+ 'body-sm': {
+ className: [
+ 'text-[length:var(--joy-fontSize-sm,0.875rem)]',
+ '[line-height:var(--joy-lineHeight-md,1.5)]',
+ colorTokens.text.tertiary,
+ ],
+ values: {
+ fontWeight: undefined,
+ fontSize: 'var(--joy-fontSize-sm,0.875rem)',
+ lineHeight: 'var(--joy-lineHeight-md,1.5)',
+ letterSpacing: undefined,
+ },
+ tokens: {
+ color: baseTokens.text.tertiary,
+ },
+ },
+ 'body-xs': {
+ className: [
+ 'font-[var(--joy-fontWeight-md,500)]',
+ 'text-[length:var(--joy-fontSize-xs,0.75rem)]',
+ '[line-height:var(--joy-lineHeight-md,1.5)]',
+ colorTokens.text.tertiary,
+ ],
+ values: {
+ fontWeight: 'var(--joy-fontWeight-md,500)',
+ fontSize: 'var(--joy-fontSize-xs,0.75rem)',
+ lineHeight: 'var(--joy-lineHeight-md,1.5)',
+ letterSpacing: undefined,
+ },
+ tokens: {
+ color: baseTokens.text.tertiary,
+ },
+ },
+ },
};
diff --git a/packages/tailwind-joy/src/components.ts b/packages/tailwind-joy/src/components.ts
index b0a2741..3dea860 100644
--- a/packages/tailwind-joy/src/components.ts
+++ b/packages/tailwind-joy/src/components.ts
@@ -12,3 +12,4 @@ export { Radio } from './components/Radio';
export { RadioGroup } from './components/RadioGroup';
export { Sheet } from './components/Sheet';
export { Switch } from './components/Switch';
+export { Typography } from './components/Typography';
diff --git a/packages/tailwind-joy/src/components/Typography.tsx b/packages/tailwind-joy/src/components/Typography.tsx
new file mode 100644
index 0000000..cf8db10
--- /dev/null
+++ b/packages/tailwind-joy/src/components/Typography.tsx
@@ -0,0 +1,316 @@
+import { clsx } from 'clsx';
+import type { ComponentProps, ForwardedRef, ReactNode } from 'react';
+import {
+ createContext,
+ forwardRef,
+ createElement,
+ cloneElement,
+ useContext,
+ useMemo,
+} from 'react';
+import { twMerge } from 'tailwind-merge';
+import type {
+ BaseVariants,
+ GeneratorInput,
+ GenericComponentPropsWithVariants,
+} from '@/base/types';
+import { theme } from '../base/theme';
+import { baseTokens } from '../base/tokens';
+import { excludeClassName } from '../base/utils';
+
+type TypographyLevel =
+ | 'h1'
+ | 'h2'
+ | 'h3'
+ | 'h4'
+ | 'title-lg'
+ | 'title-md'
+ | 'title-sm'
+ | 'body-lg'
+ | 'body-md'
+ | 'body-sm'
+ | 'body-xs'
+ | 'inherit';
+
+export const TypographyNestedContext = createContext(false);
+
+const defaultLevelMapping: Record<
+ TypographyLevel,
+ keyof JSX.IntrinsicElements
+> = {
+ h1: 'h1',
+ h2: 'h2',
+ h3: 'h3',
+ h4: 'h4',
+ 'title-lg': 'p',
+ 'title-md': 'p',
+ 'title-sm': 'p',
+ 'body-lg': 'p',
+ 'body-md': 'p',
+ 'body-sm': 'p',
+ 'body-xs': 'span',
+ inherit: 'p',
+};
+
+function typographyStartDecoratorVariants() {
+ return twMerge(
+ clsx([
+ 'tj-typography-start-decorator',
+ 'inline-flex',
+ 'me-[clamp(4px,var(--Typography-gap,0.375em),0.75rem)]',
+ ]),
+ );
+}
+
+function typographyEndDecoratorVariants() {
+ return twMerge(
+ clsx([
+ 'tj-typography-end-decorator',
+ 'inline-flex',
+ 'ms-[clamp(4px,var(--Typography-gap,0.375em),0.75rem)]',
+ ]),
+ );
+}
+
+function typographyRootVariants(
+ props?: Pick
& {
+ gutterBottom?: boolean;
+ hasEndDecorator?: boolean;
+ hasSkeleton?: boolean;
+ hasStartDecorator?: boolean;
+ level?: TypographyLevel;
+ nesting?: boolean;
+ noWrap?: boolean;
+ },
+) {
+ const {
+ color,
+ variant,
+ gutterBottom = false,
+ hasEndDecorator = false,
+ hasSkeleton = false,
+ hasStartDecorator = false,
+ level = 'body-md',
+ nesting = false,
+ noWrap = false,
+ } = props ?? {};
+ const lineHeight =
+ level !== 'inherit' ? theme.typography[level].values.lineHeight : '1';
+
+ return twMerge(
+ clsx([
+ 'tj-typography-root group/tj-typography',
+ `[--Icon-fontSize:calc(1em*${lineHeight})]`,
+ color && '[--Icon-color:currentColor]',
+ 'm-[var(--Typography-margin,0px)]',
+ nesting ? 'inline' : ['block', hasSkeleton && 'relative'],
+ (hasStartDecorator || hasEndDecorator) && [
+ 'flex',
+ 'items-center',
+ nesting && ['inline-flex', hasStartDecorator && 'align-bottom'],
+ ],
+ level !== 'inherit' && theme.typography[level].className,
+ level !== 'inherit'
+ ? `text-[length:var(--Typography-fontSize,${theme.typography[level].values.fontSize})]`
+ : 'text-[length:var(--Typography-fontSize,inherit)]',
+ noWrap && 'truncate',
+ gutterBottom && 'mb-[0.35em]',
+ color &&
+ baseTokens[color].mainChannel.replace(
+ /(joy-[a-z0-9]+-[a-z0-9]+)/g,
+ 'text-[var(--variant-plainColor,var(--$1))]',
+ ),
+ variant && [
+ 'rounded-[2px]',
+ '[padding-block:min(0.1em,4px)]',
+ '[padding-inline:0.25em]',
+ !nesting && '[margin-inline:-0.25em]',
+ color && theme.variants[variant][color].className,
+ ],
+ ]),
+ );
+}
+
+type TypographyRootVariants = Pick & {
+ endDecorator?: ReactNode;
+ gutterBottom?: boolean;
+ level?: TypographyLevel;
+ levelMapping?: Partial>;
+ noWrap?: boolean;
+ startDecorator?: ReactNode;
+ textColor?: string;
+} & {
+ slotProps?: {
+ root?: ComponentProps<'a'>;
+ startDecorator?: ComponentProps<'span'>;
+ endDecorator?: ComponentProps<'span'>;
+ };
+};
+
+type TypographyRootProps = GenericComponentPropsWithVariants<
+ 'span',
+ TypographyRootVariants,
+ T
+>;
+
+function TypographyRoot<
+ T extends keyof JSX.IntrinsicElements | undefined = undefined,
+>(
+ {
+ // ---- non-passing props ----
+ // base variants
+ color,
+ variant,
+
+ // non-base variants
+ className,
+ endDecorator,
+ gutterBottom = false,
+ level,
+ levelMapping = defaultLevelMapping,
+ noWrap = false,
+ startDecorator,
+ style,
+ textColor,
+
+ // slot props
+ slotProps = {},
+
+ // others
+ component,
+ children,
+ ...otherProps
+ // ---------------------------
+ }: TypographyRootProps,
+ ref: ForwardedRef,
+) {
+ const nesting = useContext(TypographyNestedContext);
+ const slotPropsWithoutClassName = useMemo(
+ () => excludeClassName(slotProps),
+ [slotProps],
+ );
+
+ const instanceColor = variant ? (color ?? 'neutral') : color;
+
+ const instanceLevel = nesting ? level || 'inherit' : level || 'body-md';
+
+ // TODO: Implement this while implementing the skeleton component.
+ const hasSkeleton = false;
+
+ const instanceComponent =
+ component ||
+ (nesting
+ ? 'span'
+ : levelMapping[instanceLevel] ||
+ defaultLevelMapping[instanceLevel] ||
+ 'span');
+
+ return (
+
+ {createElement(
+ instanceComponent,
+ {
+ ref,
+ className: twMerge(
+ typographyRootVariants({
+ color: instanceColor,
+ variant,
+ gutterBottom,
+ hasEndDecorator: Boolean(endDecorator),
+ hasSkeleton,
+ hasStartDecorator: Boolean(startDecorator),
+ level: instanceLevel,
+ nesting,
+ noWrap,
+ }),
+ className,
+ slotProps.root?.className ?? '',
+ ),
+ style: {
+ ...style,
+ ...(textColor === undefined
+ ? {}
+ : {
+ color: textColor,
+ }),
+ },
+ ...otherProps,
+ ...(slotPropsWithoutClassName.root ?? {}),
+ },
+ <>
+ {startDecorator && (
+
+ {startDecorator}
+
+ )}
+ {hasSkeleton
+ ? cloneElement(children as JSX.Element, {
+ variant: (children as JSX.Element).props.variant || 'inline',
+ })
+ : children}
+ {endDecorator && (
+
+ {endDecorator}
+
+ )}
+ >,
+ )}
+
+ );
+}
+
+export const Typography = forwardRef(TypographyRoot) as <
+ T extends keyof JSX.IntrinsicElements | undefined = undefined,
+>(
+ props: TypographyRootProps & { ref?: ForwardedRef },
+) => JSX.Element;
+
+export const generatorInputs: GeneratorInput[] = [
+ {
+ generatorFn: typographyStartDecoratorVariants,
+ variants: {},
+ },
+ {
+ generatorFn: typographyEndDecoratorVariants,
+ variants: {},
+ },
+ {
+ generatorFn: typographyRootVariants,
+ variants: {
+ color: [undefined, 'primary', 'neutral', 'danger', 'success', 'warning'],
+ variant: [undefined, 'solid', 'soft', 'outlined', 'plain'],
+ gutterBottom: [false, true],
+ hasEndDecorator: [false, true],
+ hasSkeleton: [false, true],
+ hasStartDecorator: [false, true],
+ level: [
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'title-lg',
+ 'title-md',
+ 'title-sm',
+ 'body-lg',
+ 'body-md',
+ 'body-sm',
+ 'body-xs',
+ 'inherit',
+ ],
+ nesting: [false, true],
+ noWrap: [false, true],
+ },
+ },
+];
diff --git a/packages/tailwind-joy/src/plugins/safelist-generator.ts b/packages/tailwind-joy/src/plugins/safelist-generator.ts
index 129a4e2..938e420 100644
--- a/packages/tailwind-joy/src/plugins/safelist-generator.ts
+++ b/packages/tailwind-joy/src/plugins/safelist-generator.ts
@@ -13,6 +13,7 @@ import { generatorInputs as radioClassNameGeneratorInputs } from '../components/
import { generatorInputs as radioGroupClassNameGeneratorInputs } from '../components/RadioGroup';
import { generatorInputs as sheetClassNameGeneratorInputs } from '../components/Sheet';
import { generatorInputs as switchClassNameGeneratorInputs } from '../components/Switch';
+import { generatorInputs as typographyClassNameGeneratorInputs } from '../components/Typography';
import { generatorInputs as adaptedIconClassNameGeneratorInputs } from '../components/internal/class-adapter';
const SPACE = ' ';
@@ -32,6 +33,7 @@ const inputs: GeneratorInput[] = [
...radioGroupClassNameGeneratorInputs,
...sheetClassNameGeneratorInputs,
...switchClassNameGeneratorInputs,
+ ...typographyClassNameGeneratorInputs,
...adaptedIconClassNameGeneratorInputs,
];