Alert
What stays true while the person is on the screen: "your subscription expires in 3 days", "this contract is locked for editing". The toast is for what just happened and goes away on its own; an alert that disappears before it's read didn't alert anyone. Classes only, no JavaScript.
The figures on this screen are refreshed every 5 minutes.
Contract signed
Both parties signed on 2026-09-12.
Subscription expires in 3 days
Renew so you don't lose access to reports.
Contract locked for editing
There's a disputed charge. Contact the billing team.
Examples
The parts are optional: title, icon and actions go in only when they're needed.
No tone
Without is-* the alert is neutral, for what's neither good nor bad.
Draft saved at 14:32.
No icon
The icon is the first child, when there is one; without it the text sits against the edge.
Scheduled maintenance
Saturday, 10 PM to 11 PM.
With two actions
The actions are the system's buttons. On a narrow screen, they drop below the text.
There are unpublished changes.
Shows up later
The response to a submit gets role="alert" and is announced right away.
How to use
A .tuc-alert with the tone, the icon as the first child and the text in .tuc-alert__body.
<div class="tuc-alert is-warning">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 9v4M12 17h.01M10.3 3.9L1.8 18a2 2 0 001.7 3h17a2 2 0 001.7-3L13.7 3.9a2 2 0 00-3.4 0z"/></svg>
<div class="tuc-alert__body">
<p class="tuc-alert__title">Subscription expires in 3 days</p>
<p>Renew so you don't lose access.</p>
</div>
<div class="tuc-alert__actions">
<a class="tuc-btn is-outline is-sm" href="/subscription/">Renew</a>
</div>
</div>The icon doesn't come from the library: it's the SVG you paste, and it inherits the tone's color. The ones on this page, to copy:
<!-- info -->
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 16v-4M12 8h.01M12 22a10 10 0 100-20 10 10 0 000 20z"/></svg>
<!-- success -->
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 11.1V12a10 10 0 11-5.9-9.1M22 4L12 14l-3-3"/></svg>
<!-- warning -->
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 9v4M12 17h.01M10.3 3.9L1.8 18a2 2 0 001.7 3h17a2 2 0 001.7-3L13.7 3.9a2 2 0 00-3.4 0z"/></svg>
<!-- danger -->
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 22a10 10 0 100-20 10 10 0 000 20zM15 9l-6 6M9 9l6 6"/></svg>In a Django template
The error that doesn't belong to a field, non_field_errors, is the most common case. And Django messages can
become an alert, instead of a toast, when they need to stay on the screen. Django's error level is called error; the alert's,
is-danger.
{% if form.non_field_errors %}
<div class="tuc-alert is-danger">
<div class="tuc-alert__body">
<p class="tuc-alert__title">Couldn't save</p>
{% for error in form.non_field_errors %}<p>{{ error }}</p>{% endfor %}
</div>
</div>
{% endif %}
{% for m in messages %}
<div class="tuc-alert {% if m.level_tag == 'error' %}is-danger{% else %}is-{{ m.level_tag }}{% endif %}">
<div class="tuc-alert__body"><p>{{ m }}</p></div>
</div>
{% endfor %}The debug level becomes is-debug, which has no rule, so the alert stays neutral.
Accessibility
role="alert" only on an alert that appears after the page loads: the response to a submit, an HTMX swap.
Why not on all of them
An alert that's already in the HTML is read along with the page. With role="alert" it would interrupt the screen reader for nothing, and on a page with three alerts, three times. What arrives later needs the role precisely to interrupt: without it, the submit response shows up on screen and no screen reader user ever finds out.
{# partial HTMX response: shows up later, so it interrupts #}
<div class="tuc-alert is-success" role="alert">
<div class="tuc-alert__body"><p>Proposal sent to {{ customer.email }}.</p></div>
</div>The icon gets aria-hidden="true": the text is what says what happened, and the tone can't be the only cue.
Color
The background is a light tint of the tone over --tuc-bg, not the transparent -soft from the
tokens: over a colored card or an image, the transparent one would leave the text without guaranteed contrast.
The tones read --tuc-info, --tuc-success, --tuc-warning and --tuc-danger, and
never --tuc-accent. The brand can be any color, and the meaning can't change along with it. That's why
info got its own tone: with an orange accent, the info alert looked the same as the warning one.
Classes
| Class or attribute | What it's for |
|---|---|
.tuc-alert | The alert. Without a tone, neutral |
is-info is-success is-warning is-danger | The tone |
> svg | Icon, as the first child: 16px, centered on the first line, in the tone's color |
.tuc-alert__body | The text; takes up the remaining space |
.tuc-alert__title | First line in bold. The rest of the body uses the muted color |
.tuc-alert__actions | Buttons on the right; below 40rem, they drop below the text |
role="alert" | Only on what appears after the page loads |
--tuc-alert-tone | The color the tone picks; background, border and icon come from it |