Files
WRNexusJS/packages/ui/components/button.wrn
T
ClintchizandClaude Opus 5 b34930f8f6 Stop link buttons claiming a pressed state, and lift tab contrast
A Button rendered as a link emitted aria-pressed="", which is not allowed
on a link and fails an accessibility check on every page that uses one
for a secondary action. A link has no pressed state, so the attribute is
gone from that branch; a real button still carries it.

An unselected tab used the muted text colour, which does not reach 4.5:1
against the tab strip at this size. It now blends back toward the text
colour, and the selected tab still reads as selected through its surface,
accent and shadow.

The visual contract manifest is regenerated to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 16:29:05 +05:30

483 lines
14 KiB
Plaintext

component Button {
outputs {
click(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
focus(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
blur(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
label: string = "Button"
loadingLabel: string = "Loading…"
description: string = ""
as: string = ""
href: string = ""
target: string = ""
rel: string = ""
type: string = "button"
variant: string = "default"
color: string = "primary"
size: string = "default"
disabled: boolean = false
loading: boolean = false
pill: boolean = false
fullWidth: boolean = false
icon: string = ""
iconPosition: string = "start"
ariaLabel: string = ""
ariaPressed: string = ""
ariaExpanded: string = ""
ariaControls: string = ""
title: string = ""
tooltipPlacement: string = "top"
autofocus: boolean = false
controlClass: string = ""
class: string = ""
}
view {
<span
class="wrn-action {fullWidth ? 'w-full' : ''} {class}"
data-full-width="{fullWidth ? 'true' : 'false'}"
>
{#if as === "a" || (as === "" && href)}
<!-- No aria-pressed here: it states a toggle button's state, and a link has none. -->
<a
{...attrs}
href="{disabled || loading ? '' : href}"
target="{target}"
rel="{rel}"
title="{title}"
aria-label="{ariaLabel || ((size === 'icon' || size === 'icon-xs' || size === 'icon-sm' || size === 'icon-lg') ? label : '')}"
aria-disabled="{disabled || loading}"
aria-busy="{loading}"
aria-expanded="{ariaExpanded}"
aria-controls="{ariaControls}"
tabindex="{disabled || loading ? '-1' : '0'}"
class="wrn-btn wrn-btn--variant-{variant} wrn-btn--color-{color} wrn-btn--size-{size} {pill ? 'wrn-btn--pill' : ''} {description ? 'wrn-btn--with-description' : ''} {fullWidth ? 'w-full' : ''} {controlClass}"
>
{#if loading}
<span
class="wrn-spinner wrn-spinner--inline"
aria-hidden="true"
>
</span>
{/if}
{#if icon && !loading && iconPosition === "start"}
<span
class="wrn-btn__icon {icon}"
aria-hidden="true"
>
</span>
{/if}
<span
class="wrn-btn__copy {(size === 'icon' || size === 'icon-xs' || size === 'icon-sm' || size === 'icon-lg') ? 'wrn-visually-hidden' : ''}"
>
<span
class="wrn-btn__label"
>
<slot>{loading ? loadingLabel : label}</slot>
</span>
{#if description && !loading}
<span
class="wrn-btn__description"
>
{description}
</span>
{/if}
</span>
{#if icon && !loading && iconPosition === "end"}
<span
class="wrn-btn__icon {icon}"
aria-hidden="true"
>
</span>
{/if}
{#if (size === "icon" || size === "icon-xs" || size === "icon-sm" || size === "icon-lg") && (ariaLabel || label)}
<span
class="wrn-btn__tooltip wrn-btn__tooltip--{tooltipPlacement}"
data-placement="{tooltipPlacement}"
role="tooltip"
aria-hidden="true"
>
{ariaLabel || label}
</span>
{/if}
</a>
{:else}
<button
{...attrs}
type="{type}"
title="{title}"
autofocus="{autofocus}"
disabled="{disabled || loading}"
aria-label="{ariaLabel || ((size === 'icon' || size === 'icon-xs' || size === 'icon-sm' || size === 'icon-lg') ? label : '')}"
aria-busy="{loading}"
aria-pressed="{ariaPressed}"
aria-expanded="{ariaExpanded}"
aria-controls="{ariaControls}"
class="wrn-btn wrn-btn--variant-{variant} wrn-btn--color-{color} wrn-btn--size-{size} {pill ? 'wrn-btn--pill' : ''} {description ? 'wrn-btn--with-description' : ''} {fullWidth ? 'w-full' : ''} {controlClass}"
>
{#if loading}
<span
class="wrn-spinner wrn-spinner--inline"
aria-hidden="true"
>
</span>
{/if}
{#if icon && !loading && iconPosition === "start"}
<span
class="wrn-btn__icon {icon}"
aria-hidden="true"
>
</span>
{/if}
<span
class="wrn-btn__copy {(size === 'icon' || size === 'icon-xs' || size === 'icon-sm' || size === 'icon-lg') ? 'wrn-visually-hidden' : ''}"
>
<span
class="wrn-btn__label"
>
<slot>{loading ? loadingLabel : label}</slot>
</span>
{#if description && !loading}
<span
class="wrn-btn__description"
>
{description}
</span>
{/if}
</span>
{#if icon && !loading && iconPosition === "end"}
<span
class="wrn-btn__icon {icon}"
aria-hidden="true"
>
</span>
{/if}
{#if (size === "icon" || size === "icon-xs" || size === "icon-sm" || size === "icon-lg") && (ariaLabel || label)}
<span
class="wrn-btn__tooltip wrn-btn__tooltip--{tooltipPlacement}"
data-placement="{tooltipPlacement}"
role="tooltip"
aria-hidden="true"
>
{ariaLabel || label}
</span>
{/if}
</button>
{/if}
</span>
}
style {
.wrn-spinner {
display: inline-block;
width: 1.15rem;
height: 1.15rem;
border: 2px solid var(--wrn-color-border);
border-top-color: var(--wrn-color-primary);
border-radius: 999px;
animation: wrn-spin 0.7s linear infinite;
}
.wrn-spinner--inline {
width: 1rem;
height: 1rem;
}
@keyframes wrn-spin {
to {
transform: rotate(360deg);
}
}
/* --- Button --------------------------------------------------------------- */
.wrn-btn {
position: relative;
--wrn-btn-color: var(--wrn-color-primary);
--wrn-btn-contrast: var(--wrn-color-primary-contrast);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font: inherit;
font-size: 0.875rem;
font-weight: 600;
line-height: 1;
border: 1px solid transparent;
border-radius: var(--wrn-radius-sm);
padding: 0 0.75rem;
cursor: pointer;
text-decoration: none;
min-height: 2rem;
transition:
color var(--wrn-motion-base) var(--wrn-ease-standard),
border-color var(--wrn-motion-base) var(--wrn-ease-standard),
background var(--wrn-motion-base) var(--wrn-ease-standard),
box-shadow var(--wrn-motion-base) var(--wrn-ease-standard),
transform var(--wrn-motion-base) var(--wrn-ease-emphasized);
}
.wrn-btn:active {
transform: scale(0.98);
}
.wrn-btn:focus-visible {
outline: 2px solid var(--wrn-color-primary);
outline-offset: 2px;
}
.wrn-btn--color-primary {
--wrn-btn-color: var(--wrn-color-primary);
--wrn-btn-contrast: var(--wrn-color-primary-contrast);
}
.wrn-btn--color-secondary {
--wrn-btn-color: var(--wrn-color-muted);
--wrn-btn-contrast: var(--wrn-color-bg);
}
.wrn-btn--color-success {
--wrn-btn-color: var(--wrn-color-success);
--wrn-btn-contrast: var(--wrn-color-primary-contrast);
}
.wrn-btn--color-warning {
--wrn-btn-color: var(--wrn-color-warning);
--wrn-btn-contrast: var(--wrn-color-bg);
}
.wrn-btn--color-danger {
--wrn-btn-color: var(--wrn-color-danger);
--wrn-btn-contrast: var(--wrn-color-primary-contrast);
}
.wrn-btn--color-info {
--wrn-btn-color: var(--wrn-color-info);
--wrn-btn-contrast: var(--wrn-color-primary-contrast);
}
.wrn-btn--variant-default,
.wrn-btn--variant-solid,
.wrn-btn--default,
.wrn-btn--solid,
.wrn-btn--primary {
color: var(--wrn-btn-contrast);
background: var(--wrn-btn-color);
box-shadow:
0 1px 2px color-mix(in srgb, black 20%, transparent),
0 0 0 1px color-mix(in srgb, var(--wrn-btn-color) 72%, transparent);
}
.wrn-btn--variant-default:hover,
.wrn-btn--variant-solid:hover,
.wrn-btn--default:hover,
.wrn-btn--solid:hover,
.wrn-btn--primary:hover {
background: color-mix(in srgb, var(--wrn-btn-color) 84%, black);
transform: translateY(-1px);
box-shadow:
0 0.35rem 0.9rem color-mix(in srgb, var(--wrn-btn-color) 20%, transparent),
0 0 0 1px color-mix(in srgb, var(--wrn-btn-color) 82%, transparent);
}
.wrn-btn--variant-outline,
.wrn-btn--outline {
color: var(--wrn-btn-color);
background: transparent;
border-color: color-mix(in srgb, var(--wrn-btn-color) 55%, var(--wrn-color-border));
}
.wrn-btn--variant-outline:hover,
.wrn-btn--outline:hover {
background: color-mix(in srgb, var(--wrn-btn-color) 10%, transparent);
border-color: var(--wrn-btn-color);
}
.wrn-btn--variant-destructive,
.wrn-btn--danger,
.wrn-btn--destructive {
color: var(--wrn-btn-color);
background: color-mix(in srgb, var(--wrn-btn-color) 12%, transparent);
border-color: color-mix(in srgb, var(--wrn-btn-color) 28%, transparent);
}
.wrn-btn--danger,
.wrn-btn--destructive {
--wrn-btn-color: var(--wrn-color-danger);
}
.wrn-btn--variant-destructive:hover,
.wrn-btn--danger:hover,
.wrn-btn--destructive:hover {
background: color-mix(in srgb, var(--wrn-btn-color) 20%, transparent);
}
.wrn-btn--variant-ghost,
.wrn-btn--ghost {
background: transparent;
color: var(--wrn-btn-color);
}
.wrn-btn--variant-ghost:hover,
.wrn-btn--ghost:hover {
background: color-mix(in srgb, var(--wrn-btn-color) 10%, transparent);
}
.wrn-btn--variant-secondary,
.wrn-btn--secondary {
color: var(--wrn-color-text);
background: color-mix(in srgb, var(--wrn-btn-color) 10%, var(--wrn-color-surface));
border-color: color-mix(in srgb, var(--wrn-btn-color) 24%, var(--wrn-color-border));
box-shadow: var(--wrn-shadow-1);
}
.wrn-btn--variant-secondary:hover,
.wrn-btn--secondary:hover {
color: var(--wrn-btn-color);
border-color: color-mix(in srgb, var(--wrn-btn-color) 55%, var(--wrn-color-border));
background: color-mix(in srgb, var(--wrn-btn-color) 15%, var(--wrn-color-surface));
transform: translateY(-2px);
}
.wrn-btn--variant-link,
.wrn-btn--link {
min-height: auto;
padding: 0;
color: var(--wrn-btn-color);
background: transparent;
border-color: transparent;
border-radius: 0;
text-underline-offset: 0.22em;
}
.wrn-btn--variant-link:hover,
.wrn-btn--link:hover {
text-decoration: underline;
}
.wrn-btn:disabled,
.wrn-btn[aria-busy="true"] {
cursor: not-allowed;
opacity: 0.58;
transform: none;
box-shadow: none;
}
.wrn-btn--size-xs,
.wrn-btn--xs {
min-height: 1.5rem;
padding-inline: 0.5rem;
gap: 0.25rem;
font-size: 0.75rem;
}
.wrn-btn--size-sm,
.wrn-btn--sm {
min-height: 2.125rem;
padding-inline: 0.625rem;
gap: 0.25rem;
font-size: 0.8rem;
}
.wrn-btn--size-default,
.wrn-btn--md {
min-height: 2.5rem;
padding-inline: 0.75rem;
font-size: 0.875rem;
}
.wrn-btn--size-lg,
.wrn-btn--lg {
min-height: 2.75rem;
padding-inline: 0.875rem;
font-size: 0.925rem;
}
.wrn-btn--size-icon,
.wrn-btn--size-icon-xs,
.wrn-btn--size-icon-sm,
.wrn-btn--size-icon-lg,
.wrn-btn--icon,
.wrn-btn--icon-xs,
.wrn-btn--icon-sm,
.wrn-btn--icon-lg {
flex: none;
padding: 0;
}
.wrn-btn--size-icon,
.wrn-btn--icon {
width: 2rem;
height: 2rem;
}
.wrn-btn--size-icon-xs,
.wrn-btn--icon-xs {
width: 1.5rem;
min-height: 1.5rem;
}
.wrn-btn--size-icon-sm,
.wrn-btn--icon-sm {
width: 1.75rem;
min-height: 1.75rem;
}
.wrn-btn--size-icon-lg,
.wrn-btn--icon-lg {
width: 2.25rem;
min-height: 2.25rem;
}
.wrn-btn__icon {
width: 1rem;
height: 1rem;
flex: none;
}
.wrn-btn__label {
font-weight: 650;
}
.wrn-btn:hover > .wrn-btn__tooltip,
.wrn-btn:focus-visible > .wrn-btn__tooltip {
opacity: 1;
transform: translate(-50%, 0);
}
.wrn-action {
position: relative;
display: inline-flex;
}
.wrn-action[data-full-width="true"],
.wrn-action[data-full-width="true"] > .wrn-btn {
width: 100%;
}
.wrn-btn--pill {
border-radius: 999px;
}
.wrn-btn--with-description {
min-height: 3.5rem;
justify-content: flex-start;
padding: 0.6rem 0.875rem;
text-align: left;
}
.wrn-btn__copy {
display: grid;
min-width: 0;
gap: 0.15rem;
}
.wrn-btn__description {
color: currentColor;
font-size: 0.75rem;
font-weight: 450;
line-height: 1.35;
opacity: 0.74;
}
.wrn-btn__tooltip {
position: absolute;
z-index: 80;
inset-block-end: calc(100% + 0.5rem);
inset-inline-start: 50%;
width: max-content;
max-width: min(16rem, calc(100vw - 2rem));
padding: 0.4rem 0.6rem;
color: var(--wrn-color-bg);
background: var(--wrn-color-text);
border: 1px solid color-mix(in srgb, var(--wrn-color-text) 85%, var(--wrn-color-border));
border-radius: var(--wrn-radius-sm);
box-shadow: var(--wrn-shadow-2);
font-size: 0.75rem;
font-weight: 600;
line-height: 1.25;
text-align: center;
pointer-events: none;
opacity: 0;
transform: translate(-50%, 0.25rem);
transition:
opacity var(--wrn-motion-fast) var(--wrn-ease-standard),
transform var(--wrn-motion-fast) var(--wrn-ease-standard);
}
.wrn-btn__tooltip--bottom {
inset-block-start: calc(100% + 0.5rem);
inset-block-end: auto;
transform: translate(-50%, -0.25rem);
}
}
}