diff --git a/.changeset/decouple-text-heading-semantics.md b/.changeset/decouple-text-heading-semantics.md new file mode 100644 index 0000000000..e8d2f00d46 --- /dev/null +++ b/.changeset/decouple-text-heading-semantics.md @@ -0,0 +1,23 @@ +--- +"@cloudflare/kumo": minor +--- + +feat(Text): decouple visual heading variants from semantic HTML elements + +**Breaking change:** `heading1`, `heading2`, `heading3` variants no longer auto-render `

`, `

`, `

` tags. They now render as `` by default. Use the `as` prop to set the appropriate semantic heading level for your document outline. + +Before: + +```tsx +Title // rendered

+``` + +After: + +```tsx + + Title + // explicit semantic element +``` + +The `as` prop is now restricted to valid text elements: `"h1"` through `"h6"`, `"p"`, and `"span"`. diff --git a/packages/kumo-docs-astro/src/components/demos/TextDemo.tsx b/packages/kumo-docs-astro/src/components/demos/TextDemo.tsx index 09bc96ce2f..16ec180b71 100644 --- a/packages/kumo-docs-astro/src/components/demos/TextDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/TextDemo.tsx @@ -4,15 +4,21 @@ export function TextVariantsDemo() { return (
- Heading 1 + + Heading 1 + text-3xl (30px)
- Heading 2 + + Heading 2 + text-2xl (24px)
- Heading 3 + + Heading 3 + text-lg (16px)
diff --git a/packages/kumo-docs-astro/src/pages/components/text.mdx b/packages/kumo-docs-astro/src/pages/components/text.mdx index 3e6d1e36d9..67ca21dda7 100644 --- a/packages/kumo-docs-astro/src/pages/components/text.mdx +++ b/packages/kumo-docs-astro/src/pages/components/text.mdx @@ -9,6 +9,7 @@ import ComponentExample from "~/components/docs/ComponentExample.astro"; import ComponentSection from "~/components/docs/ComponentSection.astro"; import Heading from "~/components/docs/Heading.astro"; import PropsTable from "~/components/docs/PropsTable.astro"; +import { Text } from "@cloudflare/kumo"; import { TextVariantsDemo, TextTruncateDemo, @@ -54,10 +55,33 @@ export default function Example() { ```
-

Restrictions

-

+ Semantic HTML + + The `variant` prop controls visual styling only—it does not determine the HTML element rendered. + Use the `as` prop to set the appropriate semantic element for your document outline: + + +```tsx +// Heading variants render as by default +Styled as h1, but renders a span + +// Use `as` to set semantic heading levels +Page Title +Section Title +Visually large, but semantically h3 +``` + + + The `as` prop accepts: `"h1"` through `"h6"`, `"p"`, and `"span"`. + Body variants default to `"p"`, while heading and mono variants default to `"span"`. + +

+ +
+ Restrictions + The `bold` and `size` props are intentionally restricted to the `base`, `secondary`, `success`, and `error` text variants. -

+
```tsx Body @@ -66,10 +90,10 @@ export default function Example() { Error ``` -

+ Monospace variants (`mono` and `mono-secondary`) can only set `size` to `lg` and cannot use the `bold` prop: -

+ ```tsx Monospace @@ -77,12 +101,13 @@ export default function Example() { Monospace // Doesn't compile ``` -

- Headings (i.e. `h1`, `h2` and `h3` variants) cannot use these props at all: -

+ + Headings (i.e. `heading1`, `heading2` and `heading3` variants) cannot use + these props at all: + ```tsx - + Heading 1 // Doesn't compile ``` @@ -95,9 +120,9 @@ export default function Example() { Truncate -

+ Use the `truncate` prop to clip overflowing text with an ellipsis. This adds `truncate min-w-0` classes, which is useful when `Text` is inside a flex or grid container. -

+
diff --git a/packages/kumo/src/components/text/text.tsx b/packages/kumo/src/components/text/text.tsx index 41dfa0a4de..6d110b6f74 100644 --- a/packages/kumo/src/components/text/text.tsx +++ b/packages/kumo/src/components/text/text.tsx @@ -5,7 +5,6 @@ import { type ForwardedRef, forwardRef, useMemo, - type ElementType, } from "react"; import { cn } from "../../utils/cn"; @@ -140,13 +139,24 @@ type Monospace = "mono" | "mono-secondary"; type TextSize = KumoTextSize; type TextVariant = KumoTextVariant; +/** Valid HTML elements for the Text component's `as` prop. */ +export type TextElement = + | "h1" + | "h2" + | "h3" + | "h4" + | "h5" + | "h6" + | "p" + | "span"; + type BaseTextProps = Omit< ComponentPropsWithoutRef<"span">, "className" | "style" > & { DANGEROUS_className?: string; DANGEROUS_style?: CSSProperties; - as?: ElementType; + as?: TextElement; }; type TextPropsInternal = BaseTextProps & @@ -213,20 +223,21 @@ export interface TextProps { bold?: boolean; /** Whether to truncate overflowing text with an ellipsis. Adds `truncate min-w-0` classes. */ truncate?: boolean; - /** The HTML element type to render as (e.g. `"span"`, `"p"`, `"h1"`). Auto-selected based on variant if omitted. */ - as?: ElementType; + /** The HTML element to render (`"h1"`-`"h6"`, `"p"`, or `"span"`). Defaults to `"p"` for body variants and `"span"` for headings/mono. Use this to set semantic heading levels. */ + as?: TextElement; /** Text content. */ children?: React.ReactNode; } /** * Typography component for rendering text with consistent styling. - * Automatically selects the appropriate HTML element based on variant - * (`h1`/`h2`/`h3` for headings, `p` for body, `span` for mono). + * Renders as `

` for body variants and `` for headings/mono. + * Use the `as` prop to set semantic HTML elements for proper document outlines. * * @example * ```tsx - * Dashboard + * Page Title + * Section Title * Default body text * ``` */ @@ -247,12 +258,14 @@ function _Text( const isCopy = ["body", "secondary", "success", "error"].includes(variant); const isMono = ["mono", "mono-secondary"].includes(variant); + // Heading variants no longer auto-select h1/h2/h3 to avoid coupling visual + // presentation to semantic HTML. Use the `as` prop to set the appropriate + // heading level for your document outline (e.g., as="h2"). const Component = useMemo(() => { if (as) return as; - if (variant === "heading1") return "h1"; - if (variant === "heading2") return "h2"; - if (variant === "heading3") return "h3"; if (["mono", "mono-secondary"].includes(variant)) return "span"; + // Headings and body text default to span; use `as` for semantic elements + if (["heading1", "heading2", "heading3"].includes(variant)) return "span"; return "p"; }, [variant, as]);