Components / structure
Feed
Carousel controls had no aria-controls until the upstream port found it; the same class of defect is what aria-posinset prevents here — a structure the eye reads and the ear cannot.
Summary
A stream of articles that grows as the reader scrolls.
| APG pattern | Feed |
| Built on | native elements |
| Status | stable |
| Since | 0.1.0 |
<Feed label="Recent activity" busy={loading} total={340} onLoadMore={fetchMore} articles={…} />
Anatomy
A data-se="feed" region holding articles, each a focus stop.
| Part | Element | Required |
|---|---|---|
article | <article> | yes |
article-meta | <p> | no |
article-title | <h3> | yes |
busy | <div> | no |
label | <span> | yes |
Properties
| Property | Values | Default | What it does |
|---|---|---|---|
articles | FeedArticle[] | required | The loaded articles, oldest position first. Each is a focus stop and carries its position in the set. |
label | string | required | Names the feed region. |
busy | boolean | none | Sets aria-busy so a reader is not dropped into half-rendered content. |
total | number | none | Total across the whole stream, or -1 when genuinely unknown. |
offset | number | 0 | Index of the first rendered article within the whole feed. Required for a windowed or paged feed — without it article 101 announces as "1 of 340". |
onLoadMore | () => void | none | Called when the reader reaches the end. Set busy while the request is in flight, or a screen reader is read a half-inserted article. |
States
| State | Expressed as | |
|---|---|---|
| Pressed | :active | |
| Disabled | :disabled | |
| Focused | :focus-visible | |
| Hover | :hover |
Behavior
Page Down and Page Up move between articles. onLoadMore fires when the reader reaches the end.
busy sets aria-busy while more is arriving, so a screen reader is not read a half-inserted article.
Accessibility
Each article carries aria-posinset and aria-setsize, because "12 of 340" is the only way a screen-reader user can tell where they are in something with no visible end.
Pass total={-1} when the length is genuinely unknown. That is the honest value, not a guess — a wrong setsize is worse than none, because a reader believes it.
A native <article> carries no role attribute, so the port asserts the role by computation rather than by looking for a string.
Keyboard
| Key | Result |
|---|---|
Page Down / Page Up | Move to the next or previous article |
Home / End | Jump to the first or last loaded article |
Roles, states and properties
| Attribute | Set by |
|---|---|
aria-busy | set at render |
aria-describedby | set at render |
aria-label | set at render |
aria-labelledby | set at render |
aria-posinset | set at render |
aria-setsize | set at render |
role | always feed |
Focus
Each article is a focus stop carrying its position and the set size, because "12 of 340" is the only way a reader with no visible end can tell where they are.
Screen reader
Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.
Contrast and target size
npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.
It takes the touch floor under (pointer: coarse) — the visual size is unchanged on a desktop pointer.
Tested
test/apg/batch-a.test.jsxtest/apg/batch-b.test.jsxtest/apg/partials-sweep.test.jsxtest/browser/keyboard.spec.mjstest/browser/upstream-natives.spec.mjs
Known gaps
_No upstream requirement is recorded as not applicable._
Appearance
| Token | Used for |
|---|---|
--se-accent | intent |
--se-bg | background, border-color |
--se-border | border, border-color |
--se-border-width | border, border-width |
--se-control-h | block-size, inline-size |
--se-control-h-lg | block-size, inline-size |
--se-control-h-sm | block-size, inline-size |
--se-duration-instant | transition |
--se-ease-standard | transition |
--se-focus-color | outline |
--se-focus-offset | outline-offset |
--se-focus-width | outline |
--se-leading-heading | line-height |
--se-mix-down | background, border-color |
--se-mix-hover | background, border-color |
--se-mix-line | border-color |
--se-mix-press | background |
--se-mix-shade | background, border-color |
--se-mix-soft | background |
--se-negative | intent |
--se-positive | intent |
--se-radius-lg | border-radius |
--se-space-1 | margin |
--se-space-3 | padding-inline |
--se-space-4 | gap, padding, padding-inline |
--se-space-5 | padding-inline |
--se-surface | background |
--se-surface-sunk | background |
--se-target-min | block-size, inline-size, min-block-size |
--se-text | color, intent |
--se-text-disabled | color |
--se-text-md | font-size |
--se-text-muted | color |
--se-text-on-intent | color |
--se-text-sm | font-size |
--se-text-xs | font-size |
--se-tracking-heading | letter-spacing |
--se-weight-semibold | font-weight |
Motion
None. New articles appear; they do not slide in, because the reader may be mid-sentence above them.
Content
meta is the one line that places the article in time or context — 2 hours ago, Draft.
Usage
When not to use it. For a list a reader needs to get back to a known place in. A feed has no addresses; that is what Pagination is for.
Its keep-or-cut decision is on the v3 plan for week nine, and it has not been made.
Notes
Carousel controls had no aria-controls until the upstream port found it; the same class of defect is what aria-posinset prevents here — a structure the eye reads and the ear cannot.