Close Button
An always-icon-only button for dismissing overlays, banners, and chips. It ships a built-in X, self-labels for screen readers, and adapts to whatever surface it sits on — no color or variant to choose.Import
import { CloseButton } from "@hope-ui/components/close-button";Usage
CloseButton needs nothing to render: it draws hope's built-in X glyph and gives itself a
localized accessible name ("Close"). Wire up onClick to dismiss whatever it belongs to.
<CloseButton onClick={() => setOpen(false)} />Sizes
Three sizes — sm (the default), md, and lg — scaling the box and the glyph together. It stays
a compact, square corner affordance at every size.
<CloseButton size="sm" />
<CloseButton size="md" />
<CloseButton size="lg" />Adapts to the surface
This is the whole point: CloseButton asserts no color of its own. Unlike Button,
it has no variant or colorScheme. The glyph inherits currentColor, and its hover/press wash and
focus ring are derived from that — so a single component reads correctly on any surface (light,
soft-tinted, solid-colored, dark) with zero configuration. Hover or focus each row to see the wash
and ring pick up that surface's text color.
<div class="bg-surface text-foreground">…<CloseButton /></div>
<div class="bg-primary-soft text-primary-emphasis">…<CloseButton /></div>
<div class="bg-primary text-on-primary">…<CloseButton /></div>
<div class="bg-surface-inverse text-on-inverse">…<CloseButton /></div>In context
The everyday job: sit in the corner of a transient surface and remove it on click. onClick is an
ordinary handler — CloseButton is a real button underneath.
Changes saved
Your preferences were updated across all devices.
<div class="relative rounded-lg border border-subtle bg-surface-raised p-4">
<div>
<p class="font-medium">Changes saved</p>
<p class="text-foreground-muted">Your preferences were updated.</p>
</div>
<CloseButton
aria-label="Dismiss notification"
onClick={() => setOpen(false)}
/>
</div>Dialog.CloseTrigger, Alert.CloseTrigger, and friends render a CloseButton wired to their own dismiss behavior — reach for a standalone CloseButton for your own dismissible surfaces.Custom icon
icon overrides the built-in X — per instance, or app-wide via a preset (see
App-wide defaults). The glyph inherits currentColor, so a
stroked SVG needs no color of its own. When the icon changes the meaning of the affordance
(here, "remove" rather than "close"), pass a matching aria-label — the default name is "Close".
<CloseButton aria-label="Remove" icon={<TrashIcon />} />Disabled
disabled blocks interaction and dims the button via the opacity-disabled token. A native
<button> uses the disabled attribute (and drops out of the tab order); a non-native element (see
Polymorphism) instead gets aria-disabled with blocked handlers.
<CloseButton disabled />Polymorphism
Render CloseButton as a different element or component with the render prop — a function that
receives CloseButton's fully-computed props and spreads them onto your element. There is no as
prop; render is the single polymorphism API.
<CloseButton
nativeButton={false}
render={(props) => <a href="#" {...props} />}
/>button, set nativeButtonto false. CloseButton then applies role="button", manages the tab order, and synthesizes keyboard activation (Space and Enter) — the behavior a real button has for free.Theming
CloseButton's look comes from the active preset's recipe. You can override it at three levels,
applied in order (later wins a Tailwind conflict): recipe base → preset slotClasses → instance
slotClasses → class.
Parts
Every part carries a data-slot attribute you can target, and each is addressable by name through
slotClasses.
| Slot | data-slot | Description |
|---|---|---|
root | close-button | The rendered element (a native button by default). Compound consumers may re-scope this value. |
icon | close-button-icon | The host span wrapping the glyph. Keeps the glyph from becoming the pointer target. |
Overriding one button
class merges onto the root slot (your utilities win conflicts). slotClasses targets any slot by
name. Use literal class strings so your Tailwind build can see them.
<CloseButton slotClasses={{ root: "rounded-full bg-surface-sunken" }} />
<CloseButton size="lg" class="rounded-full ring-1 ring-subtle" />App-wide defaults & overrides
Set defaults and global part classes for every CloseButton through the theme with
definePreset, then pass the derived preset to ThemeProvider. defaultProps are resolved at
instance ?? preset ?? builtin precedence, so an explicit prop on a single button always wins.
import { definePreset } from "@hope-ui/theming";
import { hope } from "@hope-ui/presets/hope";
export const myPreset = definePreset(hope, {
components: {
closeButton: {
// Every recipe variant is defaultable.
defaultProps: { size: "lg" },
// Global part classes, folded in before per-instance slotClasses.
slotClasses: { root: "rounded-full" },
},
},
});The glyph is defaultable app-wide too — but as a factory (() => JSX.Element), never a bare
element. A preset value is one object shared by every instance, and a built Solid node would move
between them; a factory is called fresh per instance, so every close button gets its own glyph.
definePreset(hope, {
components: {
closeButton: {
// A factory, not <MyIcon /> — reuse-safe across every instance.
defaultProps: { icon: () => <MyIcon /> },
},
},
});Defaultable through defaultProps: size and the icon factory. Per-usage props — disabled,
nativeButton, and event handlers — are intentionally not defaultable app-wide.
API
Props
CloseButton accepts every native <button> attribute in addition to the props below.
| Prop | Default | Type |
|---|---|---|
size | "sm" | CloseButtonSizeDensity scale: sm, md, lg. Scales the box and glyph together. The only visual axis — there is no variant or colorScheme. |
icon | hope's X | JSX.Element | (() => JSX.Element)The glyph. A bare element per instance, or a factory as an app-wide preset default. Inherits currentColor. |
disabled | false | booleanDisables the button. Native buttons use the disabled attribute; a non-native element uses aria-disabled and blocked handlers. Dimmed via opacity-disabled. |
nativeButton | true | booleanWhether the rendered element is a real button. Set false when rendering another element so CloseButton supplies role, tabIndex, and keyboard activation. |
render | — | (props) => JSX.ElementRender CloseButton as another element or component. Receives CloseButton's computed props. The only polymorphism mechanism — there is no as prop. |
aria-label | "Close" (localized) | stringAccessible name. Defaults to the localized common.close message; override it whenever the affordance means something other than close. |
class | — | stringExtra classes merged onto the root slot; your utilities win conflicts via tailwind-merge. |
slotClasses | — | SlotClasses<"closeButton">Per-slot class overrides, keyed by root and icon. |
Accessibility
CloseButton renders a real <button> by default and — unlike an ordinary icon-only button — can
never be nameless: it falls back to a localized "Close" accessible name, so it is
screen-reader-ready out of the box. Provide aria-label (or aria-labelledby) when the affordance
means something more specific than "close".
| Key | Action |
|---|---|
Enter | Activates the button (native and rendered-as elements). |
Space | Activates the button (synthesized for rendered-as elements; page scroll is prevented). |
Tab | Moves focus. A disabled native button is skipped. |
- Self-labeling — the localized
"Close"name means it is never an unlabeled icon button. Overridearia-labelwhen the glyph changes the meaning (e.g. a trash icon for "remove"). - Focus is keyboard-only — a pointer press does not leave a lingering focus ring; the
:focus-visiblering appears for keyboard focus, matching every other hope control. - Disabled uses the native
disabledattribute on a<button>(no redundantaria-disabled), andaria-disabledon a non-native element rendered viarender+nativeButton={false}.