hope-uiearly-preview

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.

On a light surface
On a soft surface
On a solid surface
On a dark surface
The same <CloseButton /> on four surfaces
<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>

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.

Render as an anchor
<CloseButton
  nativeButton={false}
  render={(props) => <a href="#" {...props} />}
/>

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 slotClassesclass.

Parts

Every part carries a data-slot attribute you can target, and each is addressable by name through slotClasses.

Slotdata-slotDescription
rootclose-buttonThe rendered element (a native button by default). Compound consumers may re-scope this value.
iconclose-button-iconThe 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.

theme.ts
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.

A preset-wide close 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.

PropDefaultType
size"sm"
CloseButtonSizeDensity scale: sm, md, lg. Scales the box and glyph together. The only visual axis — there is no variant or colorScheme.
iconhope's X
JSX.Element | (() => JSX.Element)The glyph. A bare element per instance, or a factory as an app-wide preset default. Inherits currentColor.
disabledfalse
booleanDisables the button. Native buttons use the disabled attribute; a non-native element uses aria-disabled and blocked handlers. Dimmed via opacity-disabled.
nativeButtontrue
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".

KeyAction
EnterActivates the button (native and rendered-as elements).
SpaceActivates the button (synthesized for rendered-as elements; page scroll is prevented).
TabMoves focus. A disabled native button is skipped.
  • Self-labeling — the localized "Close" name means it is never an unlabeled icon button. Override aria-label when 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-visible ring appears for keyboard focus, matching every other hope control.
  • Disabled uses the native disabled attribute on a <button> (no redundant aria-disabled), and aria-disabled on a non-native element rendered via render + nativeButton={false}.