<Button onClick={save}>Save</Button>
That is the whole call site. onPress is the same thing under the name the vanilla core
uses, so either reads fine. Disabled while the work runs, aria-busy and data-loading
while it does, and a cooldown afterwards if you ask for one.
Three reasons a button cannot be pressed - it is disabled, it is busy, it is cooling down -
are one thing to whoever renders it. They collapse into available, so nothing has to
combine three flags correctly at every call site.
An href makes it a link - next/link in a Next app, a plain <a> anywhere else. Nothing
to pass, nothing to configure.
Customize
Turn a prop on and press the thing. The preview is the real component, and the code is generated from the same values that drove it - so what you copy is what you just used.
Usage
Installation
Adds the package and prints the import to use. Append --copy to write the source into your project instead.
enigma add buttonInstalls with your project's own package manager - npm, pnpm, yarn or bun - read from its packageManager field or its lockfile.
1 Install the dependencies
2 Use it
Import from @enigmax/primitives/react.
The primitive is the behaviour only - it renders nothing, so there is no recipe to copy.
Style it through the attributes props puts on your element.
Links
In Next, import from @enigmax/primitives/next and an href is next/link:
import { Button } from "@enigmax/primitives/next";
<Button href="/settings">Settings</Button>
The import path is the whole configuration - the same components, the same props, and the
entry is only ever resolved by a project that has Next. That is why the main entry cannot
just import next/link itself: that import fails to resolve for every consumer on Vite,
Astro, Remix or plain React, which is most of them.
React Router has an entry too, and it translates its to for you - so the call site is the
same href whatever is underneath:
import { Button } from "@enigmax/primitives/react-router";
<Button href="/settings">Settings</Button>
Everywhere else an href is a plain <a>, which is correct and only means a full page load
under a router. For one that has no entry here yet, hand over its link once:
import { setLinkComponent } from "@enigmax/primitives/react";
setLinkComponent(MyRouterLink);
That is the same shape as React Aria’s
RouterProvider, which takes the
router’s navigate instead of its link: one mechanism for every router, at the cost of a
line of setup. The entries above exist so the two routers most people are on do not need it.
as overrides everything, for the one link in the app that needs something else.
The four decisions
The cooldown starts when the work finishes, not when it was asked for. Starting it at request time lets a slow call eat its own cooldown, and the button is free again the instant the request returns - which is the moment it most needs not to be.
A cooldown with a key survives a reload. Without one, refreshing the page is a free
retry, and preventing exactly that is the whole reason a cooldown exists. A stored time in
the past is finished rather than pending.
useButton({ cooldown: { ms: 60_000, key: "resend-verification", storage: "local" } });
A shortcut is refused while the visitor is typing - in an input, a textarea, a select or
anything contenteditable - and whenever any modifier is held, so it never types into a
field or steals a real shortcut.
An anchor cannot be disabled. Only aria-disabled, which is advisory: the browser
still follows the link. So the press is refused in the handler rather than trusted to the
attribute.
State
available |
Not disabled, not loading, not cooling down |
element |
"a" when there is an href, else "button" |
loading |
Set it yourself, or let an async onPress manage it |
cooldown |
Milliseconds left, 0 when there is none |
shortcut |
The key, for rendering a <kbd> |
When the markup is not a button
useButton is the same behaviour without the element - for a card, a table row, anything
whose markup is nothing like a button. It returns props ready to spread, carrying
aria-disabled, aria-busy, data-loading and data-cooldown.
const { element: Tag, props, loading, cooldown } = useButton({ onPress: save, cooldown: 3000 });
return <Tag {...props}>{loading ? "Saving..." : "Save"}</Tag>;
Reaching for it to render an ordinary button means writing those six lines at every call
site, which is how they drift apart. That is what <Button> is for.
onClick or onPress
Both, and they are one handler rather than two. onClick is the name React already uses;
onPress is the one the framework-agnostic core uses, so a vanilla call site and a React
one read alike.
It has to be an alias because the component owns the element’s real onClick - that is
where a press is refused while the button is loading, disabled or cooling down. A handler
passed straight through to the element would replace that and take the behaviour with it,
silently.
Props
onClick / onPress |
The same handler under two names. Sync or async; an async one drives loading for its duration |
href |
Renders an anchor. A link cannot be disabled, so the press is refused in the handler |
as |
Your router’s Link, or any element |
cooldown |
Milliseconds, or { ms, key, storage } to survive a reload |
shortcut |
One key. Refused while typing and with any modifier held |
pending |
Replaces the label while async work runs |
children |
A node, or a function of the state |
type |
"button" by default, so it never submits the form it lives in |