Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .changeset/eighty-pugs-repeat.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The resulting scroll-into-view puts the list behind React Aria's 300ms `pointer-
cooldown, which swallows a click on an option.

The observable failure was confined to browser-mode test runs, where the width never becomes
finite. In a real browser the first-pass window never reaches the DOM `ScrollView` settles its
width before commit so no change in behaviour is expected for `Select`, `ComboBox`,
finite. In a real browser the first-pass window never reaches the DOM, because `ScrollView` settles its
width before commit, so no change in behaviour is expected for `Select`, `ComboBox`,
`Autocomplete` or `TagField`. `ListBoxLayout` now rejects measurements taken in that state, closing
both paths.
2 changes: 1 addition & 1 deletion .changeset/olive-mice-brake.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
classes. It renders the positioned box itself and lets the child fill it, so a
Fragment or a non-element child is placed like anything else, and a `className`
you set on the icon or action is left alone instead of being merged. Rendered
geometry is unchanged; the icon and action each gain a wrapping `<span>`.
geometry is unchanged. The icon and action each gain a wrapping `<span>`.
16 changes: 16 additions & 0 deletions .changeset/prose-style-as-lint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@marigold/docs': patch

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ @marigold/cli needs a patch bump too.

npm publishes README.md with the package, and packages/cli/README.md is rewritten in 57 lines here (67 of the 135 fixes by your own count). Without a bump the npm page keeps the old prose indefinitely, so the rewrite never reaches the place the README is actually read.

Suggested change
'@marigold/docs': patch
'@marigold/docs': patch
'@marigold/cli': patch

