Button
Five variants across six tones, rendered on the server with no JavaScript. Through render it becomes a real link that looks the same.
server - 0 KB
Button
server - 0 KBFive variants across six tones, rendered on the server with no JavaScript. Through render it becomes a real link that looks the same.
Try it
variant
tone
size
loading
fullWidth
The code
button.tsx
import { Button } from 'topclassui/button'
<Button>Save</Button>loading blocks clicks and announces itself with aria-busy, but keeps full colour, so it never reads as disabled.
Every state
5 statesExamples
- For anything the user does on the page: submit, save, confirm, open a dialog.
- To go somewhere else, keep
Buttonand passrender={<Link href="/docs" />}: it stays a real anchor, so middle-click and "open in new tab" still work. loadingkeeps full colour and shows a spinner; a disabled button dims. Both block it, and a blockedrender={<a href />}loses itshref, so it cannot be followed.- A long label ends in an ellipsis and never pushes the page wider;
fullWidthstretches the button to its container. size="custom"emits no size classes, so your ownclassNamesets the height and padding.
variants
variants.tsx
import { Button } from 'topclassui/button'
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button>Save</Button>
<Button variant="soft">Draft</Button>
<Button variant="outline">Cancel</Button>
<Button variant="ghost">Skip</Button>
<Button variant="link">Learn more</Button>
</div>
)
}
tones
tones.tsx
import { Button } from 'topclassui/button'
export default function Demo() {
return (
<div className="flex flex-col gap-3">
<div className="flex flex-wrap items-center gap-3">
<Button tone="primary">Primary</Button>
<Button tone="neutral">Neutral</Button>
<Button tone="success">Success</Button>
<Button tone="warning">Warning</Button>
<Button tone="danger">Danger</Button>
<Button tone="info">Info</Button>
</div>
<div className="flex flex-wrap items-center gap-3">
<Button variant="soft" tone="primary">Primary</Button>
<Button variant="soft" tone="neutral">Neutral</Button>
<Button variant="soft" tone="success">Success</Button>
<Button variant="soft" tone="warning">Warning</Button>
<Button variant="soft" tone="danger">Danger</Button>
<Button variant="soft" tone="info">Info</Button>
</div>
<div className="flex flex-wrap items-center gap-3">
<Button variant="outline" tone="primary">Primary</Button>
<Button variant="outline" tone="success">Success</Button>
<Button variant="outline" tone="danger">Danger</Button>
<Button variant="ghost" tone="warning">Warning</Button>
<Button variant="ghost" tone="info">Info</Button>
<Button variant="link" tone="neutral">Neutral</Button>
</div>
</div>
)
}
sizes
sizes.tsx
import { Button } from 'topclassui/button'
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button size="xs">Extra small</Button>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra large</Button>
</div>
)
}
with-icon
with-icon.tsx
import { arrowRight } from 'topclassui/icons/arrow-right'
import { check } from 'topclassui/icons/check'
import { Svg } from 'topclassui/svg'
import { Button } from 'topclassui/button'
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button>
<Svg icon={check} />
Approve
</Button>
<Button variant="outline">
Next
<Svg icon={arrowRight} />
</Button>
</div>
)
}
as-link
as-link.tsx
import { Button } from 'topclassui/button'
/** The render prop makes a real anchor that looks like a button and still costs 0 KB. */
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button render={<a href="/docs" />}>Read the docs</Button>
<Button render={<a href="/docs" />} variant="outline">Outlined link</Button>
</div>
)
}
full-width
full-width.tsx
import { Button } from 'topclassui/button'
export default function Demo() {
return (
<div className="flex max-w-sm flex-col gap-3">
<Button fullWidth>Continue</Button>
<Button variant="outline" fullWidth>Back</Button>
</div>
)
}
long-label
long-label.tsx
import { check } from 'topclassui/icons/check'
import { Svg } from 'topclassui/svg'
import { Button } from 'topclassui/button'
const LONG = 'Save this document and continue to the next step of the wizard'
export default function Demo() {
return (
<div className="flex flex-col gap-3">
<div className="w-50 rounded-box border border-line p-2" data-narrow>
<Button>{LONG}</Button>
</div>
<div className="w-50 rounded-box border border-line p-2" data-narrow>
<Button variant="outline" tone="danger">
<Svg icon={check} />
{LONG}
</Button>
</div>
<div className="w-50 rounded-box border border-line p-2" data-narrow>
<Button fullWidth size="sm">{LONG}</Button>
</div>
</div>
)
}
custom-size
custom-size.tsx
import { Button } from 'topclassui/button'
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button size="md">The scale</Button>
<Button size="custom" className="h-control-xl px-pad-x-sm text-xs">Your own row</Button>
</div>
)
}
skeleton
skeleton.tsx
import { ButtonSkeleton } from 'topclassui/button'
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-3">
<ButtonSkeleton size="sm" />
<ButtonSkeleton />
<ButtonSkeleton size="lg" />
</div>
)
}
In Arabic, right to left
rtl.tsx
import { arrowRight } from 'topclassui/icons/arrow-right'
import { Svg } from 'topclassui/svg'
import { Button } from 'topclassui/button'
export default function Demo() {
return (
<div dir="rtl" lang="ar" className="flex flex-wrap items-center gap-3">
<Button>احفظ</Button>
<Button variant="soft" tone="danger">احذف</Button>
<Button variant="outline">
التالي
<Svg icon={arrowRight} />
</Button>
</div>
)
}
Props
Keyboard and accessibility
- A native
<button type="button">by default, named by its own text; an icon beside the text is hidden from screen readers. - While blocked, a native button carries the
disabledattribute and leaves the tab order; any other element getsaria-disabledandtabindex="-1". - At least 24 by 24 px, 44 under a touch pointer, with a visible focus ring.
Right to left
Nothing to set: the padding and the gap are logical, so the icon and the text swap sides together, and a directional icon such as an arrow flips.
Server and client
Renders on the server and ships no JavaScript.
Files: index.tsx.
On the chip: server - 0 KB.