v0.1 public beta early access on GitHub
Home / Screens / misc-article-reader
Screen template

Article Reader Screen

Long-form article with actions

pro Expo Go ✓ iosandroid gestures: tap · scroll haptics: light · selection
$ npx lumioui add misc-article-reader
import { ArticleReaderScreen } from "@/screens/misc/article-reader";
Registry dependencies5
Native packages installed6
@lumioui/ui@latest@lumioui/core@latestreact-native-safe-area-context@*react-native-reanimated@*react-native-svg@*nativewind@*
misc/article-reader.tsxsource · yours to edit
import * as React from "react";
import { Pressable, ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Avatar, Button, Glass, Icon, Image, Text } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { Display, Mount, Press, StickyBottomBar, World } from "../shared";

export interface ArticleReaderScreenProps {
  source?: string;
  headline?: string;
  author?: string;
  readTime?: string;
  paragraphs?: string[];
  onBack?: () => void;
}

const DEFAULT_PARAGRAPHS = [
  "The new design language rolls out to every supported iPhone this week, and the biggest change is what you don't notice at first: chrome that bends like glass.",
  "Panels refract the content beneath them, so a toolbar over a photo carries the photo's colors instead of flat gray. It's subtle on static screenshots and obvious in motion — exactly the trade-off the team says they wanted.",
  "Adoption has been quick on the OS side. Third-party apps lag, mostly because the material only reads well when the underlying layout is clean — the system exposes seams that used to hide under opaque chrome.",
  "Whether it sticks is an open question. The last big visual rework took two releases to settle. This one arrives more complete, but the real test is what happens when apps start fighting the material for contrast.",
];