The alternative is dropping {packages,themes,config}/*/README.md from .vale.ini scope, which would also remove half the prose churn from this PR.

---

Prose style is now enforced rather than remembered. Vale runs over the docs site, the
changesets and the published READMEs, wired into the pre-commit hook and a CI check, with
the rules in `.vale/styles/Marigold/` and the writing guidance in `CLAUDE.md`.

Three rules block CI: no em dashes, no semicolons in prose, and no en dash asides. Table
cells are exempt, because there an em dash is a legitimate "not applicable" marker, and
ranges keep the en dash so quoted component output stays accurate. All 135 existing
violations across 30 files are rewritten in this change, so the check starts green.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 The file count disagrees with itself. This says 30 files, the PR description says 28, and the branch touches 32 prose files (33 changed minus this changeset). Since this text ships to the published changelog, either fix the number or drop it and keep the violation count.

While you are in here: DST-1526 lists "Rewriting existing docs to pass" under Not included. I think overruling that is right, because vale . lints the whole repo, so fix-forward would leave the check red on day one no matter which files a PR touches. Worth saying so explicitly in the PR description so the reporter sees it was a decision rather than an oversight.


The en dash rule matches only the aside form (a letter, a spaced en dash, then a lowercase
letter). German uses a spaced en dash where English uses an em dash, which makes it an easy
slip, but a range reads as digits or a capital around the dash and stays legal.
2 changes: 1 addition & 1 deletion .changeset/stepper.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ feat(DST-1391): add `Stepper`, a progress indicator for multi-step tasks.

State is entirely consumer-owned. `completedKeys` is a set rather than a high-water mark, so non-contiguous completion coming from a server is expressible, and the component never infers that a step is finished: only your code knows whether validation passed. `selectableKeys` replaces the built-in "completed, errored, or current" rule when a backend decides what is reachable, and `disabledKeys` always wins over both. Errored steps stay clickable by default, so a user who is told a step failed has a way back to it, unless `selectableKeys` leaves them out.

Steps with an `href` render as real links and route through `RouterProvider`; steps without one render as buttons. Steps that are not reachable render as plain text rather than as disabled controls, since an unreachable step is not a disabled widget. `hideLabels` drops labels visually for flows with too many steps to label, keeping them for screen readers and adding a visible "Step 3 of 5" counter so sighted users still know how far along they are.
Steps with an `href` render as real links and route through `RouterProvider`. Steps without one render as buttons. Steps that are not reachable render as plain text rather than as disabled controls, since an unreachable step is not a disabled widget. `hideLabels` drops labels visually for flows with too many steps to label, keeping them for screen readers and adding a visible "Step 3 of 5" counter so sighted users still know how far along they are.
23 changes: 19 additions & 4 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
":rebaseStalePrs"
],
"timezone": "Europe/Berlin",

"ignoreScripts": true,
"branchConcurrentLimit": 5,

"packageRules": [
{
"matchDatasources": ["npm"],
Expand Down Expand Up @@ -117,7 +115,7 @@
"automerge": true
},
{
"description": "Hold @react-types/* at their last type-only versions (button ~3.15, checkbox ~3.10, grid ~3.3, table ~3.13). Newer minors invert the dependency direction: the type-only packages start depending on the full @react-spectrum/* component packages, which drag in @adobe/react-spectrum and a duplicate react-aria/react-stately copy, splitting the i18n + overlay React contexts. See #5485 and upstream https://github.com/adobe/react-spectrum/issues/10139. Patches within the type-only line stay allowed; minor/major are blocked (and must not automerge). Note that some of these (e.g. @react-types/grid 3.4.0) pull @react-spectrum/provider via a *peerDependency*, so `npm view <pkg> dependencies` looks clean while pnpm still auto-installs the peer verify with `scripts/check-react-aria-dedupe.mjs` after any install, not by reading the manifest. Drop this rule once Adobe ships type-only @react-types/* again.",
"description": "Hold @react-types/* at their last type-only versions (button ~3.15, checkbox ~3.10, grid ~3.3, table ~3.13). Newer minors invert the dependency direction: the type-only packages start depending on the full @react-spectrum/* component packages, which drag in @adobe/react-spectrum and a duplicate react-aria/react-stately copy, splitting the i18n + overlay React contexts. See #5485 and upstream https://github.com/adobe/react-spectrum/issues/10139. Patches within the type-only line stay allowed; minor/major are blocked (and must not automerge). Note that some of these (e.g. @react-types/grid 3.4.0) pull @react-spectrum/provider via a *peerDependency*, so `npm view <pkg> dependencies` looks clean while pnpm still auto-installs the peer \u2014 verify with `scripts/check-react-aria-dedupe.mjs` after any install, not by reading the manifest. Drop this rule once Adobe ships type-only @react-types/* again.",
"matchPackageNames": [
"@react-types/button",
"@react-types/checkbox",
Expand All @@ -126,6 +124,12 @@
],
"matchUpdateTypes": ["minor", "major"],
"enabled": false
},
{
"description": "Vale minors change rule semantics (native MDX in 3.18, JSX-children scoping in 3.19), so review the bump instead of automerging it",
"matchDepNames": ["vale-cli/vale"],
"automerge": false,
"minimumReleaseAge": "3 days"
}
],
"vulnerabilityAlerts": {
Expand All @@ -134,5 +138,16 @@
"schedule": ["at any time"],
"minimumReleaseAge": "0 days"
},
"osvVulnerabilityAlerts": true
"osvVulnerabilityAlerts": true,
"customManagers": [
{
"customType": "regex",
"description": "Vale is a downloaded binary, not an npm dependency, so no native manager sees its pin",
"managerFilePatterns": ["/^scripts/install-vale\\.mjs$/"],
"matchStrings": ["const VERSION = '(?<currentValue>[^']+)';"],
"depNameTemplate": "vale-cli/vale",
"datasourceTemplate": "github-releases",
"extractVersionTemplate": "^v(?<version>.+)$"
}
]
}
24 changes: 24 additions & 0 deletions .github/workflows/prose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Prose

on: ['push']

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
prose:
name: Check prose style
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Node (using .node-version)
uses: actions/setup-node@v6
with:
node-version-file: '.node-version'

- name: Install Vale (pinned in the script, checksum verified)
run: node scripts/install-vale.mjs

- name: Check prose style
run: .vale/bin/vale .
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,5 @@ next-env.d.ts
# Vitest browser-mode failure artifacts
**/__screenshots__
.vitest-attachments

.vale/bin/
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/sh
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
npx lint-staged
npx lint-staged --relative
2 changes: 1 addition & 1 deletion .lintstagedrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"*": "prettier --write"
"*": ["prettier --write", "node scripts/install-vale.mjs", ".vale/bin/vale"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ This defeats the installer's own platform fallback.

scripts/install-vale.mjs prints no pinned build for <key>; skipping and exits 0 when there is no asset for the platform, but the next command runs .vale/bin/vale unconditionally, so the commit fails with ENOENT instead of skipping.

Windows is the realistic case. There is no win32-x64 entry in ASSETS, and adding one would need zip extraction and a vale.exe name rather than tar xzf plus vale. As it stands a Windows contributor cannot commit at all.

Either add the win32 build to the script, or route the vale step through a small wrapper that no-ops when the binary is absent, so the graceful degradation the script already implements actually reaches the caller.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Two smaller things about this line.

Scope it. The * glob hands every staged file to Vale, including PNGs, SVGs and pnpm-lock.yaml. Nothing matches a .vale.ini section so nothing is reported, but the hook pays for it on every commit. The installer also receives the same filename list and ignores it, which reads as a lint-staged task but is really a setup step, so .husky/pre-commit ahead of npx lint-staged is a better home for it.

Suggested change
"*": ["prettier --write", "node scripts/install-vale.mjs", ".vale/bin/vale"]
"*": "prettier --write",
"*.{md,mdx}": ".vale/bin/vale"

(pairs with moving node scripts/install-vale.mjs into the husky hook)

--relative is load-bearing and invisible. If that flag is what makes the repo-relative .vale.ini section globs match, then dropping it later makes Vale silently lint nothing and exit 0 rather than failing loudly. One sentence in the CLAUDE.md Prose Style section would keep that from being rediscovered the hard way.

}
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
.npmrc
.nvmrc
.prettierignore
.vale.ini

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 .vale/bin/ needs to be ignored here too, or pnpm format breaks for everyone.

