---
title: "الزر"
description: "يتيح للأشخاص تنفيذ إجراء، أو تشغيل شيء وإيقافه، أو الذهاب إلى وجهة، بنقرة واحدة."
url: "https://tanrei.chnotchy.com/ar/components/button/"
last_updated: 2026-10-26T22:31:46.000Z
---

# الزر

> يتيح للأشخاص تنفيذ إجراء، أو تشغيل شيء وإيقافه، أو الذهاب إلى وجهة، بنقرة واحدة.

## نظرة عامة

### الاستخدام

تعرض عيّنتا Web و React المواصفة الحالية. أما SwiftUI و Compose و Flutter و React Native فمتأخرة: ما زالت تقدّم المراجعة السابقة من الزر، وفيها تكون هيئة الأيقونة وحدها والتبديل زر أيقونة منفصلًا، وليس فيها عروض للزر ذي الأيقونة وحدها ولا هيئة الرابط. وتعرض عيّناتها تلك المراجعة إلى أن تلحق بالمواصفة.

**Web**

```html
<tanrei-button variant="filled">نشر</tanrei-button>
<tanrei-button variant="outlined" size="small">إلغاء</tanrei-button>
<tanrei-button busy busy-label="جارٍ النشر">نشر</tanrei-button>

<!-- مع href يكون رابطًا، مرسومًا بهيئة الزر نفسه. -->
<tanrei-button variant="tonal" href="/pricing/">عرض الأسعار</tanrei-button>

<script type="module">
  import '@tanrei/web/components/button';
  document.querySelector('tanrei-button').addEventListener('click', deploy);
</script>
```

**React**

```tsx
import { Button } from '@tanrei/react';

<Button variant="filled" onClick={deploy}>نشر</Button>
<Button variant="outlined" size="small" onClick={cancel}>إلغاء</Button>
<Button disabled>جارٍ النشر…</Button>

// مع href يكون رابطًا، مرسومًا بهيئة الزر نفسه.
<Button variant="tonal" href="/pricing/">عرض الأسعار</Button>
```

**SwiftUI**

```swift
import Tanrei

TanreiButton("نشر", variant: .filled) { deploy() }
TanreiButton("إلغاء", variant: .outlined, size: .small) { cancel() }

// التعطيل يتبع بيئة SwiftUI.
TanreiButton("جارٍ النشر…") {}
  .disabled(true)
```

**Compose**

```kotlin
import io.github.chnotchy.tanrei.components.*

TanreiButton(text = "نشر", onClick = ::deploy, variant = TanreiButtonVariant.Filled)
TanreiButton(text = "إلغاء", onClick = ::cancel, variant = TanreiButtonVariant.Outlined, size = TanreiButtonSize.Small)

// يستخدم Compose الراية المعاكسة.
TanreiButton(text = "جارٍ النشر…", onClick = {}, enabled = false)
```

**Flutter**

```dart
import 'package:tanrei/tanrei.dart';

TanreiButton(variant: TanreiButtonVariant.filled, onPressed: deploy, child: const Text('نشر'));
TanreiButton(variant: TanreiButtonVariant.outlined, size: TanreiButtonSize.small, onPressed: cancel, child: const Text('إلغاء'));

// القيمة null في onPressed تعطّل الزر.
TanreiButton(onPressed: null, child: const Text('جارٍ النشر…'));
```

**React Native**

```tsx
import { Button } from '@tanrei/react-native';

<Button variant="filled" onPress={deploy}>نشر</Button>
<Button variant="outlined" size="small" onPress={cancel}>إلغاء</Button>
<Button disabled>جارٍ النشر…</Button>
```

### أيقونة، وأيقونة وحدها، وأزرار التبديل

يمكن أن يحمل الزر أيقونة بجانب تسميته، في البداية، أو في النهاية حين تُضبط الخاصية `iconPosition` على القيمة `trailing`. الحشو من جهة الأيقونة أضيق بدرجة من الحشو من جهة التسمية، فيبدو الاثنان متوازنين. ومن دون تسمية يكون الزر أيقونة وحدها: دائرة عرضها يساوي ارتفاع الزر، تسمّيها الخاصية `label`، وهي عندئذ مطلوبة. وحين تُضبط الخاصية `toggle` يبدّل الزر قيمة `selected` الخاصة به عند كل ضغطة، ويُظهر الحالة بألوانه وبتعبئة رسم الأيقونة (glyph)، ويُعلِنها حالةَ ضغط.

زر الأيقونة ليس مكوّنًا منفصلًا في Tanrei. إن كنت تعرف أسماء Material، فهذا ما آلت إليه:

| Material | Tanrei |
| --- | --- |
| زر الأيقونة (Icon button) | زر له أيقونة وليست له تسمية |
| زر الأيقونة القياسي (Standard icon button) | الطراز `text` |
| أزرار الأيقونة Filled و Tonal و Outlined | الطُرُز `filled` و `tonal` و `outlined` |
| زر تبديل الأيقونة (Icon toggle button) | `toggle` و `selected` |

أعطِ زر التبديل من الطراز text أيقونة. فمن دون حاوية، لا يُظهر زر التبديل من الطراز text الذي له تسمية فقط حالته إلا بتغيّر لون النص؛ أما تعبئة رسم الأيقونة فتجعل الحالة مرئية.

### عروض الزر ذي الأيقونة وحدها

للزر ذي الأيقونة وحدها ثلاثة عروض. لا يتغيّر ارتفاعه معها أبدًا، والزر الذي له تسمية يتجاهل الخاصية `width`.

| `width` | في النظام الافتراضي (small و medium و large) | يُستخدم لـ |
| --- | --- | --- |
| `default` | عرضه يساوي ارتفاعه: 32 و 40 و 56، دائرة | معظم إجراءات الأيقونات. |
| `narrow` | رسم الأيقونة وهامش داخلي صغير من كل جهة: 28 و 32 و 32، كبسولة | صف متراصّ من إجراءات الأيقونات، كالأدوات في نهاية حقل أو صف في جدول كثيف. |
| `wide` | 40 و 52 و 72، كبسولة | الإجراء الوحيد في صف الذي ينبغي أن يكون أثقل وزنًا من جيرانه. |

