Tucano v0.37.2

Button

It came from what the components needed internally — the toast's "Undo", the calendar's "Apply". Instead of each one inventing its own, they all come from here, and it's available to your project too. It's just classes: a <button> is still a <button>.

Examples

Variant, size and state are classes and attributes that combine.

Sizes

is-sm is 30px, the default comes from --tuc-control-height and is-lg is 44px.

With an icon

The <svg> picks up the button's size on its own. is-icon makes it square — and then aria-label is required.

Busy

aria-busy="true" with .tuc-spinner. Click "Save" to see it switch.

Disabled

disabled gets neither clicks nor focus; aria-disabled="true" stays focusable and announced.

Full width

is-block fills the row — the button of a narrow form or a phone screen.

As a link

On an <a> the class removes the underline; it's the right choice when the button navigates rather than acts.

How to use

tuc-btn plus a variant. Without a variant the button is transparent and borderless — the base the variants build on.

<button type="submit" class="tuc-btn is-primary">Save</button>
<button type="button" class="tuc-btn is-outline is-sm">Cancel</button>
<button type="button" class="tuc-btn is-ghost is-icon" aria-label="Close">
  <svg>...</svg>
</button>
<a class="tuc-btn is-link" href="/help/">Learn more</a>

In a Django form

The button belongs to the template, not to the form. With type="submit" it submits the form as usual.

<form method="post">
  {% csrf_token %}
  {{ form }}
  <a class="tuc-btn is-outline" href="{% url 'customers' %}">Cancel</a>
  <button type="submit" class="tuc-btn is-primary">Save</button>
</form>

In JavaScript

There's no component to instantiate. What you do in code is switch the state — for example, busy during a submit:

const btn = document.querySelector('#save');
btn.setAttribute('aria-busy', 'true');
btn.disabled = true;               // prevents a second click
btn.prepend(Object.assign(document.createElement('span'), { className: 'tuc-spinner' }));

// ...when it's done
btn.removeAttribute('aria-busy');
btn.disabled = false;
btn.querySelector('.tuc-spinner').remove();

Variants and when to use them

One primary action per area. The others step back in visual weight, and the destructive one stands out by color.

ClassWhat it's for
is-primaryPrimary action, in the accent color (--tuc-accent)
is-outlineSecondary action, with a border
is-ghostTertiary, no visual weight — actions inside a table, a toolbar
is-dangerDestructive, filled with --tuc-danger-fill
is-linkLooks like a link, behaves like a button

Why the button's red is a different token

--tuc-danger is lighter in dark mode, so it has contrast as text — and a button filled with it would look pastel with white text. --tuc-danger-fill is the same in both themes.

Buttons and fields share the same height

Both come from --tuc-control-height — 38px, or 44px in the compact layout, where the touch target needs to be bigger. Radius and border come from --tuc-radius-md and --tuc-border-width. A button next to a select lines up with no adjustment.

Busy and disabled

Both block the action, but they say different things, so they look different.

<button class="tuc-btn is-primary" aria-busy="true" disabled>
  <span class="tuc-spinner"></span> Saving
</button>

<button class="tuc-btn is-outline" aria-disabled="true">Export</button>

Busy is not disabled

disabled alongside aria-busy prevents the second click, but the 50% fade would say "not available" — and the button is working. With aria-busy="true" it keeps its full color and the wait cursor. The spinner is sized in em and painted with currentColor: inside the button it takes the icon's place and comes out white on a primary, with no variant needed.

Disabled shows the not-allowed cursor

No pointer-events: none: an element that doesn't receive the pointer can't change the cursor, and the person gets no sign that the button won't respond. On a <button disabled> the browser already blocks the click. With aria-disabled="true" the click still arrives on purpose — the button stays focusable and announced, and whoever uses the attribute handles the click in their own handler.

Disabled also doesn't react to hover — changing color would suggest it still works — and it loses its shadow. When pressed, the button shrinks slightly: on a tap, with no hover, color alone doesn't confirm the finger hit the target. With prefers-reduced-motion that movement is removed.

Keyboard and accessibility

It's the browser's <button>: focus, Enter and Space come for free. What's left to the template is picking the right element and naming whatever has no text.

KeyAction
TabReaches the button, with the same focus ring as the fields; skips disabled, not aria-disabled
Enter SpaceActivates the <button>; on an <a class="tuc-btn">, only Enter

Classes

CSS only, no JavaScript. Don't write a second button style: an action inside a table or a code block is also a .tuc-btn, with its own class only for positioning.

Class or attributeWhat it's for
tuc-btnThe base: height, font, radius, focus. Works on <button> and <a>
is-primary is-outline is-ghost is-danger is-linkVariant
is-sm is-lgSize: 30px and 44px; the icon follows
is-iconSquare, icon only; combines with is-sm and is-lg
is-blockFills the full width
disabledFaded, not-allowed cursor, no hover; the browser blocks the click
aria-disabled="true"Same look, but focusable and with the click getting through
aria-busy="true"Busy: full color and wait cursor, even with disabled
.tuc-spinner insideTakes the icon's place and size
--tuc-btn-iconSize of the child <svg>; each button size sets its own