# Ionic Component Implementation Guide - [Button States](#button-states) * [Component Structure](#component-structure) * [Disabled](#disabled) * [Focused](#focused) * [Hover](#hover) * [Activated](#activated) * [Ripple Effect](#ripple-effect) * [Example Components](#example-components) * [References](#references) - [Accessibility](#accessibility) * [Checkbox](#checkbox) * [Switch](#switch) * [Accordion](#accordion) - [Rendering Anchor or Button](#rendering-anchor-or-button) * [Example Components](#example-components-4) * [Component Structure](#component-structure-1) - [Converting Scoped to Shadow](#converting-scoped-to-shadow) - [RTL](#rtl) - [Adding New Components with Native Input Support](#adding-new-components-with-native-input-support) * [Angular Integration](#angular-integration) * [Angular Tests](#angular-tests) * [Vue Integration](#vue-integration) * [Vue Tests](#vue-tests) * [React Integration](#react-integration) * [React Tests](#react-tests) * [Interface Exports](#interface-exports) ## Button States Any component that renders a button should have the following states: [`disabled`](#disabled), [`focused`](#focused), [`hover`](#hover), [`activated`](#activated). It should also have a [Ripple Effect](#ripple-effect) component added for Material Design. ### Component Structure #### JavaScript A component that renders a native button should use the following structure: ```jsx ``` Any other attributes and classes that are included are irrelevant to the button states, but it's important that this structure is followed and the classes above exist. In some cases they may be named something else that makes more sense, such as in item. #### CSS A mixin called `button-state()` has been added to make it easier to setup the states in each component. ```scss @mixin button-state() { @include position(0, 0, 0, 0); position: absolute; content: ""; opacity: 0; } ``` The following styles should be set for the CSS to work properly. Note that the `button-state()` mixin is included in the `::after` pseudo element of the native button. ```scss .button-native { /** * All other CSS in this selector is irrelevant to button states * but the following are required styles */ position: relative; overflow: hidden; } .button-native::after { @include button-state(); } .button-inner { /** * All other CSS in this selector is irrelevant to button states * but the following are required styles */ position: relative; z-index: 1; } ``` ### Disabled The disabled state should be set via prop on all components that render a native button. Setting a disabled state will change the opacity or color of the button and remove click events from firing. #### JavaScript The `disabled` property should be set on the component: ```jsx /** * If `true`, the user cannot interact with the button. */ @Prop({ reflectToAttr: true }) disabled = false; ``` Then, the render function should add the [`aria-disabled`](https://www.w3.org/WAI/PF/aria/states_and_properties#aria-disabled) role to the host, a class that is the element tag name followed by `disabled`, and pass the `disabled` attribute to the native button: ```jsx render() { const { disabled } = this; return ( ); } ``` > [!NOTE] > If the class being added was for `ion-back-button` it would be `back-button-disabled`. #### CSS The following CSS _at the bare minimum_ should be added for the disabled class, but it should be styled to match the spec: ```css :host(.button-disabled) { cursor: default; opacity: .5; pointer-events: none; } ``` #### User Customization TODO ### Focused The focused state should be enabled for elements with actions when tabbed to via the keyboard. This will only work inside of an `ion-app`. It usually changes the opacity or background of an element. > [!WARNING] > Do not use `:focus` because that will cause the focus to apply even when an element is tapped (because the element is now focused). Instead, we only want the focus state to be shown when it makes sense which is what the `.ion-focusable` utility mentioned below does. > [!IMPORTANT] > Make sure the component has the correct [component structure](#component-structure) before continuing. #### JavaScript The `ion-focusable` class needs to be set on an element that can be focused: ```jsx render() { return ( ); } ``` Once that is done, the element will get the `ion-focused` class added when the element is tabbed to. #### CSS Components should be written to include the following focused variables for styling: ```css /** * @prop --color-focused: Color of the button when tabbed to with the keyboard * @prop --background-focused: Background of the button when tabbed to with the keyboard * @prop --background-focused-opacity: Opacity of the background when tabbed to with the keyboard */ ``` Style the `ion-focused` class based on the spec for that element: ```scss :host(.ion-focused) .button-native { color: var(--color-focused); &::after { background: var(--background-focused); opacity: var(--background-focused-opacity); } } ``` > [!IMPORTANT] > Order matters! Focused should be **before** the activated and hover states. #### User Customization Setting the focused state on the `::after` pseudo-element allows the user to customize the focused state without knowing what the default opacity is set at. A user can customize in the following ways to have a solid red background on focus, or they can leave out `--background-focused-opacity` and the button will use the default focus opacity to match the spec. ```css ion-button { --background-focused: red; --background-focused-opacity: 1; } ``` #### When to use `.ion-focusable` versus `:focus-visible` The [`:focus-visible`](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible) pseudo-class mostly does the same thing as our JavaScript-driven utility. However, it does not work well with Shadow DOM components as the element that receives focus is typically inside of the Shadow DOM, but we usually want to set the `:focus-visible` state on the host so we can style other parts of the component. Using other combinations such as `:has(:focus-visible)` does not work because `:has` does not pierce the Shadow DOM (as that would leak implementation details about the Shadow DOM contents). `:focus-within` does work with the Shadow DOM, but that has the same problem as `:focus` that was mentioned before. Unfortunately, a [`:focus-visible-within` pseudo-class does not exist yet](https://github.com/WICG/focus-visible/issues/151). The `.ion-focusable` class should be used when you want to style Element A based on the state of Element B. For example, the Button component styles the host of the component (Element A) when the native `button` inside the Shadow DOM (Element B) has focus. On the other hand, the `:focus-visible` pseudo-class can be used when you want to style the element based on its own state. For example, we could use `:focus-visible` to style the clear icon on Input when the icon itself is focused. ### Hover The [hover state](https://developer.mozilla.org/en-US/docs/Web/CSS/:hover) happens when a user moves their cursor on top of an element without pressing on it. It should not happen on mobile, only on desktop devices that support hover. > [!NOTE] > Some Android devices [incorrectly report their inputs](https://issues.chromium.org/issues/40855702) which can result in certain devices receiving hover events when they should not. > [!IMPORTANT] > Make sure the component has the correct [component structure](#component-structure) before continuing. #### CSS Components should be written to include the following hover variables for styling: ```css /** * @prop --color-hover: Color of the button on hover * @prop --background-hover: Background of the button on hover * @prop --background-hover-opacity: Opacity of the background on hover */ ``` Style the `:hover` based on the spec for that element: ```scss @media (any-hover: hover) { :host(:hover) .button-native { color: var(--color-hover); &::after { background: var(--background-hover); opacity: var(--background-hover-opacity); } } } ``` > [!IMPORTANT] > Order matters! Hover should be **before** the activated state. #### User Customization Setting the hover state on the `::after` pseudo-element allows the user to customize the hover state without knowing what the default opacity is set at. A user can customize in the following ways to have a solid red background on hover, or they can leave out `--background-hover-opacity` and the button will use the default hover opacity to match the spec. ```css ion-button { --background-hover: red; --background-hover-opacity: 1; } ``` ### Activated The activated state should be enabled for elements with actions on "press". It usually changes the opacity or background of an element. > [!WARNING] >`:active` should not be used here as it is not received on mobile Safari unless the element has a `touchstart` listener (which we don't necessarily want to have to add to every element). From [Safari Web Content Guide](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/AdjustingtheTextSize/AdjustingtheTextSize.html): > >> On iOS, mouse events are sent so quickly that the down or active state is never received. Therefore, the `:active` pseudo state is triggered only when there is a touch event set on the HTML element > [!IMPORTANT] > Make sure the component has the correct [component structure](#component-structure) before continuing. #### JavaScript The `ion-activatable` class needs to be set on an element that can be activated: ```jsx render() { return ( ); } ``` Once that is done, the element will get the `ion-activated` class added on press after a small delay. This delay exists so that the active state does not show up when an activatable element is tapped while scrolling. In addition to setting that class, `ion-activatable-instant` can be set in order to have an instant press with no delay: ```jsx ``` #### CSS ```css /** * @prop --color-activated: Color of the button when pressed * @prop --background-activated: Background of the button when pressed * @prop --background-activated-opacity: Opacity of the background when pressed */ ``` Style the `ion-activated` class based on the spec for that element: ```scss :host(.ion-activated) .button-native { color: var(--color-activated); &::after { background: var(--background-activated); opacity: var(--background-activated-opacity); } } ``` > [!IMPORTANT] > Order matters! Activated should be **after** the focused & hover states. #### User Customization Setting the activated state on the `::after` pseudo-element allows the user to customize the activated state without knowing what the default opacity is set at. A user can customize in the following ways to have a solid red background on press, or they can leave out `--background-activated-opacity` and the button will use the default activated opacity to match the spec. ```css ion-button { --background-activated: red; --background-activated-opacity: 1; } ``` ### Ripple Effect The ripple effect should be added to elements for Material Design. It *requires* the `ion-activatable` class to be set on the parent element to work, and relative positioning on the parent. ```jsx render() { const mode = getIonMode(this); return ( ); ``` The ripple effect can also accept a different `type`. By default it is `"bounded"` which will expand the ripple effect from the click position outwards. To add a ripple effect that always starts in the center of the element and expands in a circle, set the type to `"unbounded"`. An unbounded ripple will exceed the container, so add `overflow: hidden` to the parent to prevent this. Make sure to style the ripple effect for that component to accept a color: ```css ion-ripple-effect { color: var(--ripple-color); } ``` ### Example Components - [ion-button](https://github.com/ionic-team/ionic-framework/tree/main/core/src/components/button) - [ion-back-button](https://github.com/ionic-team/ionic-framework/tree/main/core/src/components/back-button) - [ion-menu-button](https://github.com/ionic-team/ionic-framework/tree/main/core/src/components/menu-button) ### References - [Material Design States](https://material.io/design/interaction/states.html) - [iOS Buttons](https://developer.apple.com/design/human-interface-guidelines/ios/controls/buttons/) ## Accessibility ### Checkbox #### Example Components - [ion-checkbox](https://github.com/ionic-team/ionic-framework/tree/main/core/src/components/checkbox) #### VoiceOver In order for VoiceOver to work properly with a checkbox component there must be a native `input` with `type="checkbox"`, and `aria-checked` and `role="checkbox"` **must** be on the host element. The `aria-hidden` attribute needs to be added if the checkbox is disabled, preventing iOS users from selecting it: ```tsx render() { const { checked, disabled } = this; return ( ... ); } ``` #### NVDA It is required to have `aria-checked` on the native input for checked to read properly and `disabled` to prevent tabbing to the input: ```tsx render() { const { checked, disabled } = this; return ( ... ); } ``` #### Labels Labels should be passed directly to the component in the form of either visible text or an `aria-label`. The visible text can be set inside of a `label` element, and the `aria-label` can be set directly on the interactive element. In the following example the `aria-label` can be inherited from the Host using the `inheritAttributes` or `inheritAriaAttributes` utilities. This allows developers to set `aria-label` on the host element since they do not have access to inside the shadow root. > [!NOTE] > Use `inheritAttributes` to specify which attributes should be inherited or `inheritAriaAttributes` to inherit all of the possible `aria` attributes. ```tsx import { Prop } from '@stencil/core'; import { inheritAttributes } from '@utils/helpers'; import type { Attributes } from '@utils/helpers'; ... private inheritedAttributes: Attributes = {}; @Prop() labelText?: string; componentWillLoad() { this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']); } render() { return ( ) } ``` #### Hidden Input A helper function to render a hidden input has been added, it can be added in the `render`: ```tsx renderHiddenInput(true, el, name, (checked ? value : ''), disabled); ``` > This is required for the checkbox to work with forms. #### Known Issues When using VoiceOver on macOS, Chrome will announce the following when you are focused on a checkbox: ``` currently on a checkbox inside of a checkbox ``` This is a compromise we have to make in order for it to work with the other screen readers & Safari. ### Switch #### Example Components - [ion-toggle](https://github.com/ionic-team/ionic-framework/tree/main/core/src/components/toggle) #### Voiceover In order for VoiceOver to work properly with a switch component there must be a native `input` with `type="checkbox"` and `role="switch"`, and `aria-checked` and `role="switch"` **must** be on the host element. The `aria-hidden` attribute needs to be added if the switch is disabled, preventing iOS users from selecting it: ```tsx render() { const { checked, disabled } = this; return ( ... ); } ``` #### NVDA It is required to have `aria-checked` on the native input for checked to read properly and `disabled` to prevent tabbing to the input: ```tsx render() { const { checked, disabled } = this; return ( ... ); } ``` #### Labels Labels should be passed directly to the component in the form of either visible text or an `aria-label`. The visible text can be set inside of a `label` element, and the `aria-label` can be set directly on the interactive element. In the following example the `aria-label` can be inherited from the Host using the `inheritAttributes` or `inheritAriaAttributes` utilities. This allows developers to set `aria-label` on the host element since they do not have access to inside the shadow root. > [!NOTE] > Use `inheritAttributes` to specify which attributes should be inherited or `inheritAriaAttributes` to inherit all of the possible `aria` attributes. ```tsx import { Prop } from '@stencil/core'; import { inheritAttributes } from '@utils/helpers'; import type { Attributes } from '@utils/helpers'; ... private inheritedAttributes: Attributes = {}; @Prop() labelText?: string; componentWillLoad() { this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']); } render() { return ( ) } ``` #### Hidden Input A helper function to render a hidden input has been added, it can be added in the `render`: ```tsx renderHiddenInput(true, el, name, (checked ? value : ''), disabled); ``` > This is required for the switch to work with forms. #### Known Issues When using VoiceOver on macOS or iOS, Chrome will announce the switch as a checked or unchecked `checkbox`: ``` You are currently on a switch. To select or deselect this checkbox, press Control-Option-Space. ``` There is a WebKit bug open for this: https://bugs.webkit.org/show_bug.cgi?id=196354 ### Accordion #### Example Components - [ion-accordion](https://github.com/ionic-team/ionic-framework/tree/master/core/src/components/accordion) - [ion-accordion-group](https://github.com/ionic-team/ionic-framework/tree/master/core/src/components/accordion-group) #### NVDA In order to use the arrow keys to navigate the accordions, users must be in "Focus Mode". Typically, NVDA automatically switches between Browse and Focus modes when inside of a form, but not every accordion needs a form. You can either wrap your `ion-accordion-group` in a form, or manually toggle Focus Mode using NVDA's keyboard shortcut. ## Rendering Anchor or Button Certain components can render an `` or a `