الزر الضيق هو رسم أيقونته والهامش الداخلي من كل جهة بالضبط، فيبقى الرسم دائمًا بعيدًا عن حافة الكبسولة، ويتبع الزر الرسم حيثما غيّر نظام تصميم حجمه. يُرسم الزر أضيق من الحد الأدنى للهدف، لكن مساحة اللمس ليست كذلك: فالهدف يمتد إلى ما وراء حوافه المرئية. اترك له فسحة بجانب الزر، أو ضع الأزرار الضيقة في [شريط أدوات](https://tanrei.chnotchy.com/ar/components/toolbar/) أو في [مجموعة أزرار](https://tanrei.chnotchy.com/ar/components/button-group/)، فكلاهما يباعد بين عناصره.

### الزر المشغول، والزر المعطَّل الذي يبقى في المتناول

ما دام الإجراء الذي بدأه الزر جاريًا، اضبط الخاصية `busy` بدلًا من `disabled`. الزر المشغول يحتفظ بحجمه وبمكانه في ترتيب التركيز وبتركيز الشخص. ويرسم قوس مؤشر التحميل مكان تسميته، ويرفض كل ضغطة، فلا يستطيع زر الإرسال أن يرسل نموذجه مرتين؛ ويوقف أيضًا المفتاح Enter في حقول النموذج. وتسمع التقنيات المساعدة النص `busy-label` («جارٍ النشر») مرة واحدة، حين يصبح الزر مشغولًا. قل ما حدث حين ينتهي الإجراء: فالزر لا يُعلن شيئًا عندئذ.

الزر المعطَّل يخرج من ترتيب التركيز، فقد لا يعثر عليه مستخدم لوحة المفاتيح أو قارئ الشاشة أبدًا، أو لا يعرف لماذا لا يفعل شيئًا. وحيث يستطيع الشخص أن يجعل الإجراء متاحًا، مثل زر الإرسال ما دامت الرسالة فارغة، أضف الخاصية `focusable-when-disabled`: يُرسم الزر ويُعلَن معطَّلًا ولا يفعل شيئًا، لكن المفتاح Tab يبلغه مع ذلك، فيستطيع [تلميح](https://tanrei.chnotchy.com/ar/components/tooltip/) أو وصف أن يقول السبب.

### بعرض كامل

أضف `full-width` ليصير الزر بعرض حاويته، مثل الإجراء الوحيد في أسفل نموذج أو بطاقة أو لوحة ضيقة. ويحتفظ بارتفاع حجمه وهوامشه الداخلية وزواياه، وتتوسّطه تسميته وأيقونته. والتسمية التي لا تتسع تنتقل إلى السطر التالي بدل أن تُقطع. وحيث يرتّب الأزرارَ [مجموعة أزرار](https://tanrei.chnotchy.com/ar/components/button-group/) أو [شريط أدوات](https://tanrei.chnotchy.com/ar/components/toolbar/) أو إجراءات حوار أو نافذة منبثقة، فهي التي تقرر العرض، ولا تفعل `full-width` على الزر شيئًا: أعطِ مجموعة الأزرار `full-width` لتتقاسم عدة أزرار صفًّا واحدًا.

### الإجراءات المدمرة

اضبط الخاصية `destructive` على زر يحذف إجراؤه شيئًا أو يتخلص منه وقد لا يمكن استعادته: «حذف»، «تجاهل»، «إزالة العضو». يحتفظ الزر بدرجة إبراز طرازه ويأخذ لون الخطأ فيه. فالزر من الطراز filled يُملأ بلون الخطأ، ومن الطراز tonal بلون حاوية الخطأ؛ والزر من الطراز elevated أو outlined أو text يرسم تسميته بلون الخطأ، والزر outlined يرسم حافته به أيضًا. وحين يكون معطَّلًا يبدو كأي زر معطَّل من طرازه.

اختر الطراز كما تختاره لأي إجراء آخر. استخدم filled حيث يكون الحذف هو الخطوة التي وُجدت الواجهة من أجلها، مثل الإجراء الذي يُتمّ مربع حوار «حذف المشروع؟». واستخدم outlined أو text حيث يكون إجراءً بين عدة إجراءات، مثل «إزالة» في كل صف من قائمة. والزر ذو الأيقونة وحدها يقبل `destructive` أيضًا، لكن التسمية أوضح من رسم الأيقونة لإجراء لا يمكن التراجع عنه. وزر التبديل ليس له مظهر مدمِّر.

ولا للرابط مظهر مدمِّر كذلك. فالزر الذي له `href` يتجاهل `destructive` ويُرسم رابطًا من طرازه، لأن اتباع الرابط لا يحذف شيئًا: إنه لا يفعل إلا أن يأخذ الشخص إلى المكان الذي يمكن فيه حذف شيء. فضع العلامة على الإجراء في ذلك المكان، مثل زر «حذف» في الصفحة التي يفتحها الرابط. وتبلّغ بنيات التنقيح (debug) عن أي رابط أُعطي `destructive`.

### الزر الذي له وجهة رابط

أعطِ الزر الخاصية `href` فيصير رابطًا لا زرًّا: عنصر التحكم نفسه، مرسومًا من الرموز (tokens) نفسها في كل حالة، لكن بدور الرابط وبكل ما تمنحه المنصة للرابط. على الويب هو عنصر anchor حقيقي، فيستطيع الأشخاص فتحه في تبويب جديد، ونسخ عنوانه، ورؤية وجهته قبل أن يتبعوه. استخدم هذه الهيئة كلما كان تفعيل عنصر التحكم لا يفعل إلا نقل الشخص إلى مكان آخر. لا تنفّذ التنقل من معالج النقر على زر عادي.

**Web**

```html
<tanrei-button href="/getting-started/">البدء</tanrei-button>

<!-- تبويب جديد: العلامة التي بعد التسمية تُرسم لك؛ و target-label هو ما يسمعه قارئ الشاشة. -->
<tanrei-button variant="outlined" href="https://example.com/source" target="new" target-label="يُفتح في تبويب جديد">
  عرض المصدر
</tanrei-button>

<!-- على الويب فقط: rel و download يذهبان إلى عنصر anchor. -->
<tanrei-button variant="tonal" href="/report.pdf" download="report.pdf"><span slot="icon">download</span>تنزيل التقرير</tanrei-button>

<script type="module">
  import '@tanrei/web/components/button';
  // الحدث click يصدر أولًا. الموجِّه من جهة العميل يلغيه ويتولى التنقل بنفسه.
  document.querySelector('[href="/getting-started/"]').addEventListener('click', (event) => {
    event.preventDefault();
    router.navigate('/getting-started/');
  });
</script>
```

**React**

```tsx
import { Button } from '@tanrei/react';

<Button href="/getting-started/">البدء</Button>

<Button variant="outlined" href="https://example.com/source" target="new" targetLabel="يُفتح في تبويب جديد">
  عرض المصدر
</Button>

// الحدث click يصدر أولًا. الموجِّه من جهة العميل يلغيه ويتولى التنقل بنفسه.
<Button
  href="/getting-started/"
  onClick={(event) => {
    event.preventDefault();
    router.navigate('/getting-started/');
  }}
>
  البدء
</Button>
```

- **المفاتيح.** المفتاح Enter يتبع الرابط. والمفتاح Space يفعل ما يفعله على أي رابط (على الويب يمرّر الصفحة)، لا ما يفعله على زر.
- **الخاصية `target`.** القيمة `same` (الافتراضية) تفتح الوجهة حيث يكون الشخص. والقيمة `new` تفتح تبويبًا جديدًا أو نافذة جديدة. عندئذ يعرض الزر الذي له تسمية رسم الأيقونة `open_in_new` بعد تسميته، ما لم تكن أيقونتك الخاصة في النهاية هناك، وتصير الخاصية `targetLabel` مطلوبة: لا يقدّم Tanrei كلمات من عنده، فتقول أنت «يُفتح في تبويب جديد» بلغة منتجك.
- **التعطيل.** زر الرابط المعطَّل لا يمكن اتباعه ولا نسخه ولا سحبه، ويُعرَّف للتقنيات المساعدة رابطًا معطَّلًا.
- **أزرار التبديل.** زر التبديل يحمل حالة، فلا يأخذ وجهة: تُتجاهل الخاصية `href` ما دامت الخاصية `toggle` مضبوطة.
- **على الويب فقط**، تُمرَّر الخاصيتان `rel` و `download` إلى عنصر anchor. وضبط `target` على القيمة `new` يضيف دائمًا `noopener` إلى `rel`.
- لا مظهر خاصًّا للرابط الذي سبقت زيارته. لون زر الرابط يقول مقدار توكيده وحالته، كما يفعل لون أي زر.

### في نموذج

على الويب يكون الزر واحدًا من عناصر التحكم في نموذجه، والخاصية `type` تقول ما تفعله الضغطة بالنموذج. أما المنصات الأخرى فليس فيها عنصر نموذج: هناك تستدعي دالة واحدة من ضغطة الزر ومن `action` الخاصة بالحقل.

| `type` | ما تفعله الضغطة |
| --- | --- |
| `button` (الافتراضية) | لا تفعل شيئًا بالنموذج. يستطيع زر في نموذج أن يفتح منتقيًا أو يضيف صفًّا من دون أن يرسل شيئًا. |
| `submit` | تطلب من النموذج أن يُرسَل، كما يفعل زر الإرسال الخاص بالمتصفح: الحدث `click` أولًا، ثم تحقق النموذج، ثم حدثه `submit`. |
| `reset` | تعيد كل عنصر تحكم في النموذج إلى قيمته الأولى، بعد الحدث `click` وحدث `reset` الخاص بالنموذج. |

**Web**

```html
<form id="profile">
  <tanrei-text-field label="الاسم" name="name" required></tanrei-text-field>
  <tanrei-button type="reset" variant="text">مسح</tanrei-button>
  <tanrei-button type="submit" variant="outlined" name="intent" value="draft">حفظ المسودة</tanrei-button>
  <tanrei-button type="submit" name="intent" value="publish">نشر</tanrei-button>
</form>

<!-- خارج النموذج: الخاصية form تسمّيه بمعرّفه (id). -->
<tanrei-button type="submit" form="profile" name="intent" value="publish">نشر</tanrei-button>

<script type="module">
  import '@tanrei/web/components/button';
  import '@tanrei/web/components/text-field';
  document.querySelector('#profile').addEventListener('submit', (event) => {
    event.preventDefault();
    // اسم الزر المضغوط وقيمته في البيانات: "draft" أو "publish".
    const data = new FormData(event.target, event.submitter);
    save(data.get('name'), data.get('intent'));
  });
</script>
```

**React**

```tsx
import { Button, TextField } from '@tanrei/react';

<form
  id="profile"
  onSubmit={(event) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget, (event.nativeEvent as SubmitEvent).submitter);
    save(data.get('name'), data.get('intent'));
  }}
>
  <TextField label="الاسم" name="name" required />
  <Button type="reset" variant="text">مسح</Button>
  <Button type="submit" variant="outlined" name="intent" value="draft">حفظ المسودة</Button>
  <Button type="submit" name="intent" value="publish">نشر</Button>
</form>

// خارج النموذج: قيمة form هي معرّف النموذج (id).
<Button type="submit" form="profile" name="intent" value="publish">نشر</Button>
```

- **أي نموذج.** النموذج الذي يحيط بالزر، أو الذي تسمّيه خاصية HTML `form` بمعرّفه (id)، وبهذا يرسل زر في إجراءات مربع حوار أو في رأس صفحة نموذجًا لا يقع داخله. أما خاصية العنصر `form` فتعيد ذلك النموذج ولا يمكن ضبطها.
- **المفتاح Enter في حقل.** أول زر إرسال في النموذج هو زره الافتراضي، سواء أكان زرًّا من Tanrei أم زر المتصفح نفسه. والمفتاح Enter في حقل من حقول النموذج يضغطه: في حقل النص وحقل العدد وحقل اللون ومربع التحرير والسرد وحقل إدخال الرمز لمرة واحدة والبحث من Tanrei، وعلى خانة الاختيار وزر الاختيار ومفتاح التبديل من Tanrei، وفي حقول الإدخال ذات السطر الواحد الخاصة بالمتصفح. ويُصدر الزر الحدث `click` كما في أي ضغطة، وإلغاء ذلك الحدث يلغي الإرسال.
- **التعطيل.** زر الإرسال المعطَّل لا يرسل شيئًا، وما دام هو الزر الافتراضي للنموذج فهو يوقف المفتاح Enter أيضًا، فلا يُرسَل نموذج غير جاهز من وراء زره. والزر داخل `<fieldset disabled>` يتعطّل معه، أيًّا كانت قيمة `type`. أما زر الرابط داخل `<fieldset disabled>` فلا يتعطّل.
- **أي زر ضُغط.** لا تُرسَل `name` و `value` إلا حين يكون ذلك الزر هو الذي أرسل النموذج. اقرأهما في مستمع الحدث `submit` من `event.submitter.value` أو من `new FormData(form, event.submitter)`. والمُرسِل (submitter) `event.submitter` ليس عنصر Tanrei. إنه زر `<button>` أصلي يضعه العنصر في مكانه هو أثناء إصدار الحدث `submit`، فلا تقارن `event.submitter` بالعنصر.
- **طريقة إرسال النموذج** شأن النموذج وحده. الخصائص `formaction` و `formmethod` و `formenctype` و `formtarget` و `formnovalidate` لا تُقرأ. ولحفظ مسودة بلا تحقق، اضبط `noValidate` للنموذج في مستمع الحدث `click` لذلك الزر، فهو يعمل قبل التحقق.
- **الإرسال مرتين.** الزر لا يحمي من ضغطة ثانية. عطّله ما دام طلبك جاريًا، أو تجاهل الحدث `submit` الثاني.
- **زر التبديل وزر الرابط** يتجاهلان `type`: الضغطة التي تبدّل حالة أو تذهب إلى مكان لا ترسل نموذجًا. وقيمة `type` التي ليست واحدة من الثلاث تسلك مسلك `button`.

### اسم أطول من التسمية

حين تُرسم الكلمة نفسها على عدة أزرار، مثل «إزالة» في كل صف من قائمة، لا تقول الكلمة وحدها لمستخدم قارئ الشاشة إلى أي صف ينتمي الزر. أعطِ كل زر `label`: فتصير عندئذ هي الاسم المتاح للتقنيات المساعدة كاملًا، مكان النص المرسوم، ويُرسم الزر تمامًا كما كان.

| `label` | التسمية المرسومة | الاسم المتاح للتقنيات المساعدة |
| --- | --- | --- |
| معطاة | لا شيء (أيقونة وحدها) | `label` |
| فارغة | مرسومة | النص المرسوم |
| معطاة | مرسومة | `label`، كاملةً ووحدها |

- **يجب أن يحتوي الاسم على النص المرسوم.** من يستخدم الإدخال الصوتي ينطق الكلمة التي يراها على الزر. ابدأ الاسم بتلك الكلمة حيث تسمح لغتك: «إزالة: Inter» لزر رُسمت عليه «إزالة». ولا أثر لحالة الأحرف ولا للمسافات الزائدة.
- **تبلّغ بنيات التنقيح (debug)** عن `label` لا تحتوي على النص المرسوم. ويبقى الزر يستخدم الاسم الذي أعطيته. لا يرجع أبدًا إلى النص المرسوم ولا يضمّ الاثنين، لأن ترتيب العبارة شأن لغتك.
- **الوصف ليس اسمًا.** ما يفرّق الزر من جيرانه هو اسمه. أما ما يحدث حين يُضغط، مثل «يُفتح في تبويب جديد»، فوصف يُقرأ بعد الاسم والدور.

## المواصفات

- Category: Actions
- Level: base
- Listing: default
- Used in: Copy button, Chat composer
- Page: https://tanrei.chnotchy.com/components/button/
- Spec: https://github.com/chnotchy/tanrei/blob/main/specs/button.spec.json

### Availability

| Design system | Has it | Notes |
| --- | --- | --- |
| Tanrei (`tanrei`) | Yes | — |
| Cupertino (Apple HIG-inspired) (`cupertino`) | Yes | — |
| M3 Expressive-like (Material 3 Expressive-inspired) (`m3-expressive-like`) | Yes | — |
| M3-like (Material 3-inspired) (`m3-like`) | Yes | — |

### Platforms

- Web: the element `<tanrei-button>`, registered by importing `@tanrei/web` (or only its own module, `@tanrei/web/components/button`).
- React: `Button` from `@tanrei/react`.

| Platform | Name | Status |
| --- | --- | --- |
| Web | `tanrei-button` | lagging |
| SwiftUI | `TanreiButton` | lagging |
| Jetpack Compose | `TanreiButton` | lagging |
| Flutter | `TanreiButton` | lagging |
| React Native | `Button` | lagging |

Names a platform gives in its own idiom:

| Spec | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
| --- | --- | --- | --- | --- | --- |
| `destructive` (prop) | `destructive` | `role` | `destructive` | `isDestructiveAction` | `destructive` |
| `iconPosition` (prop) | `iconPosition` | `iconPosition` | `iconPosition` | `iconAlignment` | `iconPosition` |
| `toggle` (prop) | `toggle` | `isSelected` | `onCheckedChange` | `isSelected` | `toggle` |
| `selected` (prop) | `selected` | `isSelected` | `checked` | `isSelected` | `selected` |
| `disabled` (prop) | `disabled` | `disabled` | `enabled` | `onPressed` | `disabled` |
| `busyLabel` (prop) | `busyLabel` | `busyLabel` | `busyLabel` | `busySemanticLabel` | `busyLabel` |
| `href` (prop) | `href` | `destination` | `href` | `href` | `href` |
| `targetLabel` (prop) | `targetLabel` | `targetLabel` | `targetLabel` | `targetSemanticLabel` | `targetLabel` |
| `label` (prop) | `label` | `label` | `contentDescription` | `semanticLabel` | `accessibilityLabel` |
| `click` (event) | `click` | `action` | `onClick` | `onPressed` | `onPress` |
| `change` (event) | `change` | `isSelected` | `onCheckedChange` | `onPressed` | `onSelectedChange` |

Statuses: `planned` — In the inventory. Nothing shippable exists yet. `lagging` — An implementation of an earlier spec exists; it has not caught up with the current one. `alpha` — Implemented with tests. The API may still change. `beta` — Feature complete and at parity. Only fixes are expected. `stable` — Covered by semantic versioning. `deprecated` — Still shipped; scheduled for removal.

### Anatomy

- `container`: The shape that holds the content, defines the visual bounds and carries hover, focus and press. A pill with a label; with an icon alone, as wide as it is high (a circle in the default system's corner) or at the narrow or the wide width of its size.
- `label` (optional): Text that names the action.
- `icon` (optional): A glyph of the system's icon set, at the leading or the trailing side of the label, or alone. Outlined when unselected, filled when selected.
- `outline` (optional): The border of the outlined variant.
- `focus-ring`: Gap-then-ring indicator shown for keyboard focus.

### Props

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `variant` | `enum`: `filled`, `tonal`, `elevated`, `outlined`, `text` | `"filled"` | `variant` | Emphasis level; whether the action deletes or discards something is destructive, which every variant takes. Use one filled button per view for the main action; an icon-only text button is the quiet icon action of a toolbar or a row. A design system may add values. Default filled; a Button placed as a dialog's or a side sheet's action without a variant takes that host's (DLG-20, SSH-17), and a side sheet's close button takes comp.side-sheet.close-button.variant (SSH-5). |
| `destructive` | `boolean` | `false` | `destructive` | Marks an action that deletes or discards something a person may not get back (Delete, Discard, Remove member): the button keeps its variant's emphasis and takes the error role's color in it — filled in the error color, tonal in its tonal container, and the error color for the label of an elevated, outlined or text button and the edge of an outlined one (BTN-40). Use it for that kind of action alone, never for emphasis. It adds a color, not a meaning: the label still says what the button does (BTN-42). A toggle has no destructive look: ignored while toggle is true (BTN-41). A link deletes nothing: ignored while href is not empty (BTN-43). |
| `size` | `enum`: `small`, `medium`, `large` | `"medium"` | `size` | Visual height (32 / 40 / 56), which is also an icon-only button's width at its default width. The hit area always meets the minimum target. A design system may add values. |
| `width` | `enum`: `default`, `narrow`, `wide` | `"default"` | `width` | How wide an icon-only button is. default: as wide as it is high (32 / 40 / 56). narrow: its glyph with a small inset on each side (28 / 32 / 32), for a tight row of icon actions. wide: 40 / 52 / 72, for the one action of a row that should weigh more. Ignored when the button has a label, and while fullWidth is true. The hit area meets the minimum target at every width. |
| `fullWidth` | `boolean` | `false` | `full-width` | Makes the button take the whole width its container gives it, its content centred, at the height of its size (BTN-39): a form's only action on a narrow screen, the action at the foot of a card or a sheet. Ignored while a host lays the button out (a button group, a toolbar, or the actions of a dialog, a side sheet, a popover, a callout or an empty state), which gives it a width of its own (a button group's fullWidth for a row of them). |
| `iconPosition` | `enum`: `leading`, `trailing` | `"leading"` | `icon-position` | Which side of the label the icon sits on, in the reading direction. Ignored when the button has no label or no icon. |
| `toggle` | `boolean` | `false` | `toggle` | Makes the button a two-state control: activation switches selected, and assistive technology announces it as pressed or not pressed. A toggle takes no destination: href is ignored while toggle is true. |
| `selected` | `boolean` | `false` | `selected` | Whether a toggle is on. Ignored unless toggle is true. |
| `disabled` | `boolean` | `false` | `disabled` | Prevents interaction and removes the button from the focus order — unless focusableWhenDisabled keeps it there (BTN-38). A disabled button with a destination is not a link that can be followed (BTN-25); on the web a disabled submit button sends nothing, and still stops Enter from sending its form where it is the form's default button (BTN-32). |
| `focusableWhenDisabled` | `boolean` | `false` | `focusable-when-disabled` | Keeps a disabled button in the focus order: it is drawn and exposed as disabled and refuses every activation, but a keyboard and a screen reader can still reach it, so that a person can learn why it does nothing (a tooltip, a description). For an action a person can make available — a send button while the message is empty. Ignored unless disabled is true (BTN-38). |
| `busy` | `boolean` | `false` | `busy` | The action it started is under way: the button keeps its focus, its place in the focus order and its size, draws the loading indicator's arc in place of its label and icon, refuses every activation, and tells assistive technology through busyLabel (BTN-36, BTN-37). For a save or a send that takes a moment; it is not disabled, so focus stays where the person left it. |
| `busyLabel` | `string` | `""` | `busy-label` | What assistive technology is told while the button is busy ("Saving"): announced once when it becomes busy and read after its name while it is. Tanrei ships no words of its own. Required while busy is true; ignored otherwise. |
| `href` | `string` | `""` | `href` | The destination: an address as the platform's links take one (a URL; on the web a path or a fragment as well). Not empty, it makes the button a link and not a button: the link role, activated by pointer, touch or Enter and not by Space, opened by the platform as its own links are, and drawn exactly as the button it would otherwise be (BTN-22, BTN-23). Ignored while toggle is true: a control that holds a state is not a destination (BTN-26). |
| `target` | `enum`: `same`, `new` | `"same"` | `target` | Where the destination opens. same: where the person is — the same tab on the web, the app's own handling of the address elsewhere. new: a new context — a new tab or window on the web, outside the current screen elsewhere — which a labelled button marks with the glyph open_in_new after its label and says through targetLabel (BTN-24). Ignored without href. |
| `targetLabel` | `string` | `""` | `target-label` | What assistive technology reads after the link's name while target is new ("opens in a new tab"). Tanrei ships no words of its own, so the product gives them in its own language, as it gives a chip its removeLabel. Required while target is new; ignored otherwise. |
| `label` | `string` | `""` | `label` | The accessible name, where it is more than what the button draws. Required for an icon-only button: an icon alone is not a name. Optional for a button that draws a label: left empty, the drawn text is the name; given, it is the whole name in place of the drawn text, which stays drawn — for an action repeated in every row (drawn "Reset", named "Reset: Spacing") — and it must contain the drawn text, so that a person who says what they see is heard (BTN-16). It is about this one button, so it is the instance's alone and reads no word of the product's. |

### Events

| Event | Description |
| --- | --- |
| `click` | The button was activated by pointer, touch, keyboard or assistive technology. A link button emits it before its destination opens; a product that takes the navigation itself (a client-side router) cancels it (BTN-23). |
| `change` | A toggle was activated; carries the new value of selected. |

### Slots

| Slot | Description |
| --- | --- |
| `default` | Label content: the text that names the action. Leave it empty for an icon-only button. |
| `icon` | The icon: a glyph of the system's icon set, drawn through the platform's icon helper. With a label it sits at the side iconPosition names; without one it is the button's only content. |

### States

`enabled`, `disabled`, `hovered`, `focused`, `pressed`, `selected`, `loading`

### Accessibility

- Role: `button (link while href is set)`
- Labeling: A button that draws a label is named by that text, unless label is given: then label is the whole name, in place of the drawn text, and it must contain the drawn text (BTN-16). An icon-only button is named by label, which is required; its glyph is decorative to assistive technology. A tooltip may show the same text to sighted pointer users, but it never replaces label. A link button is named in the same way; its name should say where it goes. While target is new, targetLabel is its description ("opens in a new tab"), and the mark is decorative. A description is read after the name and is never the name: what tells one button from its neighbours belongs in the name, and what happens next belongs in the description.
- Minimum target size: yes

| Keys | Action |
| --- | --- |
| Tab | Moves focus to the button — also to a busy one, and to a disabled one while focusableWhenDisabled is true. |
| Enter or Space | Activates the button; a toggle switches selected. A link button is activated by Enter alone: Space does what it does on any link (on the web it scrolls the page). |
| Enter in a field of the same form | On the web, presses the form's default button — its first submit button — where that is a Button of type submit, and so submits the form; nothing while that button is disabled (BTN-30). Enter in a Tanrei field, checkbox, radio button or switch does the same where the default button is the platform's own. |

- A toggle is a button with an on/off state (aria-pressed on the web, the selected trait on Apple platforms, a toggleable state on Android), not a checkbox or a switch; its name does not change with its state.
- Text must remain readable at 200% text size: the container grows, the label never truncates by default.
- Container heights are minimums: at larger text sizes the container grows in both axes. An icon-only button's glyph is not text, at any of its three widths: where sizes are points or dp (SwiftUI, Compose, Flutter, React Native), text size leaves its circle and its glyph as they are; on the web every Tanrei size follows the root font size (rem), so at 200% text size the circle and its glyph grow together and stay round and whole (BTN-12). Either way its label is still announced.
- The icon's side follows the reading direction: leading is the start (left in left-to-right, right in right-to-left). A glyph whose name the system's icon set marks as mirrored (arrow_forward, chevron_right, reply) is mirrored in a right-to-left layout by the icon helper, not by the button, and the product can force or forbid that for one use (BTN-19).
- How a state looks is a value decision of the active design system (a state-layer mix in Tanrei, possibly explicit colors, a dim, or no hover at all elsewhere). Components read the state's tokens and never compute the effect themselves, with one exception: on a tone a host gives it (BTN-45), whose color is known only where the button is drawn, the button lays the tone over its container at the system's state-layer opacities, as a carousel lays its state layer over a product's image. A system that draws no state layer sets those opacities to 0.
- A narrow icon-only button is drawn narrower than the minimum target; its hit area is not (BTN-21). Leave room for the target beside it, or put narrow buttons in a toolbar or a group that spaces them.
- The outline of an outlined button is the hairline of the default system (the divider's color), which is far below 3:1 against the surface. That is within WCAG 1.4.11: the edge is not what identifies the control. Its label is text held to 4.5:1 and its icon to the same ratio, a toggle that is on is shown by its fill, and focus by the focus ring. A product whose outlined buttons must show a 3:1 edge sets comp.button.outlined.outline.color to sys.color.outline in a layer.
- A button with a destination is a link: the link role, the platform's own link, Enter and not Space. It is not a button that navigates from a click handler: on the web such a button has no address to copy, cannot be opened in a new tab and is announced as something that acts on the page. The look does not say which of the two it is, and need not: sighted people tell a destination from an action by its label, as they do in a toolbar.
- Tanrei draws no visited state for a link button: it is a control whose color says its emphasis and its state, and the web restricts what a visited link may change to colors a page cannot read back. Visited links are a matter for links in running text.
- A link that opens a new context says so twice: the open_in_new mark for people who see it, and targetLabel for people who do not (WCAG 1.1.1: the mark carries information, so it needs a text alternative; technique G201 advises warning before a new window). The words are the product's.
- An action repeated in every row of a list or a table is drawn with one word and named for its row: label is the whole name ("Reset: Spacing"), opening with the drawn word where the language allows. Six buttons that are all named "Reset" cannot be told apart in the list of controls a screen reader offers, and a description does not appear there. The name must contain the drawn text (WCAG 2.5.3, label in name): a person who uses speech input says the word on the button. A debug build reports a name that does not; it never repairs one, because joining two texts into a phrase is a decision about a language, and the words are the product's.
- On the web a Button of type submit or reset is its form's button as the platform's own is: the form validates, fires its submit or reset event, and the pressed button's name and value are sent with it. Enter in a field presses the form's first submit button, and a disabled one stops it. The default type is button, not the platform's submit: a button put in a form for another purpose must never send it by accident (BTN-27, BTN-33).
- A button can keep a form from being sent twice: while busy it is shown and focusable but refuses every press, emits no click and therefore submits nothing (BTN-27 follows click), and it stops Enter in a field as a disabled default button does (BTN-37, BTN-32). Prefer it to disabling the button while the first submission is on its way: a disabled button drops the person's focus, and says nothing of why.
- Where script does not run, or before the element is defined, a web Button is its text and nothing else, and submits nothing; so is every Tanrei field, so a form made of them cannot be filled either. A form that must be sent without script is made of the platform's own controls, with the platform's own submit button.
- The native platforms have no form element, and the Button has no type there (BTN-34): which button the Return key presses is the app's to say, in the platform's idiom.
- A disabled button leaves the focus order, so a keyboard or screen-reader user may never learn it is there or why it does nothing. Where a person can make the action available (fill in the message, choose a file), keep it reachable with focusableWhenDisabled and say why in a tooltip or a description; where the action simply does not apply, leave it out of the focus order (BTN-38).
- destructive adds a color, never a meaning (WCAG 1.4.1, use of color): the error color tells people who see it that the action deletes or discards something, and nothing tells a screen reader user so but the label. So the label names the action and what it acts on ("Delete project", not "OK"), and a dialog that confirms it says what will be lost in its headline and supporting text. The web has no role or state for a destructive action, so none is invented; SwiftUI's destructive role is given where it exists, and the platform decides what it does with it.
- Disabled, a destructive button looks like any disabled button of its variant: the error color is a warning about what a press would do, and a button that cannot be pressed does nothing.
- A tone keeps a button readable on a colored surface its host draws (a status callout, an inverse snackbar): the button's content takes the surface's own content color, which the host holds to the contrast its own text needs, and its container is a translucent layer of that color rather than a color chosen against the page. The layer lowers the contrast between the content and what is under it a little (at 16 % in the default system, about a sixth of the way from the surface toward the content color): a host's content color must still hold 3:1 against that blend for an icon-only button (WCAG 1.4.11) and 4.5:1 for a labelled one (WCAG 1.4.3).

### Behaviour

Requirements in EARS form, each with its stable id.

- **BTN-1** WHEN the user activates the button by pointer, touch, Enter or Space — a link button (BTN-22) by pointer, touch or Enter — THE button SHALL emit click exactly once.
- **BTN-2** WHILE disabled is true THE button SHALL NOT emit click or change, SHALL NOT be focusable and SHALL expose the disabled state to assistive technology.
- **BTN-3** WHILE hovered, keyboard-focused or pressed THE button SHALL render its container, label, icon, outline, elevation and opacity from the component tokens of the highest-precedence active state, with precedence pressed over focused over hovered; focus gained by pointer or touch SHALL NOT select the focused state.
- **BTN-4** WHEN the button receives keyboard focus THE button SHALL show the focus ring without animation; focus gained by pointer or touch SHALL NOT show it.
- **BTN-5** WHERE the visual size is below the minimum target THE button SHALL extend its hit area on both axes to sys.target.touch for touch input and sys.target.pointer for fine pointers, without changing its visual size; a platform that cannot tell the input type SHALL use sys.target.touch.
- **BTN-6** WHILE disabled is true THE button SHALL render its container, label, icon, outline, elevation and opacity from the disabled component tokens of its variant and, for a toggle, of its selection, and, while destructive is true, of its destructive parts (BTN-40).
- **BTN-7** WHEN variant or size names a value that the active design system does not have THE button SHALL render with the default value's tokens, and in debug builds report the unknown value.
- **BTN-8** WHEN the button is pressed by pointer, touch, Enter or Space THE button SHALL show the pressed state without delay and SHALL keep it visible for at least sys.motion.duration.press-hold, even when the press ends sooner.
- **BTN-9** WHILE pressed THE button SHALL answer as sys.behavior.pressed-feedback names: overlay applies the pressed tokens to the whole container at once; ripple grows the pressed state layer from the point of contact (from the center for keyboard activation); dim applies the pressed opacity to the whole control and draws no state layer; none draws no answer to the press. Under ripple and dim only the container color stays with the state under the press; label, icon, outline, elevation, opacity and shape still come from the pressed tokens. Under none every part stays with the state under the press. The ripple grows over comp.button.motion.feedback-duration with motion.state-easing, fades over the motion.state-duration of its size with motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced.
- **BTN-10** THE button SHALL ease its container corners by sys.shape.corner-smoothing (0 is a quarter circle, 0.6 the iOS continuous corner; a platform with only the OS curve draws it for any amount above 0) and show its keyboard focus as sys.behavior.focus-style names (ring or glow); a platform that cannot draw a choice SHALL fall back to the first value and state that in its README. WHILE the button is an item of a connected button group THE button SHALL draw its corners and minimum width and height as the group sets them, the padding the group adds at a rounded outer end, and neither outline nor elevation ring along an edge the group joins to a neighbour under a divider, and, from a dialog, a side sheet or an image preview, its variant while none is set (DLG-20, SSH-17, IMP-8) and a host's close-button variant — the popover's, the side sheet's, the bottom sheet's, the callout's and the snackbar's close button, each an icon-only Button of the small size (POP-13, SSH-5, BSH-29, CLO-10, SNK-34; the image preview's bar names one variant for its close and zoom controls, IMP-8), and, from any component that draws it on a colored surface of its own, a tone (BTN-45), through an internal channel that is not part of its public surface (BGR-2, BGR-3, BGR-16, BGR-22, BGR-23).
- **BTN-11** WHILE the button has both a label and an icon THE button SHALL draw the icon at the side iconPosition names in the reading direction (leading at the start, trailing at the end, both mirrored in right-to-left layouts) and the label comp.button.container.gap.medium from it at the medium size (and the gap of its own size otherwise), with comp.button.container.padding-inline-start-with-icon.medium on the icon's side for a leading icon and comp.button.container.padding-inline-end-with-icon.medium for a trailing one, and the plain padding (comp.button.container.padding-inline-start.medium, comp.button.container.padding-inline-end.medium) on the other side, each of its own size; WHILE it has a label and no icon it SHALL use the plain padding on both sides and ignore iconPosition.
- **BTN-12** WHILE the button has an icon and no label THE button SHALL draw its container at the height of its size — comp.button.container.height.medium at the medium size, and the height of its own size otherwise — and at the width its width names: as wide as it is high WHILE width is default, as wide as its glyph's box (comp.button.icon.size-without-label.medium) with comp.button.container.padding-inline-narrow.medium on each side of it WHILE width is narrow, and comp.button.container.width-wide.medium wide WHILE width is wide (each of its own size), with the glyph centred, and SHALL round it by comp.button.container.shape, capped at half its shorter side, so that the default system's sys.shape.corner.full draws a circle at the default width and a capsule at the other two, and a smaller corner a rounded square or rectangle. The narrow width SHALL be the glyph and that inset twice, and nothing more: the button has no narrow width of its own to disagree with them, so a narrow button follows its glyph wherever a design system re-sizes it, and its container, state layer, ripple and outline are a capsule of that width. In the default system the inset is sys.space.space50 at every size, the least room that keeps the glyph clear of the capsule's edge; a design system that wants its narrow buttons to differ more by size gives a size a larger inset. It SHALL NOT draw an icon-only button at any other width, except to the minimum width a connected button group sets and the padding that group adds at an outer end (BTN-10). WHERE text size scales the glyph (every size follows the root font size on the web) the container SHALL grow with it and keep the proportion of its width to its height.
- **BTN-13** THE button SHALL draw its icon at the icon box of its size — comp.button.icon.size.medium beside a label and comp.button.icon.size-without-label.medium alone at the medium size, and so on — with the family and fill of its selection (sys.icon.family.selected and sys.icon.fill.selected only when toggle and selected are both true; otherwise sys.icon.family.unselected and sys.icon.fill.unselected), sys.icon.weight, and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max; beside a label the icon SHALL take the label color of the button's look, and alone the icon color of its look (comp.button.text.icon.color for a plain text button, comp.button.text.selected.icon.color for a text toggle that is on, and so on).
- **BTN-14** WHEN toggle is true and the user activates the button THE button SHALL emit click and then change with the opposite of selected, in that order (as a native checkbox does, and as CHP-11 says for a filter chip), and SHALL expose selected to assistive technology as pressed or not pressed; WHILE toggle is false THE button SHALL NOT expose a pressed state.
- **BTN-15** WHILE toggle is true THE button SHALL render its container, label, icon and outline from the selected parts of its variant when selected is true (comp.button.filled.selected.container.color, and so on) and from the unselected parts when it is false (comp.button.filled.unselected.container.color, and so on), and its elevation from its variant's own comp.button.filled.container.elevation (and so on); WHILE toggle is false it SHALL render from the variant's own parts.
- **BTN-16** WHILE the label prop is not empty THE button SHALL expose it as its accessible name, whole and alone, whether or not it draws a label; WHILE the label prop is empty and the button draws a label THE button SHALL expose the drawn label's text as its accessible name; WHILE the label prop is empty and the product gives the button a name of its own on the element — aria-label, or aria-labelledby naming an element — THE button SHALL expose that name as its accessible name, over the drawn label's text where it draws one, because the platform's text alternative computation puts an author's name over the element's content; WHILE the label prop is not empty THE label prop SHALL be the name, over the product's. WHILE it has an icon and no drawn label THE button SHALL keep the glyph from assistive technology, and WHILE the label prop is empty as well and the product gives it no name of its own SHALL still render, and in debug builds report that it has no accessible name. WHILE the button draws a label and the label prop is not empty THE button SHALL draw the label exactly as it would without the prop, SHALL NOT expose the drawn text a second time beside the name, and in debug builds SHALL report a label prop that does not contain the drawn text — the two compared with white space at their ends dropped, runs of white space read as one space and letters compared without case — because a person who speaks the words they see must be heard by the control that shows them. That report SHALL be all the button does about it: it SHALL still expose the label prop, whole and alone, as the name, SHALL NOT fall back to the drawn text and SHALL NOT add the drawn text to the name, in a debug build as in any other. THE button SHALL NOT make a name of the two itself, by joining them or in any other way: the order of a phrase is the language's, and the whole name is the product's to give. A description — the platform's own, or targetLabel (BTN-24) — SHALL be read after the name and the role and SHALL NOT be exposed as a name or as part of one.
- **BTN-17** IF the button has neither a label nor an icon THE button SHALL still render its container at the height of its size and as wide as it is high, and in debug builds report that it has no content.
- **BTN-18** THE button SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the button is shown THE button SHALL draw itself for the new direction at once, without a motion of its own, keeping its selection and its focus.
- **BTN-19** WHERE the layout direction is right-to-left THE button SHALL draw the glyph of its icon slot mirrored left to right exactly when the system's icon set marks that glyph's name as mirrored or the product forces mirroring for that use, and unmirrored when the product forbids it: the icon helper mirrors the glyph inside its own box, about the box's vertical centre line, changing neither its place, size, color and fill nor the order BTN-11 gives. THE button SHALL NOT mirror a glyph itself, and SHALL NOT mirror content the product gives as an image or a view of its own rather than by a glyph's name.
- **BTN-20** THE button SHALL lay out its label as text of its own direction, isolated from the text around it: each takes the base direction of its first strong character, or the layout direction where it has none, so that a left-to-right word, a number with its sign or unit, a telephone number and an address keep their own order in a right-to-left layout and reorder nothing beside them; THE button SHALL still place the label and align its lines by the layout direction, not by the text's own.
- **BTN-21** WHILE the button has a label THE button SHALL ignore width: a labelled button is as wide as its content and its paddings (BTN-11). WHEN width names a value the button does not define THE button SHALL render at the default width, and in debug builds report the unknown value. THE button SHALL meet the minimum target of BTN-5 at every width by extending its hit area: a narrow button SHALL NOT be drawn wider to reach it.
- **BTN-22** WHILE href is not empty and toggle is false THE button SHALL be a link and not a button: it SHALL expose the link role, named as BTN-16 names a button, with no pressed state; SHALL be the platform's own link to href (the web's anchor element; SwiftUI's Link; on Compose, Flutter and React Native an element with the platform's link semantics whose address is opened by the platform's opener), so that whatever the platform gives a link — on the web its context menu, opening in a new tab or window, copying and dragging its address, and the address shown before it is followed — is the platform's, and not the button's to draw or to take over; and SHALL draw its container, label, icon, outline, elevation and opacity in every state from the same tokens as a button of its variant, size and width that has no destination, with no look of its own for a link and none for a destination already visited.
- **BTN-23** WHEN the user activates a link button by pointer, touch or Enter THE button SHALL emit click exactly once and then open href where target names, unless the product cancelled that click, in which case it SHALL open nothing: the product's own router has taken the navigation. It SHALL show the pressed state as BTN-8 and BTN-9 say and SHALL NOT hold the navigation back for sys.motion.duration.press-hold. THE button SHALL NOT activate a link button on Space, which on the web scrolls the page as it does on any link, and SHALL NOT show the pressed state for it; WHERE the platform gives its links other keys or gestures (on the web a modified or a middle click that opens a new tab, on Apple platforms a long press that previews) THE button SHALL leave them to the platform.
- **BTN-24** WHILE the button is a link and target is new THE button SHALL open href in a new context — on the web a new tab or window that is given no handle to the page that opened it; on the other platforms outside the screen the person is on, as the platform's opener decides — and, WHILE it has a label and no icon at its trailing side, SHALL draw the glyph open_in_new of the system's icon set after the label as BTN-11 draws a trailing icon: comp.button.container.gap.medium from the label, at comp.button.icon.size.medium, with comp.button.container.padding-inline-end-with-icon.medium between it and the end edge at the medium size (and each of its own size otherwise), a leading icon keeping its own side as BTN-11 says; in the label color of its look and state; through the icon helper, which mirrors it in a right-to-left layout because the icon set marks its name; and hidden from assistive technology. WHILE the button holds the product's own icon at its trailing side, or has no label, THE button SHALL draw no mark of its own: the product has chosen what that place shows. Whether a mark is drawn or not THE button SHALL expose targetLabel as its description, read after its name and role, and WHILE targetLabel is empty SHALL still render and in debug builds report that a link that opens a new context does not say so. WHILE target is same THE button SHALL draw no mark and SHALL ignore targetLabel.
- **BTN-25** WHILE href is not empty and disabled is true THE button SHALL NOT be a link that can be followed: it SHALL render as BTN-6 says, the mark of BTN-24 in the disabled label color, SHALL expose the link role with the disabled state and no destination, SHALL NOT be focusable, SHALL NOT emit click, and SHALL open nothing by any means the platform gives a link (on the web the anchor carries no href, so there is no context menu entry, no drag and no new tab for it).
- **BTN-26** WHILE toggle is true THE button SHALL ignore href, target and targetLabel and stay the toggle BTN-14 describes, and in debug builds SHALL report that a toggle cannot be a link: a control that holds a state is not a destination. WHEN target names a value the button does not define THE button SHALL behave as same, and in debug builds report the unknown value. IF href is only white space, or names a script to run rather than a place to go (the web's javascript: scheme), or cannot be read as an address by the platform, THE button SHALL be a button without a destination, and in the last two cases report it in debug builds.
- **BTN-27** WHERE the platform has a form element (the web) and WHILE type is submit, WHEN the user activates the button by any means of BTN-1 and the product has not cancelled its click THE button SHALL ask its form (BTN-29) to submit as the platform's own submit button does: after click has been emitted, exactly once for one activation, with the form's own validation run first and its failure shown by the platform and by the fields as they show it, then the form's submit event, which the product may cancel, and with the pressed button named in what is sent (BTN-31). It SHALL show the pressed state as BTN-8 and BTN-9 say and SHALL NOT hold the submission back for sys.motion.duration.press-hold. WHILE type is button, which is the default, THE button SHALL do nothing to any form, whatever it stands in.
- **BTN-28** WHERE the platform has a form element (the web) and WHILE type is reset, WHEN the user activates the button and the product has not cancelled its click THE button SHALL ask its form (BTN-29) to reset as the platform's own reset button does: after click has been emitted, the form's reset event, which the product may cancel, and then every control of the form back at its initial value; it SHALL validate nothing, submit nothing and add nothing to any later submission.
- **BTN-29** THE button's form SHALL be the one the platform gives a control at its place: the nearest form among its ancestors, or, WHILE the web's form attribute is not empty, the form of that id in the same document, and then not an ancestor; it SHALL be read when the button is activated, so that a button moved into a form, or a form attribute written later, is followed. WHERE the platform lists a form's controls (the web's form.elements) THE button SHALL be listed among them at its place in document order, whatever its type and also WHILE type is button — a link button too, since the element is one kind of control — and being listed SHALL send nothing: a Button adds to a submission only what BTN-31 says. IF type is submit or reset and the button has no form WHEN it is activated THE button SHALL emit click and do nothing more, and in debug builds report that a submit or a reset button has no form.
- **BTN-30** WHERE the platform has a form element (the web) THE default button of a form SHALL be the first submit button among the form's controls in document order, a Button whose type is submit counted exactly as the platform's own submit button is. WHEN the person presses Enter in a field of the form that submits it implicitly — the platform's own single-line inputs; Tanrei's text field (TXI-3), number field (NMI-20), color field (CLI-19), combobox (CMB-28), one-time-code input (OTC-13) and search (SRC-13), once their own event for that key has not been cancelled; and Tanrei's checkbox, radio button and switch, which emit nothing for the key (CHK-1, RAD-1, SWT-1) — and the default button is a Button, THE button SHALL act as if it had been activated: click emitted exactly once, and then, unless the product cancelled it, the submission of BTN-27 with that button named in it (BTN-31); it SHALL NOT show the pressed state for a key pressed elsewhere and SHALL NOT take focus. WHILE the default button is disabled — a Button or the platform's own — THE form SHALL NOT be submitted by Enter in any of those controls, as the platform's own disabled default button prevents it from the platform's own inputs. WHILE the default button is the platform's own, Enter in a Tanrei control SHALL press it as Enter in the platform's own input does: its click, then the submission with its name and value. WHILE the form has no submit button of either kind THE button has no part in it: the field and the platform do as they do without one. THE button SHALL draw a form's default button no differently from any other: its emphasis is its variant's.
- **BTN-31** WHEN a Button submits its form (BTN-27, BTN-30) THE submission SHALL carry the pair of that button's name and value — once, at most, and only WHILE its name is not empty; an empty value is sent as an empty value — and SHALL carry nothing of any other Button; WHEN the form is submitted by other means (another button, a script's own request) it SHALL carry nothing of this one. THE submit event SHALL name as its submitter a submit button of the platform's own that holds the same name and value and lasts no longer than the event's dispatch, because the platform lets only its own buttons be a submitter: a product reads which button was pressed from that name and value, or from click, and SHALL NOT expect the submitter to be the Tanrei element. THE button SHALL NOT override where or how its form is sent: the address, the method, the encoding, the target and whether the form is validated are the form's alone.
- **BTN-32** WHILE disabled is true THE button SHALL submit and reset nothing by any means — a press, a key, assistive technology or a script's click — SHALL add nothing to a submission, and SHALL still be counted as its form's default button where it is the first submit button (BTN-30), so that a form whose button is disabled is not sent by Enter behind it. WHERE the platform has a form element (the web), WHILE the button is not a link and stands in a group of controls the page has disabled as a whole (a disabled fieldset) THE button SHALL be disabled exactly as BTN-2 and BTN-6 say, whatever its type, and SHALL be enabled again at once when the group is; a link button SHALL NOT be disabled by it, as no link of the platform is.
- **BTN-33** WHILE toggle is true THE button SHALL ignore a type of submit or reset and stay the toggle BTN-14 describes, and in debug builds SHALL report that a toggle cannot submit or reset a form: a press that switches a state is not one that sends it. WHILE the button is a link (BTN-22) THE button SHALL ignore a type of submit or reset and stay the link, and in debug builds SHALL report that a link cannot submit or reset a form: it goes somewhere, and what a form sends on the way is the form's own action. WHEN type names a value the button does not define THE button SHALL behave as button and submit nothing — where the platform's own button would submit for an unknown type — and in debug builds report the unknown value. WHILE type is button THE button SHALL ignore the web's form, name and value without a report: they do no harm on a button that sends nothing.
- **BTN-34** WHERE the platform has no form element (SwiftUI, Compose, Flutter, React Native) THE button SHALL have no type, no form and no name and value to send: there a product calls one function of its own from the button's click and from a field's action (TXI-3), and nothing of Tanrei's joins the two or decides which button a key in a field presses.
- **BTN-36** WHILE busy is true THE button SHALL draw its container, outline, elevation and opacity from its enabled tokens (of its variant and, for a toggle, of its selection, or of its destructive parts, BTN-40), SHALL keep its label and icon in place without drawing them so that its width and height do not change, and SHALL draw at the centre of the container the loading indicator's arc (LDG-1; under reduced motion LDG-4's whole circle, its opacity easing once per cycle) in a box of the button's icon size (icon.size-without-label where it is icon-only), comp.button.busy.indicator.thickness thick, in the label color of its variant (the icon color where it is icon-only), turning once per comp.button.motion.busy-cycle-duration. It SHALL show neither the hovered nor the pressed state, and SHALL show the focused one. WHEN busy becomes false THE button SHALL draw its label and icon again at once, without a motion.
- **BTN-37** WHILE busy is true THE button SHALL keep its focus and its place in the focus order, SHALL NOT emit click or change, and SHALL submit, reset and open nothing by any means (a press, a key, assistive technology or a script's click); where it is its form's default button it SHALL stop Enter in a field of the form from submitting it, as a disabled default button does (BTN-30, BTN-32), so that a form is not sent twice. It SHALL expose to assistive technology that it cannot be activated now (on the web aria-disabled) and busyLabel as its description, read after its name, and WHEN busy becomes true SHALL have busyLabel announced once, politely, without moving focus; nothing SHALL be announced when it ends, because what the action did is the product's to say. WHILE busy is true and busyLabel is empty THE button SHALL still render and in debug builds report that a busy button does not say so.
- **BTN-38** WHILE disabled is true and focusableWhenDisabled is true THE button SHALL be drawn as BTN-6 says and refuse every activation as BTN-2, BTN-25 and BTN-32 say, but SHALL stay in the focus order, take focus from the keyboard and from a pointer, keep focus when it becomes disabled, show its focus indicator, and expose the disabled state to assistive technology while staying focusable (on the web aria-disabled, not the platform's disabled state), so that a person can reach it and learn why it does nothing. It SHALL show neither the hovered nor the pressed state. A link button so disabled is the disabled link of BTN-25, reachable by Tab. WHERE the platform disables a group of controls as a whole (a disabled fieldset on the web) THE button SHALL leave the focus order with the group, whatever focusableWhenDisabled says, as the platform's own controls do. WHILE focusableWhenDisabled is false, disabled keeps its meaning (BTN-2).
- **BTN-39** WHILE fullWidth is true and no host lays the button out THE button SHALL take the whole inline size its container gives it, and SHALL keep the height, the paddings, the corners, the outline and the minimum target of its size, variant and state, centring its content in that width — the label and the icon as one row, in the order and at the gap BTN-11 gives them, or an icon-only button's glyph, whatever width says. WHERE that width is narrower than the label THE button SHALL wrap the label, its lines centred, rather than truncate it or draw past its container. WHILE a host lays the button out (a button group, a toolbar, or the actions of a dialog, a side sheet, a popover, a callout or an empty state) THE button SHALL take the width that host gives it and ignore fullWidth: the host's own full width (BGR-17) is how a row of buttons shares one width.
- **BTN-40** WHILE destructive is true and toggle is false THE button SHALL render its container, label, icon and outline from the destructive parts of its variant (comp.button.filled.destructive.container.color, and so on for each variant), in each state from the destructive parts of that state, and its elevation, shape and opacity as it would without destructive; WHILE destructive is false it SHALL render from the variant's own parts. In every shipped system the destructive parts are the error color role at the variant's emphasis — filled: the error color with the on-error color on it; tonal: the error container with the on-error-container color on it; elevated, outlined and text: the variant's own container with the label and the icon in the error color, and an outlined button's edge in it too — and disabled they are those of the variant's plain disabled look, so that a disabled destructive button looks like any disabled button of its variant. An icon-only button takes destructive as a labelled button does; a link button ignores it (BTN-43).
- **BTN-41** WHILE toggle is true THE button SHALL ignore destructive and render as BTN-15 says, and in debug builds SHALL report that a toggle has no destructive look: a control that switches a state on and off does not delete anything by being pressed.
- **BTN-42** THE button SHALL NOT make color the only sign that its action is destructive: WHILE destructive is true it SHALL keep its role, its accessible name, its states and its keyboard behaviour exactly as they are without it, and SHALL expose nothing more to assistive technology on the web, where no role or state says it; WHERE the platform has a destructive role of its own (SwiftUI's ButtonRole.destructive) THE button SHALL be given that role. Its label, or its label prop on an icon-only button, says what it destroys.
- **BTN-43** WHILE the button is a link (BTN-22) THE button SHALL ignore destructive and render as the link button of its variant without it, in each state and on a tone a host gives it (BTN-45), and in debug builds SHALL report that a link only leads somewhere and does not itself delete anything: the action that deletes is the one at its destination, and that action carries destructive.
- **BTN-45** WHERE the component that places the button gives it a tone through the internal channel (BTN-10) — the content color of the colored surface it draws the button on (a status callout's, an inverse or status snackbar's), and optionally the color a focus indicator's outer stroke takes on that surface — THE button SHALL draw its plain look in that tone instead of its variant's own colors, at rest and in each state: its label, its icon and its state layer in the tone's color; its container, where its variant draws one at rest (filled, tonal, elevated, and a variant a layer adds), in the tone's color at comp.button.tone.container.opacity, and where its variant draws none (outlined, text) as its variant's own, an outlined button's edge taking the tone's color at that opacity instead; hovered, focused and pressed, that resting container with the tone's color laid over it at the system's state-layer opacity of the state (sys.state.hover-state-layer-opacity, sys.state.focus-state-layer-opacity, sys.state.pressed-state-layer-opacity), as the core rules lay a state over a resting color; disabled, the container (where its variant draws one) in the tone's color at sys.state.disabled-container-opacity and the label and icon at sys.state.disabled-content-opacity; and the outer stroke of its focus indicator in the tone's focus color, or its color where none is given. Its size, shape, paddings, elevation, opacity and answer to a press SHALL be its own, and a toggle and a destructive button SHALL keep their own looks (BTN-15, BTN-40). WHILE no tone is given THE button SHALL be drawn as without this clause. A tone is not part of the button's public surface: no prop, attribute or styling property gives one.
- **BTN-46** WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the host, by the attribute or by the ARIA property that reflects it, THE button SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on the element that carries its role — an id reference by the elements it names in the host's tree or a tree around it, an error message as part of the description while the button is invalid — except those the button owns (aria-pressed, aria-disabled), which it SHALL move and not expose. WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE button SHALL take it off that element. WHEN the product sets an aria- attribute the button owns THE button SHALL report it in debug builds.

### Tokens

As written in the token documents (DTCG): an alias in braces names the token it takes its value from, which each design system resolves. On the web each token is a CSS custom property: `--tanrei` and the name after `tanrei`, joined by hyphens (`tanrei.comp.button.…` → `--tanrei-comp-button-…`).

#### `tanrei.comp.button`

| Token | Type | Value |
| --- | --- | --- |
| `comp.button.container.shape` | `dimension` | `{tanrei.sys.shape.corner.full}` |
| `comp.button.container.height.small` | `dimension` | `32px` |
| `comp.button.container.height.medium` | `dimension` | `40px` |
| `comp.button.container.height.large` | `dimension` | `56px` |
| `comp.button.container.padding-inline-narrow.small` | `dimension` | `{tanrei.sys.space.space50}` |
| `comp.button.container.padding-inline-narrow.medium` | `dimension` | `{tanrei.sys.space.space50}` |
| `comp.button.container.padding-inline-narrow.large` | `dimension` | `{tanrei.sys.space.space50}` |
| `comp.button.container.width-wide.small` | `dimension` | `40px` |
| `comp.button.container.width-wide.medium` | `dimension` | `52px` |
| `comp.button.container.width-wide.large` | `dimension` | `72px` |
| `comp.button.container.gap.small` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.button.container.gap.medium` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.button.container.gap.large` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.button.container.padding-inline-start.small` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.button.container.padding-inline-start.medium` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.button.container.padding-inline-start.large` | `dimension` | `{tanrei.sys.space.space300}` |
| `comp.button.container.padding-inline-end.small` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.button.container.padding-inline-end.medium` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.button.container.padding-inline-end.large` | `dimension` | `{tanrei.sys.space.space300}` |
| `comp.button.container.padding-inline-start-with-icon.small` | `dimension` | `{tanrei.sys.space.space150}` |
| `comp.button.container.padding-inline-start-with-icon.medium` | `dimension` | `{tanrei.sys.space.space150}` |
| `comp.button.container.padding-inline-start-with-icon.large` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.button.container.padding-inline-end-with-icon.small` | `dimension` | `{tanrei.sys.space.space150}` |
| `comp.button.container.padding-inline-end-with-icon.medium` | `dimension` | `{tanrei.sys.space.space150}` |
| `comp.button.container.padding-inline-end-with-icon.large` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.button.container.padding-block.small` | `dimension` | `6px` |
| `comp.button.container.padding-block.medium` | `dimension` | `6px` |
| `comp.button.container.padding-block.large` | `dimension` | `6px` |
| `comp.button.label.typescale.small` | `typography` | `{tanrei.sys.typescale.label}` |
| `comp.button.label.typescale.medium` | `typography` | `{tanrei.sys.typescale.label}` |
| `comp.button.label.typescale.large` | `typography` | `{tanrei.sys.typescale.label}` |
| `comp.button.icon.size.small` | `dimension` | `{tanrei.sys.icon.small}` |
| `comp.button.icon.size.medium` | `dimension` | `{tanrei.sys.icon.medium}` |
| `comp.button.icon.size.large` | `dimension` | `{tanrei.sys.icon.medium}` |
| `comp.button.icon.size-without-label.small` | `dimension` | `{tanrei.comp.button.icon.size.small}` |
| `comp.button.icon.size-without-label.medium` | `dimension` | `{tanrei.comp.button.icon.size.medium}` |
| `comp.button.icon.size-without-label.large` | `dimension` | `{tanrei.comp.button.icon.size.large}` |
| `comp.button.outline.width.small` | `dimension` | `{tanrei.sys.border.hairline}` |
| `comp.button.outline.width.medium` | `dimension` | `{tanrei.sys.border.hairline}` |
| `comp.button.outline.width.large` | `dimension` | `{tanrei.sys.border.hairline}` |
| `comp.button.filled.container.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.button.filled.container.elevation` | `shadow` | `{tanrei.sys.elevation.level0}` |
| `comp.button.filled.label.color` | `color` | `{tanrei.sys.color.on-primary}` |
| `comp.button.filled.icon.color` | `color` | `{tanrei.comp.button.filled.label.color}` |
| `comp.button.filled.state-layer.color` | `color` | `{tanrei.comp.button.filled.label.color}` |
| `comp.button.filled.outline.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.button.filled.unselected.container.color` | `color` | `{tanrei.sys.color.surface-container-highest}` |
| `comp.button.filled.unselected.label.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.button.filled.unselected.icon.color` | `color` | `{tanrei.comp.button.filled.unselected.label.color}` |
| `comp.button.filled.unselected.outline.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.button.filled.unselected.state-layer.color` | `color` | `{tanrei.comp.button.filled.unselected.label.color}` |
| `comp.button.filled.selected.container.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.button.filled.selected.label.color` | `color` | `{tanrei.sys.color.on-primary}` |
| `comp.button.filled.selected.icon.color` | `color` | `{tanrei.comp.button.filled.selected.label.color}` |
| `comp.button.filled.selected.outline.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.button.filled.selected.state-layer.color` | `color` | `{tanrei.comp.button.filled.selected.label.color}` |
| `comp.button.filled.destructive.container.color` | `color` | `{tanrei.sys.color.error}` |
| `comp.button.filled.destructive.label.color` | `color` | `{tanrei.sys.color.on-error}` |
| `comp.button.filled.destructive.icon.color` | `color` | `{tanrei.comp.button.filled.destructive.label.color}` |
| `comp.button.filled.destructive.outline.color` | `color` | `{tanrei.comp.button.filled.outline.color}` |
| `comp.button.filled.destructive.state-layer.color` | `color` | `{tanrei.comp.button.filled.destructive.label.color}` |
| `comp.button.tonal.container.color` | `color` | `{tanrei.sys.color.secondary-container}` |
| `comp.button.tonal.container.elevation` | `shadow` | `{tanrei.sys.elevation.level0}` |
| `comp.button.tonal.label.color` | `color` | `{tanrei.sys.color.on-secondary-container}` |
| `comp.button.tonal.icon.color` | `color` | `{tanrei.comp.button.tonal.label.color}` |
| `comp.button.tonal.state-layer.color` | `color` | `{tanrei.comp.button.tonal.label.color}` |
| `comp.button.tonal.outline.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.button.tonal.unselected.container.color` | `color` | `{tanrei.sys.color.surface-container-highest}` |
| `comp.button.tonal.unselected.label.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.button.tonal.unselected.icon.color` | `color` | `{tanrei.comp.button.tonal.unselected.label.color}` |
| `comp.button.tonal.unselected.outline.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.button.tonal.unselected.state-layer.color` | `color` | `{tanrei.comp.button.tonal.unselected.label.color}` |
| `comp.button.tonal.selected.container.color` | `color` | `{tanrei.sys.color.secondary}` |
| `comp.button.tonal.selected.label.color` | `color` | `{tanrei.sys.color.on-secondary}` |
| `comp.button.tonal.selected.icon.color` | `color` | `{tanrei.comp.button.tonal.selected.label.color}` |
| `comp.button.tonal.selected.outline.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.button.tonal.selected.state-layer.color` | `color` | `{tanrei.comp.button.tonal.selected.label.color}` |
| `comp.button.tonal.destructive.container.color` | `color` | `{tanrei.sys.color.error-container}` |
| `comp.button.tonal.destructive.label.color` | `color` | `{tanrei.sys.color.on-error-container}` |
| `comp.button.tonal.destructive.icon.color` | `color` | `{tanrei.comp.button.tonal.destructive.label.color}` |
| `comp.button.tonal.destructive.outline.color` | `color` | `{tanrei.comp.button.tonal.outline.color}` |
| `comp.button.tonal.destructive.state-layer.color` | `color` | `{tanrei.comp.button.tonal.destructive.label.color}` |
| `comp.button.elevated.container.color` | `color` | `{tanrei.sys.color.surface-container-lowest}` |
| `comp.button.elevated.container.elevation` | `shadow` | `{tanrei.sys.elevation.level2}` |
| `comp.button.elevated.label.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.button.elevated.icon.color` | `color` | `{tanrei.comp.button.elevated.label.color}` |
| `comp.button.elevated.state-layer.color` | `color` | `{tanrei.comp.button.elevated.label.color}` |
| `comp.button.elevated.outline.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.button.elevated.unselected.container.color` | `color` | `{tanrei.sys.color.surface-container-lowest}` |
| `comp.button.elevated.unselected.label.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.button.elevated.unselected.icon.color` | `color` | `{tanrei.comp.button.elevated.unselected.label.color}` |
| `comp.button.elevated.unselected.outline.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.button.elevated.unselected.state-layer.color` | `color` | `{tanrei.comp.button.elevated.unselected.label.color}` |
| `comp.button.elevated.selected.container.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.button.elevated.selected.label.color` | `color` | `{tanrei.sys.color.on-primary}` |
| `comp.button.elevated.selected.icon.color` | `color` | `{tanrei.comp.button.elevated.selected.label.color}` |
| `comp.button.elevated.selected.outline.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.button.elevated.selected.state-layer.color` | `color` | `{tanrei.comp.button.elevated.selected.label.color}` |
| `comp.button.elevated.destructive.container.color` | `color` | `{tanrei.comp.button.elevated.container.color}` |
| `comp.button.elevated.destructive.label.color` | `color` | `{tanrei.sys.color.error}` |
| `comp.button.elevated.destructive.icon.color` | `color` | `{tanrei.comp.button.elevated.destructive.label.color}` |
| `comp.button.elevated.destructive.outline.color` | `color` | `{tanrei.comp.button.elevated.outline.color}` |
| `comp.button.elevated.destructive.state-layer.color` | `color` | `{tanrei.comp.button.elevated.destructive.label.color}` |
| `comp.button.outlined.container.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.button.outlined.container.elevation` | `shadow` | `{tanrei.sys.elevation.level0}` |
| `comp.button.outlined.label.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.button.outlined.icon.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.button.outlined.state-layer.color` | `color` | `{tanrei.comp.button.outlined.label.color}` |
| `comp.button.outlined.outline.color` | `color` | `{tanrei.sys.color.outline-variant}` |
| `comp.button.outlined.unselected.container.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.button.outlined.unselected.label.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.button.outlined.unselected.icon.color` | `color` | `{tanrei.comp.button.outlined.unselected.label.color}` |
| `comp.button.outlined.unselected.outline.color` | `color` | `{tanrei.sys.color.outline-variant}` |
| `comp.button.outlined.unselected.state-layer.color` | `color` | `{tanrei.comp.button.outlined.unselected.label.color}` |
| `comp.button.outlined.selected.container.color` | `color` | `{tanrei.sys.color.inverse-surface}` |
| `comp.button.outlined.selected.label.color` | `color` | `{tanrei.sys.color.inverse-on-surface}` |
| `comp.button.outlined.selected.icon.color` | `color` | `{tanrei.comp.button.outlined.selected.label.color}` |
| `comp.button.outlined.selected.outline.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.button.outlined.selected.state-layer.color` | `color` | `{tanrei.comp.button.outlined.selected.label.color}` |
| `comp.button.outlined.destructive.container.color` | `color` | `{tanrei.comp.button.outlined.container.color}` |
| `comp.button.outlined.destructive.label.color` | `color` | `{tanrei.sys.color.error}` |
| `comp.button.outlined.destructive.icon.color` | `color` | `{tanrei.comp.button.outlined.destructive.label.color}` |
| `comp.button.outlined.destructive.outline.color` | `color` | `{tanrei.sys.color.error}` |
| `comp.button.outlined.destructive.state-layer.color` | `color` | `{tanrei.comp.button.outlined.destructive.label.color}` |
| `comp.button.text.container.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.button.text.container.elevation` | `shadow` | `{tanrei.sys.elevation.level0}` |
| `comp.button.text.label.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.button.text.icon.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.button.text.state-layer.color` | `color` | `{tanrei.comp.button.text.label.color}` |
| `comp.button.text.outline.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.button.text.unselected.container.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.button.text.unselected.label.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.button.text.unselected.icon.color` | `color` | `{tanrei.comp.button.text.unselected.label.color}` |
| `comp.button.text.unselected.outline.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.button.text.unselected.state-layer.color` | `color` | `{tanrei.comp.button.text.unselected.label.color}` |
| `comp.button.text.selected.container.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.button.text.selected.label.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.button.text.selected.icon.color` | `color` | `{tanrei.comp.button.text.selected.label.color}` |
| `comp.button.text.selected.outline.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.button.text.selected.state-layer.color` | `color` | `{tanrei.comp.button.text.selected.label.color}` |
| `comp.button.text.destructive.container.color` | `color` | `{tanrei.comp.button.text.container.color}` |
| `comp.button.text.destructive.label.color` | `color` | `{tanrei.sys.color.error}` |
| `comp.button.text.destructive.icon.color` | `color` | `{tanrei.comp.button.text.destructive.label.color}` |
| `comp.button.text.destructive.outline.color` | `color` | `{tanrei.comp.button.text.outline.color}` |
| `comp.button.text.destructive.state-layer.color` | `color` | `{tanrei.comp.button.text.destructive.label.color}` |
| `comp.button.motion.state-duration.small` | `duration` | `{tanrei.sys.motion.role.state.compact}` |
| `comp.button.motion.state-duration.medium` | `duration` | `{tanrei.sys.motion.role.state.default}` |
| `comp.button.motion.state-duration.large` | `duration` | `{tanrei.sys.motion.role.state.large}` |
| `comp.button.motion.state-easing` | `cubicBezier` | `{tanrei.sys.motion.easing.state}` |
| `comp.button.motion.feedback-duration` | `duration` | `{tanrei.sys.motion.role.feedback}` |
| `comp.button.motion.busy-cycle-duration` | `duration` | `{tanrei.comp.loading-indicator.motion.cycle-duration}` |
| `comp.button.busy.indicator.thickness` | `dimension` | `{tanrei.comp.loading-indicator.indicator.thickness.small}` |
| `comp.button.tone.container.opacity` | `number` | `{tanrei.sys.state.selected-state-layer-opacity}` |

Per-state tokens, valued by the design system's pattern rules (not written by hand):

- `comp.button.<variant>.<state>.<property>` for the variants `filled`, `tonal`, `elevated`, `outlined`, `text`, the states `hovered`, `focused`, `pressed`, `disabled` and the properties `container.color`, `label.color`, `icon.color`, `outline.color`, `state-layer.color`, `container.elevation`, `unselected.container.color`, `unselected.label.color`, `unselected.icon.color`, `unselected.outline.color`, `unselected.state-layer.color`, `selected.container.color`, `selected.label.color`, `selected.icon.color`, `selected.outline.color`, `selected.state-layer.color`, `destructive.container.color`, `destructive.label.color`, `destructive.icon.color`, `destructive.outline.color`, `destructive.state-layer.color`.
- `comp.button.<state>.<property>` for the states `hovered`, `focused`, `pressed`, `disabled` and the properties `opacity`, `container.shape`.

## الإرشادات

### متى يُستخدم

استخدم الزر لإجراء يحدث حيث يكون الشخص: إرسال نموذج، الحفظ، بدء عملية، تأكيد مربع حوار. وعلى الويب، الزر الذي يرسل نموذجه يقول ذلك بالخاصية `type` (انظر [في نموذج](https://tanrei.chnotchy.com/ar/components/button/#%D9%81%D9%8A-%D9%86%D9%85%D9%88%D8%B0%D8%AC)).

إن كان تفعيله لا يفعل إلا نقل الشخص إلى مكان آخر فهو رابط. وحين تستحق تلك الوجهة توكيد الزر، مثل «البدء» في صفحة هبوط أو «عرض الكل» في نهاية قائمة، أبقِ الزر وأعطِه الخاصية `href`: فيكون عندئذ رابطًا، له دور الرابط ومفاتيحه وسلوكه في المتصفح (انظر [الزر الذي له وجهة رابط](https://tanrei.chnotchy.com/ar/components/button/#%D8%A7%D9%84%D8%B2%D8%B1-%D8%A7%D9%84%D8%B0%D9%8A-%D9%84%D9%87-%D9%88%D8%AC%D9%87%D8%A9-%D8%B1%D8%A7%D8%A8%D8%B7)). أما الوجهة التي داخل جملة فتبقى رابطًا نصيًّا عاديًّا. والإجراء الذي يخص نصًّا هو زر نصي بعد النص، لا عبارة داخله: فما يُرسم كرابط وليس رابطًا يترك الشخص غير متأكد مما سيفعله.

### اختيار الطراز

تعبّر الطُرُز (variants) عن التوكيد، والتوكيد ميزانية. أنفقها مرة واحدة في كل عرض.

| الطراز | التوكيد | يُستخدم لـ |
| --- | --- | --- |
| Filled | الأعلى | الإجراء الأهم الوحيد في عرض أو مربع حوار. |
| Tonal | عالٍ | إجراء مهم لا ينبغي أن ينافس زرًّا من الطراز filled، أو الإجراء الرئيسي في منطقة ثانوية. |
| Elevated | متوسط | إجراء يجب أن ينفصل عن خلفية مزدحمة أو منقوشة. |
| Outlined | متوسط | إجراءات متساوية الوزن بجانب زر من الطراز filled، مثل «إلغاء». |
| Text | منخفض | أقل الإجراءات إلحاحًا، والإجراءات ضمن السطر في البطاقات ومربعات الحوار وأشرطة الأدوات. |

الخاصية `destructive` ليست طرازًا: تُضاف إلى أي طراز، وتحفظ درجة إبرازه (انظر [الإجراءات المدمرة](https://tanrei.chnotchy.com/ar/components/button/#%D8%A7%D9%84%D8%A5%D8%AC%D8%B1%D8%A7%D8%A1%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%AF%D9%85%D8%B1%D8%A9)).

الزر الذي بلا `variant` يكون من الطراز filled، مع استثناء واحد: حين يوضع إجراءً في [مربع حوار](https://tanrei.chnotchy.com/ar/components/dialog/) أو في [لوحة جانبية](https://tanrei.chnotchy.com/ar/components/side-sheet/) يأخذ طراز ذلك المضيف، وهو tonal في النظام الافتراضي، فلا تضبط الطراز إلا على الإجراء الذي ينبغي أن يبدو مختلفًا. وزر الإغلاق في كل مضيف، في اللوحة المنبثقة واللوحة الجانبية واللوحة السفلية والتنويه وشريط الإشعار ومعاينة الصور، زر صغير (small) بأيقونة فقط يأخذ بالطريقة نفسها طراز زر الإغلاق الخاص بمضيفه، وهو tonal في النظام الافتراضي. وعلى سطح ملوّن، مثل تنويه لحالة أو شريط إشعار معكوس، يُرسم أيضًا بلون نص ذلك السطح.

### افعل

- اكتب التسميات بصيغة الفعل، أو الفعل والاسم، بحيث تقول ما سيحدث: **نشر**، **حفظ المسودة**، **إضافة عضو**.
- اكتفِ بزر واحد من الطراز filled في كل عرض. إن بدا إجراءان أساسيين بالقدر نفسه، فالأرجح أن العرض يؤدي مهمتين.
- ضع الإجراء الأساسي حيث يتوقعه مستخدمو المنصة، وحافظ على ذلك الترتيب في المنتج كله.
- استخدم `small` (32) في أشرطة الأدوات والجداول الكثيفة، و `medium` (40) افتراضيًّا، و `large` (56) للواجهات التي تعتمد اللمس أولًا أو للمواضع البارزة (hero). مساحة اللمس آمنة في كل حجم.
- اضبط `destructive` على الإجراء الذي يحذف شيئًا أو يتخلص منه، واختر طرازه بحسب درجة إبرازه، كما تفعل مع أي إجراء آخر.
- اجعل الرابط المؤدي إلى المكان الذي يُحذف فيه شيء رابطًا عاديًا، تقول تسميته إلى أين يؤدي. واضبط `destructive` على الزر الذي يحذف، في ذلك المكان.
- استخدم منفذ الأيقونة لتعزيز التسمية. لا تحذف التسمية إلا حيث يكون رسم الأيقونة مفهومًا على نطاق واسع (البحث، الإغلاق، المزيد)، وسمِّ الزر بالخاصية `label`.

### لا تفعل

- لا تنفّذ التنقل من معالج النقر على زر. عنصر التحكم الذي له وجهة رابط: اضبط الخاصية `href`، ليمكن فتحه في تبويب جديد ونسخه وإعلانه رابطًا.
- لا تعطِ الخاصية `href` لما ينفّذ فعلًا في الصفحة، مثل «حفظ» أو «حذف»، ولا تفتح تبويبًا جديدًا من دون أن تقول ذلك في `targetLabel`.
- لا تضع زرًّا من الطراز filled في كل صف من قائمة. الأفضل أزرار من الطراز text، أو إجراء واحد في الأعلى ينطبق على التحديد.
- لا تجعل تعطيل الزر الطريقة الوحيدة للإبلاغ عن مشكلة في التحقق. أبقِه مفعَّلًا واشرح ما ينقص عند ضغطه، أو أبقِه في المتناول بالخاصية `focusable-when-disabled` وقل السبب.
- لا تعطّل الزر ما دام إجراؤه جاريًا. اضبط الخاصية `busy`، فيبقى التركيز عليه.
- لا تستخدم `destructive` للإبراز، ولا لإجراء لا يدمّر شيئًا. إن كانت كل إجراءات الواجهة حمراء، لم يبرز أي منها.
- لا تجعل الرابط أحمر، ولا تعطِ زرًّا الخاصيتين `href` و`destructive` معًا. فالرابط لا يفعل إلا أن يؤدي إلى مكان ما، ولذلك يتجاهل الزر `destructive` ويُرسم رابطًا من طرازه.
- لا تقتطع التسميات. دع الزر يكبر، أو اكتب تسمية أقصر.
- لا تترك زرًّا بأيقونة وحدها من دون `label`: رسم الأيقونة ليس اسمًا.
- لا ترصّ بيدك الأزرار الضيقة ذات الأيقونة وحدها حافةً إلى حافة. أهدافها تمتد إلى ما وراء حوافها فتتداخل.

### المحتوى النصي

تُكتب التسميات من دون علامة ترقيم في آخرها. اجعلها كلمة أو كلمتين. اكتبها كما تكتب كل لغة من لغات منتجك التسمية: بأسلوب الجملة (sentence case) في الإنجليزية؛ وفي اليابانية بصيغة الاسم أو الاسم الفعلي مثل 保存 أو デプロイ لا بنهاية جملة مهذبة.

## إمكانية الوصول

### ما يتكفل به المكوّن

- يعرّف التقنيات المساعدة بدور الزر وبحالة تعطيله على كل منصة. ومع الخاصية `href` يعرّفها بدور الرابط بدلًا من ذلك، ولا يفعّله إلا المفتاح Enter.
- زر التبديل زر له حالة ضغط، لا خانة اختيار ولا مفتاح تبديل، واسمه لا يتغيّر بتغيّر حالته.
- الرابط الذي يفتح سياقًا جديدًا يقول ذلك مرتين: بالعلامة `open_in_new` لمن يرونها، وبالخاصية `targetLabel` وصفًا للرابط لمن لا يرونها. والعلامة نفسها مخفية عن التقنيات المساعدة.
- مع الخاصية `label` بجانب تسمية مرسومة يعرّف التقنيات المساعدة باسم واحد، هو `label`، على عنصر تحكم واحد. والنص المرسوم لا يُقرأ مرة ثانية.
- على الويب، المفتاح Enter في حقل يضغط الزر الافتراضي للنموذج، والزر الافتراضي المعطَّل أو المشغول يوقف ذلك المفتاح، والزر في عنصر `fieldset` معطَّل يتعطّل معه.
- الزر المشغول، والزر المعطَّل ذو الخاصية `focusable-when-disabled`، يبقيان في ترتيب التركيز ويحتفظان بالتركيز. ويُبلغان التقنيات المساعدة أنه لا يمكن تفعيلهما الآن، والزر المشغول يُعلن النص `busy-label` مرة واحدة.
- مساحة اللمس تستوفي الحد الأدنى للهدف حتى في الحجم small وفي العرض narrow، من دون أن يُرسم الزر أكبر.
- يعرض مؤشر التركيز المكوَّن من فجوة ثم حلقة عند التركيز بلوحة المفاتيح، بلا حركة.
- ألوانه تأتي من أزواج أدوار يُتحقق من تباينها عند كل مستوى تباين. حدّ الزر من الطراز outlined هو الخط الرفيع في النظام الافتراضي، وهو هادئ عن قصد عند التباين القياسي: التسمية والأيقونة هما ما يعرّف بعنصر التحكم، لا حافته. وعند التباين المتوسط والعالي يُرفع هذا الخط الرفيع إلى 3:1 و 4.5:1. والمنتج الذي يحتاج إلى حافة أقوى يضبط الرمز `comp.button.outlined.outline.color` على `sys.color.outline` في طبقة.
- عند أحجام النص الأكبر تكبر الحاوية ولا تُقتطع التسمية أبدًا.
- الخاصية `destructive` لا تغيّر إلا الألوان. الدور والاسم والحالات والمفاتيح هي نفسها للزر ذاته من دونها، لأن الويب ليس فيه دور ولا حالة تقول إن الإجراء مدمِّر. وفي SwiftUI هي دور المنصة نفسها للإجراء المدمِّر (destructive role).

### ما عليك فعله

- اكتب تسمية مفهومة خارج سياقها. «حذف المشروع» أفضل من «حذف» بجانب اسم مشروع، لأن مستخدم قارئ الشاشة قد يسمع الزر وحده.
- إن كان الإجراء يبدأ عملية طويلة، فاضبط الخاصية `busy` مع `busy-label`، وأعلن النتيجة حين ينتهي. تعطيل الزر وحده لا يقول شيئًا لمستخدم قارئ الشاشة، ويُفقده تركيزه.
- لا تعتمد على اللون وحده لتقول إن الإجراء مدمِّر. الخاصية `destructive` ترسمه بلون الخطأ، لكن من لا يرى ذلك اللون أو لا يميّزه لا يعرف ذلك إلا من التسمية. اكتب «حذف المشروع» لا «موافق»، وقل في مربع الحوار الذي يؤكده ما الذي سيضيع.
- سمِّ زر الرابط بالمكان الذي يذهب إليه: «عرض الفاتورة»، لا «انقر هنا». وحين تكون قيمة `target` هي `new`، اكتب `targetLabel`.
- حيث ترسم عدة أزرار الكلمة نفسها، أعطِ كل زر `label` تحتوي على الكلمة وتقول ما يعمل عليه الزر (انظر [اسم أطول من التسمية](https://tanrei.chnotchy.com/ar/components/button/#%D8%A7%D8%B3%D9%85-%D8%A3%D8%B7%D9%88%D9%84-%D9%85%D9%86-%D8%A7%D9%84%D8%AA%D8%B3%D9%85%D9%8A%D8%A9)).
- حين يكون زر الإرسال في نموذج معطَّلًا، قل في الصفحة ما الذي ما زال ناقصًا. فالزر المعطَّل يوقف المفتاح Enter أيضًا، ولا يشرح بنفسه شيئًا.
- سمِّ كل زر بأيقونة وحدها بالخاصية `label`. قد يعرض [التلميح](https://tanrei.chnotchy.com/ar/components/tooltip/) النص نفسه لمن يستخدمون مؤشرًا، لكنه لا يغني أبدًا عن `label`.
