Five New UI Primitives in rn-markdown-editor v1.0.22 — DropdownMenu, Accordion, Avatar, Card, and Radio

in Steem Dev8 hours ago

Previously:


Version v1.0.22 is now live on npm. This release delivers the five components we previewed in the v1.0.21 roadmap post — each one is fully themeable, built with React.memo for performance, and follows the same API conventions as the rest of the package. Here's a detailed breakdown of what shipped and how each component works.


Accordion

The Accordion component provides collapsible sections for progressive disclosure — FAQs, settings panels, expandable detail views, and anything else where you want content hidden until the user asks for it.

How it works internally:

State is managed by useDisclosure, keeping open/closed transitions predictable and consistent with how other components in the package handle visibility. The expand and collapse animation is driven by react-native-reanimated — specifically useSharedValue and withTiming — so it runs on the UI thread and stays smooth even when the JS thread is busy.

The height animation solves a real problem: you can't animate to height: "auto" in React Native. The component measures the actual content height via onLayout, stores it, and then animates between 0 and that measured value. A -1 sentinel on the shared value signals "not yet measured," which lets the component skip the animation on the very first render when defaultOpen is true.

The chevron icon rotates 180° on toggle, also animated via useSharedValue, giving clear visual feedback about the open/closed state.

<Accordion title="What is rn-markdown-editor?" defaultOpen={false}>
  <Text>A fully themeable markdown editor for React Native and Expo.</Text>
</Accordion>

Props:

PropTypeDefaultDescription
title`string \ReactNode`Header content
childrenReactNodeCollapsible body
defaultOpenbooleanfalseWhether expanded on mount

Avatar

The Avatar component renders a user's profile image when available, and falls back gracefully when it isn't — either because no src was provided or because the image failed to load.

How it works internally:

The fallback isn't a generic grey circle. It extracts the first two characters of the user's name prop as initials, uppercased, and derives a background color from the name using a simple hash function. The hash maps to a hue value between 0–360, then constructs an HSL color string with saturation and lightness adjusted for the active theme (isDark shifts lightness down to keep contrast on dark backgrounds). The result is that the same username always maps to the same color, on every device, without any stored preference.

Color values are cached in a module-level Map keyed by name:theme to avoid recomputing on every render. The cache is bounded to 200 entries and cleared when it overflows.

Image rendering uses expo-image for better caching and performance over the built-in Image. An onError callback sets a local error flag, which switches the component to the fallback view.

Props:

PropTypeDefaultDescription
src`string \null`Image URI
namestring"U"Used for initials and color derivation
sizenumber36Diameter in pixels
styleImageStyleAdditional style overrides

Card

The Card component is a themed surface container — the layout primitive you reach for when building post cards, settings rows, or any content block that needs consistent padding, a border, and a background that tracks the active theme.

How it works internally:

Card reads colors.card and colors.border from useColors() and applies them as inline styles. The border radius and layout class come from NativeWind (rounded-2xl border). The component accepts all standard ViewProps, so you can pass onPress, testID, accessibilityLabel, or any other native prop without a wrapper.

The sub-components — CardHeader, CardTitle, CardDescription, CardContent, CardFooter — are layout wrappers with consistent spacing defined in NativeWind classes. CardTitle and CardDescription both support an optional textColor prop that overrides the theme default when you need a specific color for a particular context.

<Card>
  <CardHeader>
    <CardTitle>Post Title</CardTitle>
    <CardDescription>Posted 2 hours ago</CardDescription>
  </CardHeader>
  <CardContent>
    <Text>Card body content goes here.</Text>
  </CardContent>
  <CardFooter>
    <Button label="Read More" />
  </CardFooter>
</Card>

Exports:

ComponentDescription
CardRoot surface with theme border and background
CardHeaderflex-col p-6 wrapper for title area
CardTitleThemed title text, bold, lg size
CardDescriptionThemed muted subtitle text
CardContentp-6 pt-0 content body wrapper
CardFooterHorizontal row footer with p-6 pt-0

Radio

The Radio group provides a single-select option set, built on React context so individual RadioButton instances don't need to receive selected state as a prop directly — they read it from the nearest RadioGroup.

How it works internally:

RadioGroup creates a context value containing selectedValue and onValueChange, wrapped in useMemo so the context object only re-creates when those two values actually change — not on every parent render. Each RadioButton reads from that context via useContext.

The radio indicator is two concentric circles: an outer ring whose border color switches between colors.flameon (selected) and colors.border (unselected), and an inner filled circle that renders only when the item is selected. Both are sized proportionally from the size prop using ratios (outer = size, inner = size * 0.5, border radius = size / 2 and size * 0.25 respectively).

Disabled state is handled by setting opacity: 0.5 and passing disabled={true} to the underlying Pressable, which removes touch handling at the native layer.

Each RadioButton carries accessibilityRole="radio" and accessibilityState for screen reader support out of the box.

const [selected, setSelected] = useState("option1");

<RadioGroup selectedValue={selected} onValueChange={setSelected}>
  <RadioButton value="option1" label="Option One" description="Best for most users" />
  <RadioButton value="option2" label="Option Two" />
  <RadioButton value="option3" label="Option Three (disabled)" disabled />
</RadioGroup>

RadioGroup Props:

PropTypeDescription
selectedValue`string \number`Currently selected value
onValueChange(value) => voidCalled on selection
containerStyleViewStyleOptional wrapper style

RadioButton Props:

PropTypeDefaultDescription
value`string \number`This option's value
labelstringPrimary label text
descriptionstringSecondary descriptive text
sizenumber24Radio indicator diameter
colorstringTheme accentSelected state color override
disabledbooleanfalseDisables interaction

DropdownMenu

A composable contextual action menu — the component you attach to a "•••" button on a post, a toolbar action, or any trigger that should reveal a list of options.

It follows the same theming contract as the rest of the package and is designed to be dropped into any context without additional configuration for light/dark mode.

(Full API documentation for DropdownMenu will be published in a follow-up post as the composable API surface is finalized.)


Installation

All five components are available now in v1.0.22:

npm install [email protected]

or

yarn add [email protected]

System Reliability & Support

The development team remains committed to maintaining a secure, efficient, and highly optimized open-source toolkit. Continuous updates are actively deployed to ensure total cross-platform stability across iOS, Android, and Web deployments.

For technical assistance or to report issues, please submit on the comments or our official NPM Project Page.

Thanks for your time and support. Let's make Steem great, again.


Official NPM Link: https://www.npmjs.com/package/rn-markdown-editor
Primary Architecture: TypeScript / JavaScript
Supported Environments: Expo & Bare React Native CLI
Utility Exports: useDebouncedInput, useDisclosure, useMergeState