CloseButton
The way out, on its own.
Overview
The way out, on its own.
First example
<CloseButton onPress={dismiss} accessibilityLabel="Fermer" /><CloseButton variant="ghost" onPress={dismiss} accessibilityLabel="Fermer" />
Anatomy
<CloseButton onPress={dismiss} accessibilityLabel="Fermer le panneau" />
No slots. There is one node and one mark in it, and a component with a single child has nothing to publish a context for.
It is the affordance Chip, Alert, Dialog, Popover and BottomSheet already have,
given a recipe of its own. Those five keep theirs: a close inside a component takes that
component's colours and that component's scale, resolved once at its root (R5), and
Chip.Close is five lines over the shared base for exactly that reason. What was missing is
this one — a dismiss on something the library does not own: a card header, a banner, a sheet
of your own.
Usage
The disc, and the bare cross
<CloseButton onPress={dismiss} accessibilityLabel="Fermer" />
<CloseButton variant="ghost" onPress={dismiss} accessibilityLabel="Fermer" />
secondary — the neutral disc — is the default, for the reason the Dialog gives at its
own close: a cross floating on a panel with nothing under it reads as decoration, and the
disc is what makes it a target. ghost is the bare cross, for a component that is already
providing the target around it.
A mark of your own
<CloseButton accessibilityLabel="Fermer">
<Icon as={XIcon} size={16} color={theme.colors.foreground} />
</CloseButton>
Children replace the built-in cross. Unset, the button draws its own from two bars a quarter
turn apart, so this works in a project that has installed no icon set — the same bargain the
Checkbox's tick and the Radio's dot take.
As another element
<CloseButton asChild onPress={dismiss}>
<Link href="/">…</Link>
</CloseButton>
R12, through the base. The missing-label warning does not fire under asChild: the caller's
element carries its own label or its own text.
Root props
CloseButtonProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| variant | CloseButtonVariant | undefined | No | — |
| size | Size | undefined | No | The box and the cross inside it. Square, so here it drives both. |
| radius | RadiusKey | undefined | No | Overrides the corner, which is a circle — `size` sets it to half the box. A squared close button in a squared panel is a real design, and every control here takes it. |
| color | string | undefined | No | A raw tint (`'#7c3aed'`), never a token (R7). The disc, or the cross on `ghost`. |
| isDisabled | boolean | undefined | No | Dims the button and stops the press. R8: `disabled` is not part of the public vocabulary, `isX` is. |
| children | ReactNode | No | Replaces the built-in cross — an `Icon`, or any glyph. Unset, the button draws its own from two rotated bars, so a dismissible screen works in a project that has installed no icon set. |
| style | StyleProp<ViewStyle> | ((state: PressableStateCallbackType) => StyleProp<ViewStyle>) | No | R9 — `Pressable`'s function form as much as an object or an array. |
Slots
Variants, sizes and colour
Sizes
size | Box | Bar | Cross |
|---|---|---|---|
xs | 24 | 12 | ~8 |
sm | 28 | 14 | ~10 |
md | 32 | 16 | ~11 |
lg | 40 | 20 | ~14 |
md is the reference implementation's close button measured — a 32-point square — and it is the Dialog's.
The bar is twice as long as the cross looks: a bar rotated a quarter turn spans
length / √2 on each axis. It is kept as a ratio of the box rather than tabulated, so it is
one cross at four sizes instead of four drawings of one — the Radio's dot, again.
The stroke does not scale. It is the shared thickness the Chip, the Alert and the
Dialog all draw their cross at; a family of crosses that thicken with their container
reads as four different marks rather than one at four sizes.
The box is small at xs, and it is still pressable: the target grows outwards through
hitSlop, which costs nothing in layout, rather than the glyph growing.
radius is the circle size sets — half the box — and overridable, like everywhere else.
Variants and colour
variant | Disc | Border | Cross |
|---|---|---|---|
primary | accent | — | accentForeground |
secondary | default | — | defaultForeground |
tertiary | — | border | foreground |
ghost | — | — | foreground |
Four emphasis levels and no intent: dismissing is neither a success nor a danger. The
close that carries an intent is the one inside a component that has one — Alert.Close in a
danger alert — and it is the alert's recipe that says so, not a variant here.
secondary is the neutral grey, the Checkbox's and the Radio's reading of the word
rather than the Button's accent-soft: a close button is grey before it is anything else,
and it sits beside those two more often than beside a Button.
color is a raw value (R7), and it lands where the variant puts it: the disc on primary
and secondary, the cross on tertiary and ghost.
There is no pressed colour. The press is the shared PressableFeedback treatment,
because the base owns the press state — a cross has to be a different target from the panel
around it — and a root that does not know it is pressed cannot resolve a colour for it.
Every other close button in the library reads the same way.
Accessibility
accessibilityRole="button", overridable.- An
accessibilityLabelis required, and its absence warns in development: a cross says "close" to someone who can see it and nothing at all to someone who cannot. Unlike an icon-onlyButton, the text beside it names the thing being dismissed rather than the action, so there is nothing to fall back on. - The warning does not fire under
asChild, where the caller's element carries its own label. - The touch target is 8 points larger than the box on every side.
Migration from legacy
Implementation notes
The base, and the component
Two things share the name, and the split is the point:
| Where | What it is | |
|---|---|---|
CloseButtonBase | @xaui/native/system | The behaviour: its own press state, the grown target, the missing-label warning, the built-in cross. Takes the styles a host recipe resolved. |
CloseButton | @xaui/native/close-button | The component: the base plus a recipe — a box that follows size, a disc that follows variant, and a color that tints it. |
Writing a dismissible component of your own means reaching for the base and publishing
close / closeGlyph from your recipe, exactly as Chip does. Dropping a dismiss into a
layout means reaching for the component.
Alignment with the reference implementation
Identical: the 32-point box at the default size, the circle, the filled default rather than a bare glyph, the grown touch target, and children replacing the mark.
Three deltas:
| Theirs | Ours | Why |
|---|---|---|
A Button preset — tertiary, sm, isIconOnly, hitSlop={12} | Its own recipe over the shared base | A preset inherits a label slot, a spinner and a fullWidth-shaped API for a control that has one mark and no text. |
iconProps={{ size, color }} | children, or color and size | R1 — a prop that styles the inside of another component is the thing the v1 API exists to remove. The glyph is a child. |
| The cross is an imported icon | Two bars, a quarter turn apart | A dismissible screen has to work in a project that has installed no icon set. Children still replace it. |