Base components
Close button
An X button that closes dialogs, banners, notifications and side panels, with a light and a dark background style, which eases its fill on hover and gives a small press.
Live preview
Loading preview
A light card with an sm close button in its corner, a brand banner with a dark background md button, and a row of the four sizes. Replay hovers and presses the banner button.
Installation
Run this in your app after the one-time setup on the Getting started page.
$ npx @ganisigit/lunia-ds add close-buttonUsage
The demo above, as code.
demo.tsx
'use client';
import * as React from 'react';
import { CloseButton } from './close-button';
const sizes = ['xs', 'sm', 'md', 'lg'] as const;
// Replay shows the banner button's hover fill, then its press, then lets it go.
const STEPS = [
{ at: 0, state: 'hover' },
{ at: 500, state: 'press' },
{ at: 700, state: 'hover' },
{ at: 1300, state: 'rest' },
] as const;
type DemoState = (typeof STEPS)[number]['state'];
const demoState: Record<DemoState, string> = {
rest: '',
hover: 'bg-colors-alpha-white-20',
press: 'bg-colors-alpha-white-20 scale-(--lunia-scale-press) motion-reduce:scale-100',
};
export default function CloseButtonDemo({ replay = 0 }: { replay?: number }) {
const [state, setState] = React.useState<DemoState>('rest');
const [banner, setBanner] = React.useState(true);
React.useEffect(() => {
if (replay === 0) return;
setBanner(true);
const timers = STEPS.map((step) => window.setTimeout(() => setState(step.state), step.at));
return () => {
for (const t of timers) window.clearTimeout(t);
};
}, [replay]);
return (
<div className="flex w-full max-w-md flex-col gap-6">
<div className="relative rounded-xl border border-secondary bg-primary p-5 pe-14 shadow-sm">
<p className="text-sm font-semibold text-primary">Invite your team</p>
<p className="mt-1 text-sm text-tertiary">
Add people to this workspace to work on projects together.
</p>
<CloseButton label="Close invite card" className="absolute end-3 top-3" />
</div>
{banner ? (
<div className="flex items-center gap-3 rounded-xl bg-brand-section py-2 ps-4 pe-2">
<p className="flex-1 text-sm font-medium text-white">Version 2.4 is available.</p>
<CloseButton
size="md"
darkBackground
label="Close banner"
className={demoState[state]}
onClick={() => setBanner(false)}
/>
</div>
) : (
<button
type="button"
className="self-start text-sm font-semibold text-brand-secondary underline-offset-4 hover:underline"
onClick={() => setBanner(true)}
>
Show the banner again
</button>
)}
<div className="flex items-center justify-center gap-3">
{sizes.map((size) => (
<CloseButton key={size} size={size} label={`Close, size ${size}`} />
))}
</div>
</div>
);
}
API
Parts: root (button), X icon.
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "xs" | "sm" | "md" | "lg" | "sm" | 28, 36, 40 or 44px, with a 16, 20, 20 or 24px icon. |
| darkBackground | boolean | false | White icon and a translucent white hover fill, for dark or brand surfaces. |
| label | string | "Close" | Accessible name. Name the thing when there are several on a page, for example "Close notification". |
| disabled | boolean | false | Turns the button off (50% opacity). |
| Event | Description |
|---|---|
| click | Fires on activation. The parent closes the surface. |
Motion
Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.
| ID | Trigger | Element | Property | From → to | Timing | Reduced motion |
|---|---|---|---|---|---|---|
| CLB1 | hover | root, icon | background-color, icon color | transparent, fg-tertiary (fg-white on dark) → bg-primary_hover, fg-tertiary_hover (white-20 fill, fg-white on dark) | duration-sm (200)ease-out | Swaps at once |
| CLB2 | press (pointer down or Space held) | root | scale | 1 → scale-press | duration-sm (200)ease-out | Stays still |
| CLB3 | release | root | scale | scale-press → 1 | duration-sm (200)ease-out | Stays still |
| CLB4 | keyboard focus | root | box-shadow | none → focus ring (2px bg-primary gap, 4px focus-ring) | duration-2xs (120)ease-out | Shows at once |
Accessibility
- Native button with aria-label set to label (default "Close"); the X icon is aria-hidden.
- The surface that owns the button also closes on Escape where its pattern allows it (dialogs, popovers); the button is the pointer and touch path, not the only path.
- When the button sits inside a dialog primitive, use the primitive's close part (Base UI Dialog.Close, Reka UI DialogClose) as the root so focus returns to the opener.
- Every size is 28px or larger, so the target meets the 24 x 24px minimum (WCAG 2.5.8).
- Reduced motion: colors swap at once and the button does not scale.