Display primitives

Avatar, Progress, Separator, AspectRatio, ScrollArea, AccessibleIcon and VisuallyHidden. Each is one element with the right semantics, and between them they cost almost nothing — there is almost no code to ship.

import { Avatar, Progress, Separator, AspectRatio, ScrollArea } from '@apostel/bedrock'
Loading demo…
Source
import { AspectRatio, Avatar, Progress, ScrollArea, Separator } from '../../src/index'

/**
 * The primitives that are markup and nothing else. Each renders one element
 * with the right semantics — `<progress>`, `<hr>`, an `<img>` with a fallback
 * sibling — and between them they cost close to nothing, because there is
 * almost no code to ship.
 */
export default function DisplayDemo() {
  return (
    <div style={{ width: '100%' }}>
      <div className="demo-row">
        <Avatar.Root>
          <Avatar.Image src="/does-not-exist.png" alt="Ada Lovelace" />
          <Avatar.Fallback>AL</Avatar.Fallback>
        </Avatar.Root>
        <span>Fallback, because that image 404s.</span>
      </div>

      <div className="demo-row">
        <Progress.Root value={62} max={100} />
        <span>62%</span>
      </div>

      <div className="demo-row">
        <span>Left</span>
        <Separator.Root orientation="vertical" style={{ height: '1rem' }} />
        <span>Right</span>
      </div>

      <AspectRatio.Root ratio={16 / 9} style={{ background: 'var(--card)', maxWidth: '18rem' }}>
        <div style={{ padding: '.5rem' }}>16 / 9</div>
      </AspectRatio.Root>

      <ScrollArea.Root style={{ height: 90, width: '100%', maxWidth: '18rem', marginTop: '1rem' }}>
        <ScrollArea.Viewport>
          <div style={{ height: 400, padding: '.5rem' }}>
            Native overflow, scrolled by the browser — keyboard, wheel, trackpad momentum and the OS
            scrollbar preference all included.
          </div>
        </ScrollArea.Viewport>
        <ScrollArea.Scrollbar />
      </ScrollArea.Root>
    </div>
  )
}

Avatar

part renders notes
Root <span>
Image <img> Hides itself on error.
Fallback <span> Shown until the image loads, hidden once it does.

The swap is CSS driven off the image's own load state, not a useState fed by onLoad. An image that is already in the HTTP cache is therefore never a frame of flashing initials.

Progress

Renders <progress data-bedrock-progress>.

prop type notes
value number Omit it entirely for indeterminate.
max number Default 1, as the element's own default.

Progress.Indicator renders nothing — a <progress> has no children. Style ::-webkit-progress-value and ::-moz-progress-bar.

Indeterminate is the absence of value, which is the element's own rule rather than a prop bedrock invented.

Separator

Renders <div role="separator">, or nothing accessible at all when decorative.

prop type notes
orientation 'horizontal' | 'vertical' Written as aria-orientation.
decorative boolean Drops the role, so it is not announced.

A decorative rule between two paragraphs is noise in a screen reader. Marking it so is the difference between a separator and a line.

AspectRatio

Renders one <div> with the CSS aspect-ratio property.

prop type notes
ratio number e.g. 16 / 9. Default 1.

Radix uses a padding-bottom wrapper and a ResizeObserver. This is one property, supported everywhere, in one element.

ScrollArea

part renders notes
Root <div>
Viewport <div> overflow: auto. The browser scrolls it.
Scrollbar <div> Styling hook; the scrollbar itself is the UA's.
Thumb Renders nothing.
Corner Renders nothing.

Scrolling is native, so the keyboard, the wheel, trackpad momentum, and the reader's own "always show scrollbars" preference all work. Style the bar with scrollbar-width and scrollbar-color, or ::-webkit-scrollbar.

Radix renders custom scrollbars in JavaScript, which is how it can guarantee identical bars everywhere; the cost is momentum, accessibility settings and about 12 kB. This is the trade named plainly in gaps.

AccessibleIcon

Wraps a decorative glyph in one element and gives it a name for screen readers, using VisuallyHidden internally.

<AccessibleIcon.Root label="Delete">
  <TrashGlyph />
</AccessibleIcon.Root>

VisuallyHidden

Renders one <span> with the clip-path pattern: invisible on screen, still in the accessibility tree, and still focusable if it holds a control — which is how "skip to content" links work.

VISUALLY_HIDDEN is exported as a plain style object if you want the same treatment on an element bedrock is not rendering.

What is not here