The Button component can be used to trigger an action, such as submitting a form, opening a modal, and more.
import { Button } from 'frontile';
import { Button } from 'frontile';
<template>
<Button>Button</Button>
</template>
import { Button } from 'frontile';
<template>
<div class='flex flex-wrap items-center gap-3'>
<Button>Default</Button>
<Button @variant='subtle'>Subtle</Button>
<Button @variant='outline'>Outline</Button>
<Button @variant='soft'>Soft</Button>
<Button @variant='plain'>Plain</Button>
<Button @variant='custom'>Custom</Button>
</div>
</template>
The custom variant is available for the cases where you might want to fully customize the appearance of the button.
The default styles are mainly structural. Colors are applied as color.
Every color is available in every variant. The label on each row is the
@variant value; the button labels are the @color values.
@variant='solid'
@variant='subtle'
@variant='outline'
@variant='soft'
@variant='plain'
import { Button } from 'frontile';
import { array } from '@ember/helper';
const colors = [
'neutral',
'primary',
'secondary',
'tertiary',
'success',
'warning',
'danger'
];
<template>
<div class='flex flex-col gap-6'>
{{#each (array 'solid' 'subtle' 'outline' 'soft' 'plain') as |variant|}}
<div>
<p class='font-code text-code-sm text-neutral-strong mb-2'>
@variant='{{variant}}'
</p>
<div class='flex flex-wrap items-center gap-3'>
{{#each colors as |color|}}
<Button @variant={{variant}} @color={{color}}>
{{color}}
</Button>
{{/each}}
</div>
</div>
{{/each}}
</div>
</template>
import { Button } from 'frontile';
<template>
<div class='flex flex-wrap items-center gap-3'>
<Button @size='xs'>Button xs</Button>
<Button @size='sm'>Button sm</Button>
<Button>Button md</Button>
<Button @size='lg'>Button lg</Button>
<Button @size='xl'>Button xl</Button>
<Button @size='2xl'>Button 2xl</Button>
</div>
</template>
import { Button } from 'frontile';
import { DownloadIcon, ShareIcon, CheckIcon } from 'site/components/icons';
<template>
<div class='flex flex-wrap items-center gap-3'>
<Button @size='xs' @color='primary'><DownloadIcon /> Download</Button>
<Button @size='sm' @color='primary'><DownloadIcon /> Download</Button>
<Button @color='primary'><DownloadIcon /> Download</Button>
<Button @size='lg' @color='primary'><DownloadIcon /> Download</Button>
<Button @size='xl' @color='primary'><DownloadIcon /> Download</Button>
</div>
</template>
Icons can be placed before or after text. They inherit the button's text color via currentColor.
import { Button } from 'frontile';
import { DownloadIcon, ShareIcon, CheckIcon } from 'site/components/icons';
<template>
<div class='flex gap-4'>
<Button @color='primary'><DownloadIcon /> Download</Button>
<Button @variant='outline' @color='neutral'>Share
<ShareIcon /></Button>
<Button @variant='soft' @color='success'><CheckIcon /> Confirm</Button>
</div>
</template>
Icons passed as plain content keep working exactly as above. The icon named
block is opt-in sugar: it buys you @iconPlacement and, when the button is
loading, it is the slot the spinner takes over.
import { Button } from 'frontile';
import { ShareIcon } from 'site/components/icons';
<template>
<Button>
<:icon><ShareIcon /></:icon>
<:default>Share</:default>
</Button>
</template>
A label can carry a smaller trailing unit — a price suffix like /mo, a count,
or an abbreviation. The label keeps the bold strong text role that the size
variant already applies; the unit uses the regular-weight body role, two steps
down the scale.
Wrap the pair so the unit sits tight against the label: the button's own gap
spaces the icon slots, while the wrapper's narrower gap spaces label from unit.
import { Button } from 'frontile';
import { StarIcon } from 'site/components/icons';
<template>
<div class='flex flex-wrap items-center gap-3'>
<Button @color='primary'>
<span class='inline-flex items-center gap-1'>
Upgrade
<span class='font-body text-body-xs'>/mo</span>
</span>
</Button>
<Button @variant='outline' @color='neutral'>
<StarIcon />
<span class='inline-flex items-center gap-1'>
Button
<span class='font-body text-body-xs'>/mo</span>
</span>
<StarIcon />
</Button>
</div>
</template>
The unit token pairs with the label token the size variant sets, so it needs to
change with @size:
@size |
label (automatic) | unit | wrapper gap |
|---|---|---|---|
xs |
text-strong-sm |
text-body-3xs |
gap-0.5 |
sm |
text-strong-md |
text-body-2xs |
gap-0.5 |
md |
text-strong-lg |
text-body-xs |
gap-1 |
lg |
text-strong-xl |
text-body-sm |
gap-1 |
xl |
text-strong-2xl |
text-body-md |
gap-1 |
2xl |
text-strong-3xl |
text-body-lg |
gap-1.5 |
import { Button } from 'frontile';
<template>
<div class='flex flex-wrap items-center gap-3'>
<Button @size='sm' @color='primary'>
<span class='inline-flex items-center gap-0.5'>
$9
<span class='font-body text-body-2xs'>/mo</span>
</span>
</Button>
<Button @color='primary'>
<span class='inline-flex items-center gap-1'>
$19
<span class='font-body text-body-xs'>/mo</span>
</span>
</Button>
<Button @size='lg' @color='primary'>
<span class='inline-flex items-center gap-1'>
$29
<span class='font-body text-body-sm'>/mo</span>
</span>
</Button>
<Button @size='2xl' @color='primary'>
<span class='inline-flex items-center gap-1.5'>
$99
<span class='font-body text-body-lg'>/mo</span>
</span>
</Button>
</div>
</template>
import { Button } from 'frontile';
<template>
<div class='flex flex-wrap items-center gap-3'>
<Button @color='neutral' disabled>Default</Button>
<Button @color='primary' disabled>Primary</Button>
<Button @color='secondary' disabled>Secondary</Button>
<Button @color='tertiary' disabled>Tertiary</Button>
<Button @color='success' disabled>Success</Button>
<Button @color='warning' disabled>Warning</Button>
<Button @color='danger' disabled>Danger</Button>
</div>
</template>
@isLoading renders a spinner and disables the button.
import { Button } from 'frontile';
<template>
<div class='flex flex-wrap items-center gap-3'>
<Button @isLoading={{true}}>Save</Button>
<Button @variant='outline' @isLoading={{true}}>Save</Button>
<Button @color='danger' @isLoading={{true}}>Delete</Button>
</div>
</template>
Use the loading block to swap the label while the action is in flight.
import { Button } from 'frontile';
<template>
<Button @isLoading={{true}}>
<:default>Save</:default>
<:loading>Saving…</:loading>
</Button>
</template>
The spinner takes the place of the icon block, so a button with an icon keeps
its width while loading.
import { Button } from 'frontile';
import { ShareIcon } from 'site/components/icons';
<template>
<div class='flex flex-wrap items-center gap-3'>
<Button>
<:icon><ShareIcon /></:icon>
<:default>Share</:default>
</Button>
<Button @isLoading={{true}}>
<:icon><ShareIcon /></:icon>
<:default>Share</:default>
</Button>
</div>
</template>
@iconPlacement='end' moves both the icon and the spinner after the label.
import { Button } from 'frontile';
import { ShareIcon } from 'site/components/icons';
<template>
<div class='flex flex-wrap items-center gap-3'>
<Button @iconPlacement='end'>
<:icon><ShareIcon /></:icon>
<:default>Share</:default>
</Button>
<Button @iconPlacement='end' @isLoading={{true}}>
<:icon><ShareIcon /></:icon>
<:default>Share</:default>
</Button>
</div>
</template>
Sometimes a button element is not ideal for a given case, but the same styles are still desired.
Frontile provides the option to disable rendering the button element, but instead it yields back an object with
the class names it would use.
import { Button } from 'frontile';
<template>
<Button @isRenderless={{true}} as |btn|>
<a href='#composition' class={{btn.classNames}}>Jump to composition</a>
</Button>
</template>
You can compose variant with colors and more to create the button that best fits your needs.
import { Button } from 'frontile';
<template>
<Button @variant='outline' @color='primary'>Button</Button>
<Button @variant='plain' @color='warning'>Button</Button>
<Button @size='xs' @color='danger'>Button</Button>
</template>
You can use TailwindCSS classes to customize even further.
import { Button } from 'frontile';
<template>
<Button @variant='outline' @color='primary' @class='px-20 py-2 italic'>
Button
</Button>
</template>
Here is another example using TailwindCSS classes with the custom variant.
import { Button } from 'frontile';
<template>
<Button
@variant='custom'
class='rounded-none border-teal-600 bg-teal-100 text-teal-900 hover:bg-teal-200 hover:text-teal-700 dark:border-teal-300 dark:bg-teal-950 dark:text-teal-100 dark:hover:bg-teal-900 dark:hover:text-teal-50 border-dashed'
>
Button
</Button>
</template>
Note that here we used the HTML attribute class, instead of the argument @class.
Using the class attribute will just append the class names passed in, while the
argument @class will override and merge TailwindCSS class names.
The Button component supports press interactions through the @onPress callback, which provides cross-platform support for mouse, touch, and keyboard events.
Button has been pressed 0 times
import { Button } from 'frontile';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class ButtonPressExample extends Component {
@tracked pressCount = 0;
handlePress = () => {
this.pressCount++;
};
<template>
<div class='flex items-center space-x-4'>
<Button @onPress={{this.handlePress}}>
Press me! ({{this.pressCount}})
</Button>
<p>Button has been pressed {{this.pressCount}} times</p>
</div>
</template>
}
Buttons automatically track their pressed state and add a data-pressed attribute when being pressed, which can be used for styling:
button[data-pressed='true'] {
transform: scale(0.95);
transition: transform 0.1s ease;
}
Button renders a native <button type="button">, so focus order, the disabled
state, and Enter/Space activation come from the platform. What follows is what
you still have to get right.
| Key | Behaviour |
|---|---|
Tab |
Moves focus to the button. Disabled buttons are skipped. |
Enter |
Activates the button, firing @onPress. |
Space |
Activates the button, firing @onPress. |
@onPress, not a click listenerThe press modifier calls preventDefault() on Enter and Space so the two keys
behave identically across elements. A side effect is that the browser never
synthesises the click event it would normally follow with — so a
{{on "click"}} handler on a default @type='button' fires for the mouse but
not for the keyboard:
Activate each with the mouse, then again with Tab + Enter. Only the first counter moves the second time.
import { Button } from 'frontile';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
export default class KeyboardParityExample extends Component {
@tracked pressCount = 0;
@tracked clickCount = 0;
handlePress = () => {
this.pressCount++;
};
handleClick = () => {
this.clickCount++;
};
<template>
<div class='flex flex-wrap items-center gap-4'>
<Button @color='primary' @onPress={{this.handlePress}}>
@onPress ({{this.pressCount}})
</Button>
<Button @variant='outline' {{on 'click' this.handleClick}}>
click listener ({{this.clickCount}})
</Button>
<p class='text-neutral'>
Activate each with the mouse, then again with
<kbd>Tab</kbd>
+
<kbd>Enter</kbd>. Only the first counter moves the second time.
</p>
</div>
</template>
}
@type='submit' and @type='reset' are exempt — the default is preserved
there, so a button inside a form still submits or resets it from the keyboard.
An icon on its own leaves the button unnamed. Give it an aria-label — it
passes through to the element via ...attributes:
import { Button } from 'frontile';
import { ShareIcon } from 'site/components/icons';
<template>
<Button @color='primary' aria-label='Share this page'>
<ShareIcon />
</Button>
</template>
There is no @isDisabled argument. Pass the plain HTML disabled attribute, as
the Disabled demo above does; it removes the button from the tab
order and is what assistive technology reports.
@isLoading sets the plain HTML disabled attribute, which has two
consequences worth designing around.
Focus is lost. A button that disables itself mid-press drops focus to
<body>, so keyboard and screen reader users lose their place and hear
nothing. Put the outcome of the action in a live region, or leave the button
enabled and guard against re-entry inside your handler.
The button widens by the spinner plus the gap, unless it has an icon
block for the spinner to take over. Either use the icon block, or set a
min-w-* class on buttons that toggle between states.
A plain-content icon is not replaced. The spinner only swaps in for an
icon block (see With Icons); an icon passed as ordinary
content — <Button @isLoading={{true}}><DownloadIcon /> Download</Button> —
stays on screen alongside the spinner, rendering both. If you want the
loading-swap behavior, move the icon into <:icon>.
@isRenderless hands back only class names, so every semantic the <button>
provided becomes yours. An <a href> is already focusable and activates on
Enter; anything else — a <div>, a <span> — needs role='button',
tabindex='0', and its own key handling. Prefer a real <button> or <a> over
recreating that.
@isRenderless yields only to the default block — <:icon> and <:loading>
are ignored, because there is no element for the component to compose. The
yielded hash still carries isLoading, so you can branch on it yourself.
Element: HTMLButtonElement
| Name | Type | Default | Description |
|---|---|---|---|
appearance
Deprecated
|
enum
|
- |
Deprecated. Use `variant`. `default` is now `solid`, `outlined` is `outline`, and `minimal` is `plain`. |
class
|
string
|
- | Custom class name, it will override the default ones using Tailwind Merge library. |
color
|
enum
|
- | The color of the button |
iconPlacement
|
enum
|
'start'
|
Which side the icon block — and the loading spinner that replaces it —
sits on.
|
intent
Deprecated
|
enum
|
- |
Deprecated. Use `color`. `default` is now `neutral`. |
isInGroup
|
boolean
|
- | If button is part of a group. Most of the time, this is automatically set when using the ButtonGroup component. |
isLoading
|
boolean
|
false
|
Renders a spinner in place of the icon and disables the button. |
isRenderless
|
boolean
|
- | Disable rendering the button element. It yields an object with classNames instead. |
onPress
|
function
|
- | Callback for when the button is pressed. |
size
|
enum
|
- | The size of the button |
type
|
enum
|
'button'
|
The HTML type of the button |
variant
|
enum
|
'solid'
|
The button variant. |
| Name | Type | Default | Description |
|---|---|---|---|
default
*
|
Array
|
- | |
icon
|
Array
|
- | |
loading
|
Array
|
- |