.gitignore hides the downloaded binary from git, but Prettier does not read .gitignore, and prettier --write "**/*" matches dotted directories. That is exactly why .vale.ini had to be added on this line. Reproduced on this branch:

$ npx prettier --check "**/*"
[error] No parser could be inferred for file ".vale/bin/vale".

The sequence hits every developer: commit once, the pre-commit hook downloads .vale/bin/vale, then the next pnpm format exits non-zero. CLAUDE.md tells people to run it before committing. CI stays green because the Format job never installs Vale, so nothing catches this before it lands.

Suggested change
.vale.ini
.vale.ini
.vale/bin/

.claude

coverage
Expand Down
21 changes: 21 additions & 0 deletions .vale.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
StylesPath = .vale/styles

MinAlertLevel = suggestion

[docs/content/**/*.mdx]
BasedOnStyles = Marigold

[.changeset/*.md]
BasedOnStyles = Marigold

[{README.md,contributing.md,CODE-OF-CONDUCT.md,MIGRATION-v18.md}]
BasedOnStyles = Marigold

[{packages,themes,config}/*/README.md]
BasedOnStyles = Marigold

[docs/content/releases/**/release.mdx]
BasedOnStyles =

[packages/*/src/**]
BasedOnStyles =
10 changes: 10 additions & 0 deletions .vale/styles/Marigold/EmDash.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
extends: existence
message: 'Rephrase with a comma, a colon, or a second sentence instead of an em dash.'
level: error
nonword: true
scope:
- paragraph
- list
- heading
tokens:
- '—'
10 changes: 10 additions & 0 deletions .vale/styles/Marigold/EnDash.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
extends: existence
message: 'Use a comma, a colon, or a second sentence instead of an en dash aside. German uses a spaced en dash where English uses an em dash. Ranges like `4–9` or `Jan 1 – Dec 31` are fine.'
level: error
nonword: true
scope:
- paragraph
- list
- heading
tokens:
- '[a-zA-Z] – [a-z]'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Noting the gap so it is on the record rather than rediscovered: the token misses an aside that opens with a capital or a digit, e.g. the API – React Aria wraps it – handles focus. Your PR description already explains that this is the price of keeping Reservix – Marigold and Jan 1 – Dec 31 legal, which is the right trade. No change requested.

10 changes: 10 additions & 0 deletions .vale/styles/Marigold/Semicolon.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
extends: existence
message: 'Use a period or a comma instead of a semicolon in prose.'
level: error
nonword: true
scope:
- paragraph
- list
- heading
tokens:
- ';'
29 changes: 29 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,40 @@ The CLI fetches from the Marigold docs site, caches for 24h, and works offline (

- **Typecheck**: Run `pnpm typecheck:only` after code changes
- **Lint**: Run `pnpm lint` to check code style
- **Prose**: Run `pnpm lint:prose` after editing docs prose (see [Prose Style](#prose-style))
- **Format**: Run `pnpm format` before committing
- **Branch from**: `main` (use GitHub Flow)
- **Changesets**: Use `pnpm changeset` for version management
- **Storybook**: Run `pnpm sb` to preview components locally

## Prose Style

Documentation prose is linted. `pnpm lint:prose` runs Vale over the docs site, the changesets
and the published READMEs, and the Prose CI check runs the same rules. The binary is pinned and downloaded by
`scripts/install-vale.mjs` on first use, and pinned there so Renovate can see it.

- **No em dashes.** Rephrase with a comma, a colon, or a second sentence. In a
`- **Term** — definition` list item, write `- **Term**: definition`, which is already the
dominant form in these docs.
- **No semicolons in prose.** Use a period or a comma. Semicolons in code are untouched:
fenced blocks, code spans and MDX `import` statements are all outside the linted scope.
- **No en dash asides.** German uses a spaced en dash where English uses an em dash, so this
is an easy slip to make. Ranges keep the en dash: `4–9`, `Jan 1 – Dec 31`.

Table cells are exempt from all three. There an em dash is a legitimate "not applicable"
marker, as in `| — (no class) |`.

These rules govern prose written **for a reader**: `docs/content/**`, `.changeset/*.md`,
published package READMEs, and the repo's top-level markdown. They do **not** govern prose
written for an agent. `CLAUDE.md`, `.memory/**`, `.claude/**`, `docs/superpowers/**` and
`packages/*/src/**/README.md` are deliberately out of scope, and `.claude/README.md` positively
_requires_ an em dash in skill descriptions. Do not "fix" those files. Generated output
(`CHANGELOG.md`, `docs/content/releases/*/release.mdx`) is out of scope too, which is why
changesets are linted at the source instead.

The rules live in `.vale/styles/Marigold/`. A rule at `error` blocks CI. A rule at `warning` is
advisory, which is how a new rule lands until its existing violations are cleaned up.

## Monorepo Structure

- `packages/components` - Core React components
Expand Down
4 changes: 2 additions & 2 deletions MIGRATION-v18.md
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ must remove the `SectionMessage.close` slot (see B6).
- **`SelectList` `layout` / `keyboardNavigationBehavior`** removed. The list is
always a `layout="grid"` GridList and react-aria derives
`keyboardNavigationBehavior: 'tab'` from that, so neither value ever reached the
DOM. Drop them; behavior is unchanged.
DOM. Drop them. Behavior is unchanged.
- **`Loader` `loaderType`**: the v17 JSDoc documented a `cycle` value that never
existed. The type only ever accepted `xloader` and `circle` (default `circle`),
so this is a doc fix, not a code change. Unrelated to the `Spinner` → `Loader`
Expand Down Expand Up @@ -735,7 +735,7 @@ keeps every call site valid:
| `Deal` | `BadgePercent` | `Zoom` | `ZoomIn` |
| `Print` | `Printer` | | |

`Close` is not in the official mapping table; it maps to Lucide's `X`. Three legacy
`Close` is not in the official mapping table. It maps to Lucide's `X`. Three legacy
names have no Lucide equivalent and each needs a decision: `PauseAlt` →
`CirclePause`, `PlayAlt` → `CirclePlay`, and `Whatsapp` is gone with no replacement
(Lucide dropped brand icons and it is not among the 13 retained ones, so vendor the
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ To start documentation follow these steps:

Releases are published by CI, not locally. `.github/workflows/release.yml` runs
[`changesets/action`](https://github.com/changesets/action) on every push to `main`: it either opens a
"release: version packages" PR that consumes the pending changesets, oronce that PR is merged
"release: version packages" PR that consumes the pending changesets, or, once that PR is merged,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Unrelated to this line, but this is the nearest anchor in the diff: the Command overview table above (around line 78) gains no pnpm lint:prose row, even though CLAUDE.md points people at the script. Worth a row next to pnpm lint.

publishes the bumped packages to npm.

So the only thing you do by hand is add a changeset with `pnpm changeset` when your change should
Expand All @@ -97,7 +97,7 @@ cause a version bump.

Prereleases use [changesets pre mode](https://github.com/changesets/changesets/blob/main/docs/prereleases.md),
which lives on `main` (there is no separate release branch). The mode is recorded in
`.changeset/pre.json`, and `pnpm changeset publish` derives the npm dist-tag from its `tag` field
`.changeset/pre.json`, and `pnpm changeset publish` derives the npm dist-tag from its `tag` field,
so while pre mode is active, everything publishes to that tag instead of `latest`.

```sh
Expand Down
14 changes: 7 additions & 7 deletions docs/content/components/__internal__/component-guidelines.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -116,24 +116,24 @@ adds no signal.
already captured by another, kept story (e.g. `Dialog.Basic` vs. the open-state
Dialog stories).
- The frame is **inherently flaky or non-deterministic** and can't be stabilized
(animations, time, random data) and the same surface is covered by a stable story.
(animations, time, random data), and the same surface is covered by a stable story.

**Keep the snapshot when** the story is the only one capturing a particular visual
**Keep the snapshot when** the story is the only one capturing a particular visual:
a unique variant, size, open overlay, error/edge state, or layout. When in doubt, keep it.

**This only turns off the snapshot.** Always keep the `play` test and any docs example
**This only turns off the snapshot.** Always keep the `play` test and any docs example.
`disableSnapshot` does not affect them.

**Mechanics:**

- **Named / base stories** add the parameter directly. Generated `.test()` children
- **Named / base stories**: add the parameter directly. Generated `.test()` children
inherit it via `combineParameters`, so disabling a base also disables its tests.
- **A single generated `.test()` story** use the 3-arg overload
- **A single generated `.test()` story**: use the 3-arg overload
`.test(name, annotations, fn)` so only the derivative is disabled, not its base.
- **Re-enabling a kept child** if a base is disabled but one of its `.test()` children
- **Re-enabling a kept child**: if a base is disabled but one of its `.test()` children
is the only frame capturing a state worth keeping, re-enable it explicitly with
`chromatic: { disableSnapshot: false }`.
- **Don't leave dead config** when you disable a snapshot, drop any now-unused
- **Don't leave dead config**: when you disable a snapshot, drop any now-unused
`chromatic.delay` (or similar) that only existed to stabilize the removed capture.

Example of how we write stories:
Expand Down
2 changes: 1 addition & 1 deletion docs/content/components/actions/toggle-button/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Use `<ToggleButton>` in toolbars, editors, and interactive surfaces, where the u

`<ToggleButton>` is not a form field or a filter. It has no label, description, error message, or validation state. Reach for [Checkbox](/components/form/checkbox), [Radio](/components/form/radio), or [Switch](/components/form/switch) when you need a form field, and use a `<Checkbox.Group>` for filtering.

`<ToggleButton.Group>` is a toolbar of independent on/off actions, not a selection control. Each button in the group toggles on its own, like formatting options, view options, or visible layers. When exactly one option must be selected at a timeswitching a view, mode, or filter use a [SegmentedControl](/components/form/segmented-control) instead.
`<ToggleButton.Group>` is a toolbar of independent on/off actions, not a selection control. Each button in the group toggles on its own, like formatting options, view options, or visible layers. When exactly one option must be selected at a time, switching a view, mode, or filter, use a [SegmentedControl](/components/form/segmented-control) instead.

## Anatomy

Expand Down
4 changes: 2 additions & 2 deletions docs/content/components/collection/actionbar/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ The most common home for an `<ActionBar>` is a [`<Table>`](/components/collectio

<ComponentDemo name="actionbar-table-bulk-actions" />

Inside the render prop you write only the actions; the selection count and the clear button are filled in for you. The bar stays pinned to the bottom of the scroll area as the user works down a long list.
Inside the render prop you write only the actions. The selection count and the clear button are filled in for you. The bar stays pinned to the bottom of the scroll area as the user works down a long list.

<Callout title="Acting on a full selection" type="info">
When the user toggles the Table's select-all checkbox, the selection becomes
Expand Down Expand Up @@ -101,7 +101,7 @@ Give every icon-only action a text label as well, either visible next to the ico

### Actions

Actions are plain [`<Button>`](/components/actions/button) elements. Place them directly inside the bar and it cascades the toolbar look (a `ghost` variant at the default size); a local `variant` or `size` on a button still wins. For an icon-only action, set `size="icon"` and give it an `aria-label`.
Actions are plain [`<Button>`](/components/actions/button) elements. Place them directly inside the bar and it cascades the toolbar look (a `ghost` variant at the default size). A local `variant` or `size` on a button still wins. For an icon-only action, set `size="icon"` and give it an `aria-label`.

```tsx
<ActionBar selectedItemCount={3} onClearSelection={clear}>
Expand Down
2 changes: 1 addition & 1 deletion docs/content/components/collection/card/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ Every slot is inset with `p="square-regular"`, mirroring `Panel`. Use `square-lo
</Card>
```

A bled `<Card.Content>` publishes the card's horizontal padding as `--bleed-px`, so edge-aware children realign with the title on their own: `<Table>` pulls its first and last cell in while row backgrounds still reach the edge, and `<Accordion>` insets its header and content while its dividers span the full width. Custom content can read the same variable — see [Reading container spacing](/foundations/spacing#reading-container-spacing).
A bled `<Card.Content>` publishes the card's horizontal padding as `--bleed-px`, so edge-aware children realign with the title on their own: `<Table>` pulls its first and last cell in while row backgrounds still reach the edge, and `<Accordion>` insets its header and content while its dividers span the full width. Custom content can read the same variable. See [Reading container spacing](/foundations/spacing#reading-container-spacing).

## Accessibility

Expand Down
Loading
Loading