/** Article reader — headline + source row + body copy + actions. */
export function ArticleReaderScreen({
  source = "The Verge",
  headline = "Liquid Glass ships to all supported iPhones — here's what changes",
  author = "Sam Kellan",
  readTime = "4 min read",
  paragraphs = DEFAULT_PARAGRAPHS,
  onBack,
}: ArticleReaderScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const [saved, setSaved] = React.useState(false);

  return (
    <World primary="#65A30D">
    <SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
      <View pointerEvents="box-none" className="absolute inset-x-4 z-10" style={{ top: insets.top + 8 }}>
        <View className="flex-row items-center justify-between">
          <Glass interactive effect="regular" className="h-11 w-11 items-center justify-center rounded-full">
            <Press
              haptic="light"
              onPress={() => onBack?.()}
              accessibilityRole="button"
              accessibilityLabel="Go back"
              className="h-9 w-9 items-center justify-center"
            >
              <Icon name="chevron-left" size="md" color="foreground" />
            </Press>
          </Glass>
          <View className="flex-row items-center gap-2">
            <Glass interactive effect="regular" className="h-11 w-11 items-center justify-center rounded-full">
              <Press
                haptic="light"
                onPress={() => setSaved((s) => !s)}
                accessibilityRole="button"
                accessibilityLabel={saved ? "Unsave article" : "Save article"}
                accessibilityState={{ selected: saved }}
                className="h-9 w-9 items-center justify-center"
              >
                <Icon name="bookmark" size="sm" color={saved ? "primary" : "foreground"} />
              </Press>
            </Glass>
            <Glass interactive effect="regular" className="h-11 w-11 items-center justify-center rounded-full">
              <Press
                haptic="light"
                onPress={() => haptics.light()}
                accessibilityRole="button"
                accessibilityLabel="Share article"
                className="h-9 w-9 items-center justify-center"
              >
                <Icon name="share" size="sm" color="foreground" />
              </Press>
            </Glass>
          </View>
        </View>
      </View>

      <ScrollView
        showsVerticalScrollIndicator={false}
        contentContainerStyle={{ paddingTop: insets.top + 60, paddingBottom: insets.bottom + 96 }}
      >
        {/* Head + byline — the reading contract. */}
        <Mount delay={60} className="gap-2 px-4 pt-1">
          <Text variant="small" className="font-medium text-primary">
            {source}
          </Text>
          <Display className="text-3xl leading-snug">
            {headline}
          </Display>
          <View className="flex-row items-center gap-2.5 pt-1">
            <Avatar size="sm" fallback={author} alt={author} />
            <View>
              <Text className="text-xs font-medium">{author}</Text>
              <Text className="text-[10px] text-muted-foreground">
                {readTime} · Today
              </Text>
            </View>
          </View>
          {/* Listen affordance — modern readers offer audio up front. */}
          <Pressable
            onPress={() => haptics.light()}
            accessibilityRole="button"
            accessibilityLabel="Listen to this article"
            className="mt-1 flex-row items-center gap-2 self-start rounded-full bg-primary/10 px-3 py-1.5"
          >
            <Icon name="play" size="sm" color="primary" />
            <Text className="text-xs font-medium text-primary">
              Listen · {readTime.replace("read", "")}
            </Text>
          </Pressable>
        </Mount>

        {/* Hero art — the lead image a feature story leads with. */}
        <Mount delay={120} className="px-4 pt-4">
          <Image
            source={{
              uri: "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=800&h=450&fit=crop&q=70&auto=format",
            }}
            className="h-48 w-full rounded-xl bg-muted"
            accessibilityLabel="Article hero illustration"
          />
        </Mount>

        {/* Body — generous leading, reading width. */}
        <Mount delay={180} className="gap-4 px-4 pt-5">
          {paragraphs.map((p, i) => (
            <Text key={i} className="text-[15px] leading-7 text-foreground/90">
              {p}
            </Text>
          ))}
        </Mount>
      </ScrollView>

      {/* Reader actions — size controls. */}
      <StickyBottomBar>
        <View className="flex-row items-center justify-center">
          <View className="flex-row items-center gap-4 overflow-hidden rounded-full px-4 py-1">
            <Glass interactive effect="regular" sheen={0.5} className="absolute inset-0 rounded-full" pointerEvents="none" />
            <Button
              variant="ghost"
              size="icon"
              haptic="light"
              onPress={() => haptics.light()}
              accessibilityLabel="Smaller text"
              className="h-10 w-10"
            >
              <Icon name="minus" size="sm" color="foreground" />
            </Button>
            <Text className="text-xs text-muted-foreground">Aa</Text>
            <Button
              variant="ghost"
              size="icon"
              haptic="light"
              onPress={() => haptics.light()}
              accessibilityLabel="Larger text"
              className="h-10 w-10"
            >
              <Icon name="plus" size="sm" color="foreground" />
            </Button>
          </View>
        </View>
      </StickyBottomBar>
    </SafeAreaView>
    </World>
  );
}
ArticleReaderScreen.displayName = "ArticleReaderScreen";
agent prompt — paste into Claude Code, Cursor, Devin…
Recreate the "Article Reader Screen" screen in React Native using LumioUI.

Goal: Long-form article with actions Match the reference implementation's layout, behavior, and motion exactly — this is a production screen, not a mockup.

## Setup

Fastest path — install the screen plus its component deps:

```bash
npx lumioui add misc-article-reader
```

Or pull just the components: `npx lumioui add avatar button icon image text`

Native packages:
- @lumioui/ui@latest
- @lumioui/core@latest
- react-native-safe-area-context@*
- react-native-reanimated@*
- react-native-svg@*
- nativewind@*

Stack assumptions: Expo SDK 54 / RN 0.81, Reanimated 4, NativeWind v4, TypeScript strict, Expo Go compatible.

## Components — how, where, when

### `avatar` — Circular user image with fallback initials and status dot.
- `<Avatar>` ×1

### `button` — Tactile pill button with press spring, ink wash and async loading.
- `<Button>` ×2

### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×6

### `image` — Cached image with placeholder, fallback and aspect handling.
- `<Image>` ×1

### `text` — Themed type scale from caption to display.
- `<Text>` ×6

