Button
The button element is a multi-touch enabled button, allowing simultaneous presses, pointers sliding between buttons, and concurrent use with other inputs.
It is a versatile element, designed specifically for game-like controls with touch, mirroring the Gamepad API button properties.
- html
- react
<xyba-button>X</xyba-button>
import { Button } from "xyba-elements/react";
export default () => <Button />;
Install
npm install xyba-elements
Importing
Once installed, you can import via xyba-elements
.
// Custom Elements
import "xyba-elements/elements/button/button.js";
// React
import { Button } from "xyba-elements/react";
Or import from CDN,
<script src="https://cdn.jsdelivr.net/npm/xyba-elements/dist/cdn/elements/button/button.js">
Examples
Capture pointer
Add the attribute or property capture
to designate the button as the target for future events of the pointer until it's released. It also has the effect that the button is pressed until the pointer is lifted.
NB! Capture isn't suitable for buttons implementing the
relates-to
attribute or property. In this case wrap the elements with xyba-capture
- html
- react
<xyba-button capture>A</xyba-button> <xyba-button capture>B</xyba-button>
import { Button } from "xyba-elements/react";
export default () => (
<>
<Button capture>A</Button>
<Button capture>B</Button>
</>
);
Ignore Capture
The xyba-button has an option to react to events even though the pointer has pointerCapture. This can be useful in some scenarios, where eg. a xyba-analog element has capture
but should still be able to press the button.
The capture
attribute can be empty (it will ignore all pointer captures from any element), or a string of query selectors for element captures it should ignore (only ignore pointer captures from specific elements),
The property
ignoreCapture
can also be set totrue
orfalse
to ignore or not.
- html
<xyba-analog capture></xyba-analog>
<xyba-button capture ignore-capture>A</xyba-button>
Here's an example where button A only reacts to captured events from button B, but not from button C.
- html
<xyba-button capture ignore-capture="#ignore-b-capture">A</xyba-button>
<xyba-button capture id="ignore-b-capture">B</xyba-button>
<xyba-button capture>C</xyba-button>
Relate to Other Elements
You can have a button relate to HTML elements using the attribute relates-to
or property .relatesTo
using query selectors separated by spaces in a string.
- html
- react
<xyba-button relates-to="#relates-xy">X</xyba-button>
<xyba-button relates-to="#relates-xy">Y</xyba-button>
<xyba-button id="relates-xy">XY</xyba-button>
import { Button } from "xyba-elements/react";
export default () => (
<>
<Button relatesTo="#relates-xy">X</Button>
<Button relatesTo="#relates-xy">Y</Button>
<Button id="relates-xy">XY</Button>
</>
);
Relates to advanced
Here's an advanced example for creating a diamond shaped collection of buttons with support for multiple button presses with a single pointer. It works by relating the buttons to a number of invisible HTML elements.
- html
- react
- css
<div class="rel-diamond">
<div class="rel-parent rel-top">
<xyba-button relates-to=".rel-top .rel-top-left .rel-top-right"
>Y</xyba-button
>
</div>
<div class="rel-parent rel-right">
<xyba-button relates-to=".rel-right .rel-top-right .rel-bottom-right"
>B</xyba-button
>
</div>
<div class="rel-parent rel-bottom">
<xyba-button relates-to=".rel-bottom .rel-bottom-left .rel-bottom-right"
>A</xyba-button
>
</div>
<div class="rel-parent rel-left">
<xyba-button relates-to=".rel-left .rel-bottom-left .rel-top-left"
>X</xyba-button
>
</div>
<div class="rel-area rel-top-left"></div>
<div class="rel-area rel-top-right"></div>
<div class="rel-area rel-bottom-right"></div>
<div class="rel-area rel-bottom-left"></div>
</div>
import { Button } from "xyba-elements/react";
export default () => (
<div className="rel-diamond">
<div className="rel-parent rel-top">
<Button relatesTo=".rel-top .rel-top-left .rel-top-right">Y</Button>
</div>
<div className="rel-parent rel-right">
<Button relatesTo=".rel-right .rel-top-right .rel-bottom-right">B</Button>
</div>
<div className="rel-parent rel-bottom">
<Button relatesTo=".rel-bottom .rel-bottom-left .rel-bottom-right">
A
</Button>
</div>
<div className="rel-parent rel-left">
<Button relatesTo=".rel-left .rel-bottom-left .rel-top-left">X</Button>
</div>
<div className="rel-area rel-top-left"></div>
<div className="rel-area rel-top-right"></div>
<div className="rel-area rel-bottom-right"></div>
<div className="rel-area rel-bottom-left"></div>
</div>
);
.rel-diamond {
--offset: 15%;
--size: 200px;
--button-radii: 999px;
position: relative;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
grid-template-areas:
"top-left top-right"
"bottom-left bottom-right";
width: var(--size);
height: var(--size);
background: color-mix(in srgb, currentColor 4%, transparent);
border-radius: 12px;
.rel-parent {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.rel-top {
top: 0;
transform: translate(-50%, 0);
padding-top: var(--offset);
border-radius: 0 0 var(--button-radii) var(--button-radii);
}
.rel-right {
left: unset;
right: 0;
transform: translate(0, -50%);
padding-right: var(--offset);
border-radius: var(--button-radii) 0 0 var(--button-radii);
}
.rel-bottom {
bottom: 0;
top: unset;
left: 50%;
transform: translate(-50%, 0);
padding-bottom: var(--offset);
border-radius: var(--button-radii) var(--button-radii) 0 0;
}
.rel-left {
left: 0;
transform: translate(0, -50%);
padding-left: var(--offset);
border-radius: 0 var(--button-radii) var(--button-radii) 0;
}
.rel-area {
width: 100%;
height: 100%;
}
.rel-top-left {
grid-area: top-left;
}
.rel-top-right {
grid-area: top-right;
}
.rel-bottom-right {
grid-area: bottom-right;
}
.rel-bottom-left {
grid-area: bottom-left;
}
}
Expand Interaction Area
You can use ::before
and ::after
pseudo-elements to expand its interaction area, making it larger and easier to click.
- html
- react
- css
<xyba-button class="before-example">X</xyba-button>
import { Button } from "xyba-elements/react";
export default () => <Button className="before-example">X</Button>;
.before-example::before {
content: "";
width: 200%;
height: 200%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: 1px dashed currentColor;
border-radius: 12px;
}
Toggle
Makes the button into a toggle button
- html
<xyba-button toggle>X</xyba-button>
Disable
Disables the button, along side all pointer events.
- html
- react
<xyba-button disabled>X</xyba-button>
import { Button } from "xyba-elements/react";
export default () => {
return <Button disabled>X</Button>;
};
Customizing style
Disable default visual style
Add the attribute no-style
to remove all default visual styling to implement your own style. The base styles for layout and pointer-events are preserved.
- html
- react
<xyba-button no-style>A</xyba-button>
import { Button } from "xyba-elements/react";
export default () => <Button noStyle />;
Style using ::part
The button can be styled using ::parts.
- html
- React
- css
<xyba-button class="button-part">A</xyba-button>
import { Button } from "xyba-elements/react";
export default () => <Button class="button-part">A</Button>;
.button-part {
color: black;
&::part(base) {
background-color: salmon;
border-width: 4px;
transition: background-color 0.05s ease-in-out;
}
&[pressed]::part(base) {
background-color: color-mix(in srgb, salmon 80%, black);
}
&::part(content) {
color: currentColor;
}
}
Custom visual styling
A simple custom style could be implemented like this.
- html
- react
- css
<xyba-button class="custom-style" no-style>
<span>A</span>
</xyba-button>
import { Button } from "xyba-elements/react";
export default () => (
<Button className="custom-style" noStyle>
<span>A</span>
</Button>
);
.custom-style {
width: 42px;
aspect-ratio: 1;
background-color: color-mix(in srgb, currentColor 10%, transparent);
border-radius: 8px;
&::before {
content: "";
position: absolute;
width: 100%;
height: 100%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: 2px solid currentColor;
border-radius: inherit;
pointer-events: none;
transition:
width 0.05s,
height 0.05s;
}
&[pressed] {
background-color: currentColor;
&::before {
width: calc(100% + 32px);
height: calc(100% + 32px);
border-radius: 12px;
}
& > span {
filter: invert(1);
}
}
}
Slotted elements
It's designed to be simple to use images by using slotted elements, the xyba-button accepts slotted elements for:
- base - The base element (background)
- image - The base image that is always visible.
- image-pressed - The image only visible when pressed.
- html
- react
- css
<xyba-button no-style class="slotted-elements">
<img slot="image" src="https://shorturl.at/MVwBM" loading="eager" />
<img slot="image-pressed" src="https://shorturl.at/rMHQj" loading="eager" />
</xyba-button>
import { Button } from "xyba-elements/react";
export default () => (
<Button noStyle className="slotted-elements">
<img slot="image" src="https://shorturl.at/MVwBM" loading="eager" />
<img slot="image-pressed" src="https://shorturl.at/rMHQj" loading="eager" />
</Button>
);
.slotted-elements {
width: 42px;
height: 42px;
img {
width: 100%;
height: 100%;
}
}
Events
Use Inline events
Call global functions directly in the html using inline events. All events are available with the prefix on{event_name}
as attributes and properties.
- html
- javascript
- react
<xyba-button onpress="window.handlePress">A</xyba-button>
<xyba-button onrelease="window.handleRelease">B</xyba-button>
<xyba-button onup="window.handleUp">C</xyba-button>
<xyba-button ondown="window.handleDown">D</xyba-button>
function handlePress(e) {
console.log(`Button ${e.target.innerHTML} pressed`);
}
function handleRelease(e) {
console.log(`Button ${e.target.innerHTML} released`);
}
function handleUp(e) {
console.log(
`Button ${e.target.innerHTML} released and lifted from the button`,
);
}
function handleDown(e) {
console.log(
`Button ${e.target.innerHTML} was pressed directly on the button`,
);
}
window["handlePress"] = handlePress;
window["handleRelease"] = handleRelease;
window["handleUp"] = handleUp;
window["handleDown"] = handleDown;
import { Button } from "xyba-elements/react";
export default () => (
<>
<Button onPress={window.handlePress}>A</Button>
<Button onRelease={window.handleRelease}>B</Button>
<Button onRelease={window.handleUp}>C</Button>
<Button onRelease={window.handleDown}>D</Button>
</>
);
Use event listeners
Use references to the elements and add event listeners.
- html
- javascript
- react
<xyba-button id="event-button-a">A</xyba-button>
<xyba-button id="event-button-b">B</xyba-button>
<xyba-button id="event-button-c">C</xyba-button>
<xyba-button id="event-button-d">D</xyba-button>
const buttonA = document.querySelector("#event-button-a");
const buttonB = document.querySelector("#event-button-b");
const buttonC = document.querySelector("#event-button-c");
const buttonD = document.querySelector("#event-button-d");
buttonA.addEventListener("press", (e) => {
console.log(`Button with id: ${e.target.id} was pressed`);
});
buttonB.addEventListener("release", (e) => {
console.log(`Button with id: ${e.target.id} was released`);
});
buttonC.addEventListener("up", (e) => {
console.log(
`Button ${e.target.innerHTML} released and lifted from the button`,
);
});
buttonD.addEventListener("down", (e) => {
console.log(
`Button ${e.target.innerHTML} was pressed directly on the button`,
);
});
import {
Button,
ButtonPressEvent,
ButtonReleaseEvent,
} from "xyba-elements/react";
export default () => {
const handlePress = (e: ButtonPressEvent) => {
console.log(`Button with id: ${e.target.id} was pressed`);
};
const handleRelease = (e: ButtonReleaseEvent) => {
console.log(`Button with id: ${e.target.id} was released`);
};
const handleUp = (e: ButtonUpEvent) => {
console.log(
`Button ${e.target.innerHTML} released and lifted from the button`,
);
};
const handleDown = (e: ButtonDownEvent) => {
console.log(
`Button ${e.target.innerHTML} was pressed directly on the button`,
);
};
return (
<>
<Button onPress={handlePress}>A</Button>
<Button onRelease={handleRelease}>B</Button>
<Button onUp={handleUp}>C</Button>
<Button onDown={handleDown}>D</Button>
</>
);
};
Read state in game loop
It's also possible to read the current state instead of handling events
- html
- javascript
- React
<xyba-button id="game-loop-demo">X</xyba-button>
function startGameLoop(update, fps = 60) {
let interval = 1000 / fps;
let lastTime = 0;
let rafId = null;
let running = true;
const loop = (currentTime) => {
if (!lastTime) {
lastTime = currentTime;
}
const deltaTime = currentTime - lastTime;
if (deltaTime >= interval) {
update(deltaTime);
lastTime = currentTime - (deltaTime % interval);
}
if (running) {
rafId = requestAnimationFrame(loop);
}
};
rafId = requestAnimationFrame(loop);
return () => {
running = false;
if (rafId) {
cancelAnimationFrame(rafId);
}
};
}
const buttonElement = document.getElementById("game-loop-demo");
const update = (deltaTime) => {
if (buttonElement.pressed) {
console.log(`Button element value: ${buttonElement.value}`); // 1.
}
};
const stopGameLoop = startGameLoop(update, 30); // Set FPS to 30
import React, { useEffect, useRef } from "react";
import { Button, ButtonElement } from "xyba-elements/react";
function startGameLoop(
update: (deltaTime: number) => void,
fps: number = 60,
): () => void {
const interval = 1000 / fps;
let lastTime = 0;
let rafId: number | null = null;
let running = true;
const loop = (currentTime: number) => {
if (!lastTime) {
lastTime = currentTime;
}
const deltaTime = currentTime - lastTime;
if (deltaTime >= interval) {
update(deltaTime);
lastTime = currentTime - (deltaTime % interval);
}
if (running) {
rafId = requestAnimationFrame(loop);
}
};
rafId = requestAnimationFrame(loop);
return () => {
running = false;
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
};
}
const App = () => {
const buttonRef = useRef<ButtonElement>(null);
useEffect(() => {
const update = () => {
const buttonElement = buttonRef.current;
if (buttonElement && buttonElement.pressed) {
console.log(`Button element value: ${buttonElement.value}`); // 1.
}
};
const stopGameLoop = startGameLoop(update, 30); // Set FPS to 30
return () => {
stopGameLoop();
};
}, []);
return <Button ref={buttonRef} trail />;
};
Properties / Attributes
property | attribute | type | default | description | readonly |
---|---|---|---|---|---|
value | value | number | 0 | The value of pressure (either 0 or 1) | |
disabled | disabled | boolean | false | If the button is disabled | |
pressed | pressed | boolean | false | If the button is pressed | |
touched | touched | boolean | false | If the button is touched | |
relatesTo | relates-to | string | undefined | The element querySelector queries it relates to | ||
capture | capture | boolean | false | The element querySelector queries it relates to | |
ignoreCapture | ignore-capture | boolean | string | undefined | The element querySelector queries it should ignore pointer capture from. or boolean to ignore-all | ||
noStyle | no-style | boolean | false | Whether default style should be in use | |
toggle | toggle | boolean | false | ||
movement | { x: number; y: number } | The movement of the pad since last time it was requested. | readonly | ||
onpress | onpress | ButtonPressEvent | The event attribute / property for the `press` event. | ||
onrelease | onrelease | ButtonReleaseEvent | The event attribute / property for the `release` event. | ||
ondown | ondown | ButtonDownEvent | The event attribute / property for the `down` event. | ||
onup | onup | ButtonUpEvent | The event attribute / property for the `up` event. |
CSS Slots
name | description |
---|---|
The default button content. | |
base | The base element of the button element. |
image | The button image (fills container). |
image-pressed | The button image when pressed (fills container). |
Events
name | description | type |
---|---|---|
press | Dispatched when the button is pressed. | ButtonPressEvent |
release | Dispatched when the button is released. | ButtonReleaseEvent |
change | Dispatched when the button pressed state changes. | ChangeEvent |
down | Dispatched when the button is pressed down. | ButtonDownEvent |
up | Dispatched when the button is released. | ButtonUpEvent |
movement | Dispatched when the button is released. | ButtonMovementEvent |
CSS Custom Properties
name | description | default |
---|---|---|
--xyba-button-color | The color inherited used for inheritance | |
--xyba-button-size | The size of the button | |
--xyba-button-background | The background when not pressed | |
--xyba-button-background-active | The background when pressed | |
--xyba-button-backdrop-filter | The backdrop filter used for the button | |
--xyba-button-border-width | The border width used for the button | |
--xyba-button-border-color | The border color used for the button | |
--xyba-button-border-color-active | The border color used for the button when pressed | |
--xyba-button-border-style | The border style used for the button | |
--xyba-button-text-color | The text color specifically for the button text or icon | |
--xyba-button-text-color-active | The text color specifically for the button text or icon when pressed |