alouette
alouette-data
Show a value the app already holds. Badge labels something with a status, count or category pill; Avatar stands for a person or account; EditableItem shows one labelled value behind an edit affordance and EditableSection a whole titled section behind one; Bullet is an icon + text list row; Code and CodeBlock render source inline and as a block; Blockquote (BlockquoteFrame for block content) and Citation render a quoted excerpt and its attribution; Disclosure hides content behind a caret row, like an HTML details/summary. All but Disclosure are display-only — they own no editor and no press handling, and are never wrapped in a Link or Pressable to gain one. Load when showing a status chip, a list row, an avatar, a value or section with an edit affordance, code, a quote or its attribution, collapsible details, or rendering markdown.
This skill builds on alouette-theming. Read it first for the accent model.
alouette — Data display
Badge is a small, self-sizing pill that labels something: a status, a count, a category. It is display-only — it renders no press handling and no accessible role, and is never wrapped in a Link or Pressable to gain one; put the link beside it.
Setup
import { Badge } from "alouette";
<Badge>New</Badge>;Core Patterns
Accent
accent defaults to the inherited accent ("brand" outside an accent scope) and drives the whole badge — it wraps its content in AccentScope, so the background, border and text all resolve from the accent theme in the current light/dark mode. "neutral" is accepted: a grayscale badge under an accented ancestor.
<Badge accent="success">Paid</Badge>
<Badge accent="danger">Overdue</Badge>
<Badge accent="neutral">Draft</Badge>Variants
variant is "solid" | "solid.enabled" | "outlined", default "solid".
<Badge variant="solid">Draft</Badge> {/* emphasis fill, on-emphasis text */}
<Badge variant="solid.enabled">Active</Badge> {/* enabled fill, on-accent text */}
<Badge variant="outlined">Archived</Badge> {/* pale accent tint, accent border + text */}solid is the default: the accent's emphasis fill, the same chip as a selected SegmentedBar item (a light chip when neutral). Use solid.enabled for the one badge that must read as switched-on, and outlined for the quietest one — a pale tint of the accent (bg-highlight-accent) under an accent border and ink.
Size and icon
size is "sm" | "md" (default "md") and controls padding, text size and the icon size (12px / 16px). Pass icon as a rendered element; do not size it yourself.
import { StarRegularIcon } from "alouette-icons/phosphor-icons/Star";
<Badge size="sm" icon={<StarRegularIcon />}>
Featured
</Badge>;Placement
The badge is self-start, so it never stretches to fill a stack. Position it with a wrapper — it takes no className.
<View className="flex-row gap-xs items-center">
<Text className="text-base">Invoice #128</Text>
<Badge accent="success" size="sm">
Paid
</Badge>
</View>Avatar
Avatar is the accent disc standing for a person or an account: up to two initials derived from name, or an icon in their place. size is "sm" | "md" | "lg" (28/32/40px), accent defaults to the inherited accent ("brand" outside an accent scope), and className is for layout only. variant is "solid" (default, the accent's emphasis fill under text-on-emphasis) or "enabled" (the enabled fill under text-on-accent, as Badge's solid.enabled). Display-only like Badge — a pressable avatar is a PressableBox (or an AppHeaderAccount, alouette-layout/SKILL.md) wrapped around one, never the disc given a role of its own.
import { Avatar } from "alouette";
<Avatar name="Ada Lovelace" /> {/* AL */}
<Avatar size="lg" accent="info" icon={<UserRegularIcon />} />
<Avatar variant="enabled" name="Ada Lovelace" />Bullet
Bullet is one row of an icon-led list: a leading icon tinted text-accent and the text as children. It takes no accent of its own — it reads the nearest scope, so accent a whole list by accenting its container. Stack rows in a View and choose the gap yourself.
import { Bullet, View } from "alouette";
import { CheckCircleRegularIcon } from "alouette-icons/phosphor-icons/CheckCircle";
<View className="gap-xs">
<Bullet icon={<CheckCircleRegularIcon />}>Consistent UI</Bullet>
<Bullet icon={<CheckCircleRegularIcon />}>Accessible</Bullet>
</View>;The text is text-base; the icon is centered on its first line and the text shrinks, so long content wraps under itself rather than pushing the icon down.
Code, CodeBlock, Blockquote and Citation
Code is an inline fragment that sets no size, so it inherits the text it sits in; CodeBlock is the block form, scrolling long lines (or wrapping them with wrap, for a command read end to end). Blockquote is a quoted excerpt whose children are inline text; BlockquoteFrame holds block content laid out by the caller, where a bare string throws on native. Citation is the attribution — a link (an ExternalLinkText) when given href — passed as a blockquote's citation or standing alone. All are display-only: a copy action is an IconButton beside the block, never a pressable around it.
import { Blockquote, Citation, Code, CodeBlock, Paragraph } from "alouette";
<Paragraph>
Run <Code>pnpm --filter alouette build:css</Code> after editing the palette.
</Paragraph>;
<CodeBlock title="theme.ts">{snippet}</CodeBlock>;
<Blockquote accent="brand" citation={<Citation>Le Guin</Citation>}>
Light is the left hand of darkness.
</Blockquote>;Sizes, wrap, BlockquoteFrame and Citation's link: references/code-and-quotes.md.
Disclosure
Disclosure shows or hides its children under a caret row. Its label is inline text naming the trigger (nested Text and Code runs, never a link). A markdown <details>: references/disclosure.md.
EditableItem and EditableSection
Two read-only displays carrying an edit affordance: EditableItem for one labelled value (a bold label, a summary beside it), EditableSection for a titled section whose value spans several lines. Both own no editor and no state — they call onEdit, and the pencil IconButton is the only pressable.
<EditableItem
label="Display name"
summary={<Badge accent="brand">Ada Lovelace</Badge>}
editAriaLabel="Edit display name"
onEdit={openEditor}
/>;
<EditableSection
className="surface"
title="Event details"
titleBadge={<Badge accent="brand">12 August 2026</Badge>}
editAriaLabel="Edit event details"
onEdit={openEditor}
>
<Paragraph className="text-sm">An evening of readings…</Paragraph>
</EditableSection>;When the editor is a modal form, use FormEditableItem / FormEditableSection (alouette-forms/SKILL.md) rather than wiring onEdit yourself. EditableSurface / FormEditableSurface are the former names, removed in 23.0.0; npx alouette-codemod surface-and-stacks --only=editable src renames them. Props, which ones belong to the section rather than the button, and how to choose between the two: references/editable-displays.md.
Common Mistakes
HIGH Passing the icon as a component instead of an element
Wrong:
<Badge icon={StarRegularIcon}>Featured</Badge>Correct:
<Badge icon={<StarRegularIcon />}>Featured</Badge>icon is typed SVGIconElement — a rendered element, matching Button and Message. A component reference is a type error and renders nothing.
Source: packages/alouette/src/ui/data/Badge.tsx; ui/primitives/Icon.tsx
MEDIUM Passing className to Badge
Wrong:
<Badge className="ml-xs bg-blue-500">New</Badge>Correct:
<Box className="ml-xs">
<Badge accent="info">New</Badge>
</Box>BadgeProps is accent | size | variant | icon | children — there is no className (nor style) prop. Appearance comes from accent + variant; spacing and placement come from the parent.
Source: packages/alouette/src/ui/data/Badge.tsx
MEDIUM Inventing variant or size values
Wrong:
<Badge variant="filled" size="lg">
Active
</Badge>Correct:
<Badge variant="solid.enabled" size="md">
Active
</Badge>variant is exactly "solid" | "solid.enabled" | "outlined" (note the dotted name) and size is only "sm" | "md" — unlike Message, there is no lg.
Source: packages/alouette/src/ui/data/Badge.tsx
MEDIUM Hand-rolling a pill with Box + Text
Wrong:
<Box className="rounded-full bg-green-100 px-xs py-xxs">
<Text className="text-xs">Paid</Text>
</Box>Correct:
<Badge accent="success" size="sm">
Paid
</Badge>A manual pill uses raw Tailwind colors instead of accent tokens, so it does not follow light/dark mode or the accent theme, and its padding/radius drift from the scale.
Source: packages/alouette/src/ui/data/Badge.tsx
HIGH Wrapping a display-only component in a Link or Pressable
Badge, Bullet and an EditableItem summary are display-only; wrapping one to make it interactive is forbidden — see alouette-styling/SKILL.md for the rule. On native it is also broken layout: an expo Router <Link> without asChild renders a Text, which cannot contain the View these components render.
Wrong:
<Link href={ticket.url}>
<Badge>{ticket.key}</Badge>
</Link>Correct — the link beside the badge it labels, or inside the Bullet text:
<View className="flex-row gap-xs items-center">
<ExternalLinkText size="sm" href={ticket.url} text={ticket.key} />
{ticket.status ? <Badge size="sm">{ticket.status}</Badge> : null}
</View>
<Bullet icon={<FileRegularIcon />}>
<ExternalLinkText href={doc.url} text={doc.title} />
</Bullet>When the pressable target must itself be a pill, build the chip with PressableBox (alouette-actions) rather than nesting a Badge in a link.
Source: packages/alouette/src/ui/data/Badge.tsx; ui/actions/ExternalLinkText.tsx; ui/actions/PressableBox.tsx
HIGH Hand-rolling the label + value + pencil row
Wrong:
<View className="flex-row items-center justify-between">
<View className="flex-row items-center gap-sm">
<Text className="font-body-bold text-md">Display name</Text>
<Badge>{name}</Badge>
</View>
<IconButton
size="sm"
icon={<PencilSimpleRegularIcon />}
onPress={openEditor}
/>
</View>Correct:
<EditableItem
label="Display name"
summary={<Badge>{name}</Badge>}
editAriaLabel="Edit display name"
onEdit={openEditor}
/>The hand-rolled row drifts on spacing and usually forgets the edit button's aria-label, leaving it unnamed for assistive tech. When the editor is a form modal, FormEditableItem replaces the surrounding state as well.
Source: packages/alouette/src/ui/data/EditableItem.tsx
MEDIUM Hand-rolling a bulleted row
Wrong:
<View className="flex-row gap-sm items-start">
<Icon icon={<CheckCircleRegularIcon />} className="text-accent" />
<Text className="shrink">Accessible</Text>
</View>Correct:
<Bullet icon={<CheckCircleRegularIcon />}>Accessible</Bullet>That is exactly what Bullet renders. Hand-rolled rows drift on the gap and routinely drop shrink on the Text, which stops long text from wrapping.
Source: packages/alouette/src/ui/data/Bullet.tsx
MEDIUM Expecting EditableItem or EditableSection to open an editor
Neither renders a modal nor holds state — onEdit is yours to wire. Nothing happens on press until you open something from it; the form-modal versions are FormEditableItem / FormEditableSection.
Source: packages/alouette/src/ui/data/EditableItem.tsx; ui/containers/EditableSection.tsx; ui/forms/FormEditableItem.tsx
MEDIUM Pinning a size on inline Code
Wrong:
<Paragraph className="text-xl">
Run <Code className="text-sm">build:css</Code> first.
</Paragraph>Correct:
<Paragraph className="text-xl">
Run <Code>build:css</Code> first.
</Paragraph>Code deliberately carries no size class so it inherits the text it sits in; a fixed size makes the fragment jump out of the line on both platforms.
Source: packages/alouette/src/ui/data/Code.tsx
MEDIUM Hand-rolling a code block with a lowered surface + Text
Wrong:
<Box className="surface lowered">
<Text className="font-mono text-sm">{snippet}</Text>
</Box>Correct:
<CodeBlock title="theme.ts">{snippet}</CodeBlock>The hand-rolled block wraps long lines instead of scrolling them, and drops the select-auto / web:whitespace-pre pair that keeps the code selectable and its indentation intact on web. To wrap on purpose, pass wrap: copying the frame with web:whitespace-pre-wrap also loses role="code", which react-native's Role type refuses on a caller's Text.
Source: packages/alouette/src/ui/data/CodeBlock.tsx
See also: alouette-icons/SKILL.md for importing icon elements; alouette-theming/SKILL.md for what each accent resolves to; alouette-external-links/SKILL.md and alouette-actions/SKILL.md for the link and pressable components these display-only ones must never stand in for.