## Conventions (non-negotiable)

- Styling via NativeWind `className` with semantic tokens only: `bg-background`, `text-foreground`, `bg-card`, `text-card-foreground`, `bg-secondary`, `bg-muted`, `text-muted-foreground`, `bg-accent`, `bg-destructive`, `border-border`. Hard-coded colors only inside media overlays (`text-white`, `bg-black/N`).
- Motion via the token presets from `@lumioui/core` (`spring.press`, `spring.surface`, `spring.glide`, `timing.fast`, `timing.slow`, …) — no bespoke spring configs or durations.
- Gate transform motion behind `useReducedMotion()`; keep opacity/color feedback, drop travel and overshoot.
- Haptics via `useHaptic()`: `light` on press, `selection` on toggle/select, `warning` on invalid input. Never import expo-haptics directly.
- Press feedback via `usePressAnimation()` (~0.97 scale), not `activeOpacity` tricks.
- a11y: `accessibilityRole` + `accessibilityLabel` on every control, touch targets ≥44pt.
- Entrance choreography: staggered `Mount` wrappers (delay 0/60/120…) so regions land in sequence instead of popping in at once.
- Wrap the screen in `SafeAreaView`; keep dark mode working via the semantic tokens.

## Deliverable

One file: `screens/misc/article-reader.tsx` exporting ArticleReaderScreen. Preserve the props interface — the host app wires the callbacks (navigation, submit handlers) through them.

## Reference implementation

The LumioUI source for this screen — reproduce it faithfully:

