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 @appearance='soft'>Soft</Button>
<Button @appearance='outlined'>Outlined</Button>
<Button @appearance='tonal'>Tonal</Button>
<Button @appearance='minimal'>Minimal</Button>
<Button @appearance='custom'>Custom</Button>
</div>
</template>
The custom appearance is available for the cases where you might want to fully customize the appearance of the button.
The default styles are mainly structural. Intent colors are applied as color.
Every intent is available in every appearance. The label on each row is the
@appearance value; the button labels are the @intent values.
@appearance='default'
@appearance='soft'
@appearance='outlined'
@appearance='tonal'
@appearance='minimal'
import { Button } from 'frontile';
import { array } from '@ember/helper';
const intents = [
'default',
'primary',
'secondary',
'tertiary',
'success',
'warning',
'danger'
];
<template>
<div class='flex flex-col gap-6'>
{{#each
(array 'default' 'soft' 'outlined' 'tonal' 'minimal')
as |appearance|
}}
<div>
<p class='font-code text-code-sm text-neutral-strong mb-2'>
@appearance='{{appearance}}'
</p>
<div class='flex flex-wrap items-center gap-3'>
{{#each intents as |intent|}}
<Button @appearance={{appearance}} @intent={{intent}}>
{{intent}}
</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' @intent='primary'><DownloadIcon /> Download</Button>
<Button @size='sm' @intent='primary'><DownloadIcon /> Download</Button>
<Button @intent='primary'><DownloadIcon /> Download</Button>
<Button @size='lg' @intent='primary'><DownloadIcon /> Download</Button>
<Button @size='xl' @intent='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 @intent='primary'><DownloadIcon /> Download</Button>
<Button @appearance='outlined' @intent='default'>Share
<ShareIcon /></Button>
<Button @appearance='tonal' @intent='success'><CheckIcon /> Confirm</Button>
</div>
</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 @intent='primary'>
<span class='inline-flex items-center gap-1'>
Upgrade
<span class='font-body text-body-xs'>/mo</span>
</span>
</Button>
<Button @appearance='outlined' @intent='default'>
<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-micro |
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' @intent='primary'>
<span class='inline-flex items-center gap-0.5'>
$9
<span class='font-body text-body-2xs'>/mo</span>
</span>
</Button>
<Button @intent='primary'>
<span class='inline-flex items-center gap-1'>
$19
<span class='font-body text-body-xs'>/mo</span>
</span>
</Button>
<Button @size='lg' @intent='primary'>
<span class='inline-flex items-center gap-1'>
$29
<span class='font-body text-body-sm'>/mo</span>
</span>
</Button>
<Button @size='2xl' @intent='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 @intent='default' disabled>Default</Button>
<Button @intent='primary' disabled>Primary</Button>
<Button @intent='secondary' disabled>Secondary</Button>
<Button @intent='tertiary' disabled>Tertiary</Button>
<Button @intent='success' disabled>Success</Button>
<Button @intent='warning' disabled>Warning</Button>
<Button @intent='danger' disabled>Danger</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='javascript:void(0)' class={{btn.classNames}}>My Link</a>
</Button>
</template>
You can compose appearance with intents and more to create the button that best fits your needs.
import { Button } from 'frontile';
<template>
<Button @appearance='outlined' @intent='primary'>Button</Button>
<Button @appearance='minimal' @intent='warning'>Button</Button>
<Button @size='xs' @intent='danger'>Button</Button>
</template>
You can use TailwindCSS classes to customize even further.
import { Button } from 'frontile';
<template>
<Button @appearance='outlined' @intent='primary' @class='px-20 py-2 italic'>
Button
</Button>
</template>
Here is another example using TailwindCSS classes with the custom appearance.
import { Button } from 'frontile';
<template>
<Button
@appearance='custom'
class='bg-teal-100 hover:bg-teal-200 hover:text-teal-600 border-teal-600 rounded-none 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 @intent='primary' @onPress={{this.handlePress}}>
@onPress ({{this.pressCount}})
</Button>
<Button @appearance='outlined' {{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 deliberately 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 @intent='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.
@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.
Element: <span class="hljs-title class_">HTMLButtonElement</span>
| Name | Type | Default | Description |
|---|---|---|---|
appearance
|
enum
|
'default'
|
The button appearance |
class
|
string
|
- | Custom class name, it will override the default ones using Tailwind Merge library. |
intent
|
enum
|
- | The intent of the button |
isInGroup
|
boolean
|
- | If button is part of a group. Most of the time, this is automatically set when using the ButtonGroup component. |
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 |
| Name | Type | Default | Description |
|---|---|---|---|
default
*
|
Array
|
- |