Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b333d8f
feat(ui): add h1–h6 base typography styles to global.css
MartinS-git Jun 8, 2026
6c58a32
feat(ui): add h1–h6 base typography styles to theme.css
MartinS-git Jun 8, 2026
26d2e58
refactor(ui): replace line-height values with Tailwind leading utilit…
MartinS-git Jun 8, 2026
11434dc
feat(ui): align FormattedText heading styles with typography scale
MartinS-git Jun 8, 2026
72c66a4
chore: add changeset for typography scale headings
MartinS-git Jun 8, 2026
1d6fc70
refactor(ui): move h1–h6 into @layer base and use var(--font-sans) in…
MartinS-git Jun 8, 2026
4a60bfe
fix(ui): remove font-bold and text-lg overrides from ContentHeading
MartinS-git Jun 10, 2026
90adb97
chore: update changeset with full list of typography changes
MartinS-git Jun 10, 2026
7aac887
refactor(ui): replace @apply leading utilities with plain CSS in glob…
MartinS-git Jun 10, 2026
314d330
chore(ui): remove Tailwind utility comments from h1–h6 line-height va…
MartinS-git Jun 10, 2026
07a62e7
fix(ui): replace h1 misuse in components with semantically correct el…
MartinS-git Jun 11, 2026
4c98064
fix(ui): update SignInForm test to reflect removed text-xl/font-bold …
MartinS-git Jun 11, 2026
3805d85
fix(ui): use h4 for ReactNode modal titles and add heading level asse…
MartinS-git Jun 11, 2026
713e76b
fix(ui): update Modal test to use heading level 4 instead of div for …
MartinS-git Jun 11, 2026
be77926
fix(ui): replace hardcoded font-weight with Tailwind utility and fix …
MartinS-git Jun 12, 2026
35bdb33
fix(ui): remove jn: prefix from @apply font-bold in theme.css for con…
MartinS-git Jun 12, 2026
4af9079
fix(ui): add typography styles to Modal ReactNode title div and corre…
MartinS-git Jun 12, 2026
f2aaa3f
fix(ui): align Modal ReactNode title typography with h4 base styles (…
MartinS-git Jun 12, 2026
c7eb1ab
fix(ui): add jn:font-sans to Modal ReactNode titlestyles for font-fam…
MartinS-git Jun 12, 2026
77631d9
fix(ui): apply titlestyles to both h4 and div Modal title paths for f…
MartinS-git Jun 12, 2026
05747d1
fix(ui): remove redundant span wrapper in PopupMenuSectionHeading to …
MartinS-git Jun 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/typography-scale-headings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@cloudoperators/juno-ui-components": minor
---

Add h1–h6 base typography styles aligned with the Juno design system scale.

- `global.css` and `theme.css`: h1–h6 defined in `@layer base` with IBM Plex Sans Bold, rem font sizes, and plain CSS `line-height` values. `theme.css` uses `var(--font-sans)` instead of a hardcoded font stack.
- `FormattedText`: heading sizes and line-heights aligned with the scale; h5 corrected (1.03rem → 1.125rem); h6 added; redundant `font-weight`/`font-style` declarations removed.
- `ContentHeading`: removed `jn:font-bold` and `jn:text-lg` overrides that were overriding the h1 base style with a smaller size.

Fix h1 misuse in components that rendered UI labels as `<h1>`, which caused unintended size changes after the global h1 style was introduced. Heading levels were chosen to reflect both visual size and accessibility (correct heading outline for screen readers).

- `Modal`: title changed from `<h1>` to `<h4>`. The modal has `role="dialog"` which creates an isolated landmark; the title is referenced via `aria-labelledby`. A heading inside a dialog is semantically correct and independent of the page outline. `<h4>` matches the previous visual size (~20px).
- `Form`: title changed from `<h1>` to `<h3>`. A form is a named content section; a heading is appropriate. `<h3>` matches the previous visual size (~24px).
- `FormSection`: title changed from `<h1>` to `<h4>`. One level below `Form` (`<h3>`) in the heading hierarchy. `<h4>` was chosen over `<h5>` (visual match) to maintain correct nesting.
- `SignInForm`: title changed from `<h1>` to `<h2>`. The form is often the primary content of a page but may also be embedded; `<h2>` is a safe default for both cases.
- `IntroBox`: title changed from `<h1>` to `<p>`. An info box is not a navigable section; adding it to the heading outline would confuse screen reader users navigating by heading. Bold styling preserved via existing `introboxHeading` class.
- `global.css`: replaced `font-weight: 700` with `@apply jn:font-bold` in h1–h6 base styles for consistency with Tailwind conventions.
- `theme.css`: same — replaced `font-weight: 700` with `@apply font-bold` (no `jn:` prefix, consistent with existing `@apply` usage in that file).
- `Modal`: ReactNode titles now render as `<div role="heading" aria-level={4}>` instead of `<h4>` to avoid invalid HTML (block elements inside heading elements). String titles remain `<h4>`. Both approaches are equivalent for assistive technologies.
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
import React, { HTMLAttributes, ReactNode } from "react"

const baseHeadingStyles = `
jn:font-bold
jn:text-lg
jn:text-theme-high
jn:pb-2
`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ const formBaseStyles = `
`

const formTitleStyles = `
jn:text-2xl
jn:font-bold
jn:mb-4
`

Expand Down Expand Up @@ -45,7 +43,7 @@ export interface FormProps extends FormHTMLAttributes<HTMLFormElement> {
export const Form = ({ title = "", className = "", children, ...props }: FormProps): ReactNode => {
return (
<form className={`juno-form ${formBaseStyles} ${className}`} {...props}>
{title ? <h1 className={`juno-form-heading ${formTitleStyles}`}>{title}</h1> : null}
{title ? <h3 className={`juno-form-heading ${formTitleStyles}`}>{title}</h3> : null}
{children}
</form>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ const formSectionBaseStyles = `
`

const headingStyles = `
jn:text-lg
jn:font-bold
jn:mb-4
`

Expand Down Expand Up @@ -45,7 +43,7 @@ export interface FormSectionProps extends HTMLAttributes<HTMLElement> {
export const FormSection = ({ title = "", children, className = "", ...props }: FormSectionProps): ReactNode => {
return (
<section className={`juno-form-section ${formSectionBaseStyles} ${className}`} {...props}>
{title ? <h1 className={`juno-formsection-heading ${headingStyles}`}>{title}</h1> : null}
{title ? <h4 className={`juno-formsection-heading ${headingStyles}`}>{title}</h4> : null}
{children}
</section>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,42 +36,40 @@

h1 {
font-size: 1.69rem;
font-weight: 700;
line-height: 1.11; /* round(40 / 36) */
line-height: 1.375;
margin-top: 0;
margin-bottom: 2rem; /* rem(32) */
}

h2 {
font-size: 1.56rem;
font-weight: 700;
line-height: 160%;
line-height: 1.375;
margin-top: 3rem; /* rem(48) */
margin-bottom: 1.5rem; /* rem(24) */
}

h3 {
font-size: 1.44rem;
font-weight: 700;
line-height: 160%;
line-height: 1.375;
margin-top: 1rem;
margin-bottom: 0.75rem; /* rem(12) */
}

h4 {
font-size: 1.28rem;
font-style: normal;
font-weight: 700;
line-height: 160%;
line-height: 1.625;
margin-top: 1.5rem; /* rem(24) */
margin-bottom: 0.5rem; /* rem(8) */
}

h5 {
font-size: 1.03rem;
font-style: normal;
font-weight: 700;
line-height: 160%;
font-size: 1.125rem;
line-height: 1.625;
}

h6 {
font-size: 1rem;
line-height: 1.5;
}

img,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export const IntroBox = ({
>
<div className={`${introboxBorder}`}></div>
<div className={`${introboxContent(variant, heroImage)}`}>
{title ? <h1 className={`${introboxHeading}`}>{title}</h1> : ""}
{title ? <p className={`${introboxHeading}`}>{title}</p> : ""}
{children ? children : <p>{text}</p>}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ export const Message = ({
<div className={`juno-message-border ${messageBorderStyles} ${variantStyle}`}></div>
<Icon icon={iconToRender} size={21} className={`${getIconStyles(variant)} ${messageIconStyles}`} />
<div className={`juno-message-content ${messageContentStyles}`}>
{title && <h1 className={messageHeadingStyles}>{title}</h1>}
{title && <strong className={messageHeadingStyles}>{title}</strong>}
<div>{children || text}</div>
</div>
{dismissible && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ const headerstyles = `
`

const titlestyles = `
jn:text-xl
jn:font-sans
jn:text-[1.28rem]
jn:leading-relaxed
jn:font-bold
`
Comment thread
MartinS-git marked this conversation as resolved.

Expand Down Expand Up @@ -177,13 +179,13 @@ export const Modal = ({
}
if (typeof modalTitle === "string") {
return (
<h1 className={`juno-modal-title ${titlestyles}`} id={modalTitleId}>
<h4 className={`juno-modal-title ${titlestyles}`} id={modalTitleId}>
{modalTitle}
</h1>
</h4>
Comment thread
MartinS-git marked this conversation as resolved.
)
}
return (
<div className={`juno-modal-title ${titlestyles}`} id={modalTitleId}>
<div className={`juno-modal-title ${titlestyles}`} role="heading" aria-level={4} id={modalTitleId}>
{modalTitle}
</div>
)
Comment thread
MartinS-git marked this conversation as resolved.
Comment thread
MartinS-git marked this conversation as resolved.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe("Modal", () => {
)

const dialog = screen.getByRole("dialog")
const titleWrapper = screen.getByText("Node Title").closest("div")
const titleWrapper = screen.getByRole("heading", { level: 4 })

expect(dialog).toBeInTheDocument()
expect(titleWrapper).toBeInTheDocument()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ export const PopupMenuSectionHeading = ({
}: PopupMenuSectionHeadingProps): ReactNode => {
return (
<header className={`juno-popupmenu-section-title ${sectionTitleStyles} ${className}`} {...props}>
<h1>{label && label.length ? label : children}</h1>
<span>{label && label.length ? label : children}</span>
Comment thread
Copilot marked this conversation as resolved.
Outdated
</header>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import { Message } from "../Message"
import { Stack } from "../Stack"

const signInFormTitleStyles = `
jn:text-xl
jn:font-bold
jn:text-theme-highest
jn:mb-4
`
Expand Down Expand Up @@ -75,7 +73,7 @@ export const SignInForm = ({

return (
<form className={`juno-sign-in-form ${className}`} {...props}>
{title !== false && <h1 className={`juno-sign-in-form-heading ${signInFormTitleStyles}`}>{title}</h1>}
{title !== false && <h2 className={`juno-sign-in-form-heading ${signInFormTitleStyles}`}>{title}</h2>}

{errorMessage && <Message variant="error" text={errorMessage} className="jn:mb-4" />}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ describe("SignInForm Component Tests", () => {
describe("Title Prop", () => {
test("renders default title 'Sign In' when no title prop is passed", () => {
render(<SignInForm data-testid="my-signin-form" />)
expect(screen.getByRole("heading")).toBeInTheDocument()
expect(screen.getByRole("heading")).toHaveClass("juno-sign-in-form-heading")
expect(screen.getByRole("heading")).toHaveTextContent("Sign In")
expect(screen.getByRole("heading", { level: 2 })).toBeInTheDocument()
expect(screen.getByRole("heading", { level: 2 })).toHaveClass("juno-sign-in-form-heading")
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent("Sign In")
})

test("renders custom title when string is passed", () => {
render(<SignInForm data-testid="my-signin-form" title="Welcome Back" />)
expect(screen.getByRole("heading")).toBeInTheDocument()
expect(screen.getByRole("heading")).toHaveTextContent("Welcome Back")
expect(screen.getByRole("heading", { level: 2 })).toBeInTheDocument()
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent("Welcome Back")
})

test("does not render title when title={false}", () => {
Expand All @@ -61,16 +61,14 @@ describe("SignInForm Component Tests", () => {

test("renders empty string title as empty heading", () => {
render(<SignInForm data-testid="my-signin-form" title="" />)
const heading = screen.queryByRole("heading")
const heading = screen.queryByRole("heading", { level: 2 })
expect(heading).toBeInTheDocument()
expect(heading).toHaveTextContent("")
})

test("applies correct styling to title", () => {
render(<SignInForm data-testid="my-signin-form" title="Test Title" />)
const heading = screen.getByRole("heading")
expect(heading).toHaveClass("jn:text-xl")
expect(heading).toHaveClass("jn:font-bold")
const heading = screen.getByRole("heading", { level: 2 })
expect(heading).toHaveClass("jn:text-theme-highest")
})
Comment thread
MartinS-git marked this conversation as resolved.
})
Expand Down
39 changes: 39 additions & 0 deletions packages/ui-components/src/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,45 @@
@apply jn:text-theme-link;
}

h1,
h2,
h3,
h4,
h5,
h6 {
@apply jn:font-sans jn:font-bold;
}

h1 {
font-size: 1.69rem;
line-height: 1.375;
}
Comment thread
MartinS-git marked this conversation as resolved.

h2 {
font-size: 1.56rem;
line-height: 1.375;
}

h3 {
font-size: 1.44rem;
line-height: 1.375;
}

h4 {
font-size: 1.28rem;
line-height: 1.625;
}

h5 {
font-size: 1.125rem;
line-height: 1.625;
}

h6 {
font-size: 1rem;
line-height: 1.5;
}

/* adds pointer cursor to buttons to restore tw3 behavior */
button:not(:disabled),
[role="button"]:not(:disabled) {
Expand Down
42 changes: 42 additions & 0 deletions packages/ui-components/src/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -1122,3 +1122,45 @@
--tw-content: "";
}
}

@layer base {
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-sans);
@apply font-bold;
}

h1 {
font-size: 1.69rem;
line-height: 1.375;
}

h2 {
font-size: 1.56rem;
line-height: 1.375;
}

h3 {
font-size: 1.44rem;
line-height: 1.375;
}

h4 {
font-size: 1.28rem;
line-height: 1.625;
}

h5 {
font-size: 1.125rem;
line-height: 1.625;
}

h6 {
font-size: 1rem;
line-height: 1.5;
}
}
Loading