```tsx
import * as React from "react";
import { Pressable, ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Avatar, Button, Glass, Icon, Image, Text } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { Display, Mount, Press, StickyBottomBar, World } from "../shared";

export interface ArticleReaderScreenProps {
  source?: string;
  headline?: string;
  author?: string;
  readTime?: string;
  paragraphs?: string[];
  onBack?: () => void;
}

const DEFAULT_PARAGRAPHS = [
  "The new design language rolls out to every supported iPhone this week, and the biggest change is what you don't notice at first: chrome that bends like glass.",
  "Panels refract the content beneath them, so a toolbar over a photo carries the photo's colors instead of flat gray. It's subtle on static screenshots and obvious in motion — exactly the trade-off the team says they wanted.",
  "Adoption has been quick on the OS side. Third-party apps lag, mostly because the material only reads well when the underlying layout is clean — the system exposes seams that used to hide under opaque chrome.",
  "Whether it sticks is an open question. The last big visual rework took two releases to settle. This one arrives more complete, but the real test is what happens when apps start fighting the material for contrast.",
];

/** Article reader — headline + source row + body copy + actions. */
export function ArticleReaderScreen({
  source = "The Verge",
  headline = "Liquid Glass ships to all supported iPhones — here's what changes",
  author = "Sam Kellan",
  readTime = "4 min read",
  paragraphs = DEFAULT_PARAGRAPHS,
  onBack,
}: ArticleReaderScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const [saved, setSaved] = React.useState(false);

  return (
    <World primary="#65A30D">
    <SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
      <View pointerEvents="box-none" className="absolute inset-x-4 z-10" style={{ top: insets.top + 8 }}>
        <View className="flex-row items-center justify-between">
          <Glass interactive effect="regular" className="h-11 w-11 items-center justify-center rounded-full">
            <Press
              haptic="light"
              onPress={() => onBack?.()}
              accessibilityRole="button"
              accessibilityLabel="Go back"
              className="h-9 w-9 items-center justify-center"
            >
              <Icon name="chevron-left" size="md" color="foreground" />
            </Press>
          </Glass>
          <View className="flex-row items-center gap-2">
            <Glass interactive effect="regular" className="h-11 w-11 items-center justify-center rounded-full">
              <Press
                haptic="light"
                onPress={() => setSaved((s) => !s)}
                accessibilityRole="button"
                accessibilityLabel={saved ? "Unsave article" : "Save article"}
                accessibilityState={{ selected: saved }}
                className="h-9 w-9 items-center justify-center"
              >
                <Icon name="bookmark" size="sm" color={saved ? "primary" : "foreground"} />
              </Press>
            </Glass>
            <Glass interactive effect="regular" className="h-11 w-11 items-center justify-center rounded-full">
              <Press
                haptic="light"
                onPress={() => haptics.light()}
                accessibilityRole="button"
                accessibilityLabel="Share article"
                className="h-9 w-9 items-center justify-center"
              >
                <Icon name="share" size="sm" color="foreground" />
              </Press>
            </Glass>
          </View>
        </View>
      </View>

      <ScrollView
        showsVerticalScrollIndicator={false}
        contentContainerStyle={{ paddingTop: insets.top + 60, paddingBottom: insets.bottom + 96 }}
      >
        {/* Head + byline — the reading contract. */}
        <Mount delay={60} className="gap-2 px-4 pt-1">
          <Text variant="small" className="font-medium text-primary">
            {source}
          </Text>
          <Display className="text-3xl leading-snug">
            {headline}
          </Display>
          <View className="flex-row items-center gap-2.5 pt-1">
            <Avatar size="sm" fallback={author} alt={author} />
            <View>
              <Text className="text-xs font-medium">{author}</Text>
              <Text className="text-[10px] text-muted-foreground">
                {readTime} · Today
              </Text>
            </View>
          </View>
          {/* Listen affordance — modern readers offer audio up front. */}
          <Pressable
            onPress={() => haptics.light()}
            accessibilityRole="button"
            accessibilityLabel="Listen to this article"
            className="mt-1 flex-row items-center gap-2 self-start rounded-full bg-primary/10 px-3 py-1.5"
          >
            <Icon name="play" size="sm" color="primary" />
            <Text className="text-xs font-medium text-primary">
              Listen · {readTime.replace("read", "")}
            </Text>
          </Pressable>
        </Mount>

        {/* Hero art — the lead image a feature story leads with. */}
        <Mount delay={120} className="px-4 pt-4">
          <Image
            source={{
              uri: "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=800&h=450&fit=crop&q=70&auto=format",
            }}
            className="h-48 w-full rounded-xl bg-muted"
            accessibilityLabel="Article hero illustration"
          />
        </Mount>

        {/* Body — generous leading, reading width. */}
        <Mount delay={180} className="gap-4 px-4 pt-5">
          {paragraphs.map((p, i) => (
            <Text key={i} className="text-[15px] leading-7 text-foreground/90">
              {p}
            </Text>
          ))}
        </Mount>
      </ScrollView>

      {/* Reader actions — size controls. */}
      <StickyBottomBar>
        <View className="flex-row items-center justify-center">
          <View className="flex-row items-center gap-4 overflow-hidden rounded-full px-4 py-1">
            <Glass interactive effect="regular" sheen={0.5} className="absolute inset-0 rounded-full" pointerEvents="none" />
            <Button
              variant="ghost"
              size="icon"
              haptic="light"
              onPress={() => haptics.light()}
              accessibilityLabel="Smaller text"
              className="h-10 w-10"
            >
              <Icon name="minus" size="sm" color="foreground" />
            </Button>
            <Text className="text-xs text-muted-foreground">Aa</Text>
            <Button
              variant="ghost"
              size="icon"
              haptic="light"
              onPress={() => haptics.light()}
              accessibilityLabel="Larger text"
              className="h-10 w-10"
            >
              <Icon name="plus" size="sm" color="foreground" />
            </Button>
          </View>
        </View>
      </StickyBottomBar>
    </SafeAreaView>
    </World>
  );
}
ArticleReaderScreen.displayName = "ArticleReaderScreen";
```

Screen: https://lumioui.dev/screens/misc-article-reader
iPhone 17 · iOS
9:41
Live preview — the real screen via Expo Web · open full ↗