Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions packages/radio/docs/styling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# Radio Styling

The radio component provides a comprehensive styling API through CSS custom properties (tokens).

## Styling API

### Label Tokens

Customize the appearance of the label text:

```css
--w-c-radio-label-font-size: var(--w-font-size-m);
--w-c-radio-label-line-height: var(--w-line-height-m);
--w-c-radio-label-color: currentColor;
--w-c-radio-label-color-disabled: var(--w-s-color-text-disabled);
```

### Control Tokens

Customize the radio control (the circle itself):

```css
--w-c-radio-size: 2rem;
--w-c-radio-gap: 8px;
--w-c-radio-radius: 50%;
--w-c-radio-border-width: 1px;
--w-c-radio-checked-border-width: 0.6rem;
```

### Color Tokens - Default State

```css
--w-c-radio-bg: var(--w-s-color-background);
--w-c-radio-border-color: var(--w-s-color-border-strong);
```

### Color Tokens - Checked State

```css
--w-c-radio-border-color-checked: var(--w-s-color-border-selected);
```

### Color Tokens - Invalid State

```css
--w-c-radio-border-color-invalid: var(--w-s-color-border-negative);
```

### Color Tokens - Disabled State

```css
--w-c-radio-border-color-disabled: var(--w-s-color-border-disabled);
--w-c-radio-bg-disabled: var(--w-s-color-background-disabled-subtle);
```

### Focus Tokens

```css
--w-c-radio-outline-width: 2px;
--w-c-radio-outline-color: var(--w-s-color-border-focus);
--w-c-radio-outline-offset: var(--w-outline-offset, 1px);
```

### Cursor Tokens

```css
--w-c-radio-cursor: pointer;
--w-c-radio-cursor-disabled: not-allowed;
```

### Animation Tokens

```css
--w-c-radio-transition: border-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
border-width 150ms cubic-bezier(0.4, 0, 0.2, 1),
background-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
```

## Parts

For advanced styling needs beyond tokens, you can target internal elements using CSS parts:

- `::part(base)` - The wrapper element (contains control and label)
- `::part(control)` - The visual radio control (the circle)
- `::part(label)` - The label content wrapper

```css
w-radio::part(control) {
/* Custom styling for the radio circle */
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

w-radio::part(label) {
/* Custom styling for label text */
text-transform: uppercase;
}
```

## Example Usage

### Customizing Size

```css
w-radio {
--w-c-radio-size: 2.4rem;
--w-c-radio-gap: 12px;
}
```

### Customizing Colors

```css
w-radio {
--w-c-radio-border-color-checked: var(--w-s-color-border-success);
}
```

### Customizing Border Radius

```css
/* Square radio buttons */
w-radio {
--w-c-radio-radius: 4px;
}

/* Keep it circular (default) */
w-radio {
--w-c-radio-radius: 50%;
}
```

### Customizing Checked Appearance

```css
/* Thicker border when checked */
w-radio {
--w-c-radio-checked-border-width: 0.8rem;
}

/* Or thinner */
w-radio {
--w-c-radio-checked-border-width: 0.4rem;
}
```

### Customizing Focus Outline

```css
w-radio {
--w-c-radio-outline-width: 3px;
--w-c-radio-outline-color: var(--w-s-color-border-info);
--w-c-radio-outline-offset: 2px;
}
```

### Disabling Transitions

```css
w-radio {
--w-c-radio-transition: none;
}
```

### Using Parts for Advanced Styling

```css
/* Add a subtle shadow to the radio circle */
w-radio::part(control) {
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
}

/* Style the label with custom spacing */
w-radio::part(label) {
padding-left: 4px;
font-weight: 500;
}
```

## Label Content

Like checkbox, radio labels accept rich HTML content via the default slot:

```html
<w-radio name="terms" value="accept">
I agree to the <a href="/terms">terms and conditions</a>
</w-radio>

<w-radio name="important" value="yes">
<strong>Important:</strong> Check this option
</w-radio>
```

Style the label content directly or use `::part(label)` to target the wrapper.

## Accessibility Note

Radio uses a custom implementation with `role="radio"` via ElementInternals rather than a native `<input type="radio">`. This provides full control over styling and behavior while maintaining proper ARIA semantics and form integration.

## High Contrast Mode Support

Radio includes automatic color adjustments for users with high contrast / forced colors mode enabled, ensuring the component remains usable and visible in all accessibility contexts.
6 changes: 1 addition & 5 deletions packages/radio/radio-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@

export const styles = css`
:host {
display: inline-flex;
align-items: center;
gap: var(--_gap);

--_gap: var(--w-c-radio-gap, 8px);

--_size: var(--w-c-radio-size, 2rem);
Expand Down Expand Up @@ -89,7 +85,7 @@
--w-c-radio-cursor: var(--w-c-radio-cursor-disabled, not-allowed);
}

.wrapper {
[part='base'] {

Check failure on line 88 in packages/radio/radio-styles.ts

View workflow job for this annotation

GitHub Actions / build

Replace `'base'` with `"base"`
display: inline-flex;
align-items: center;
gap: var(--_gap);
Expand Down
12 changes: 4 additions & 8 deletions packages/radio/radio.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,9 @@

await radio.updateComplete;

const control = radio.shadowRoot?.querySelector(
".control",
) as HTMLElement | null;
const control = radio.shadowRoot?.querySelector('[part="control"]') as HTMLElement | null;

Check failure on line 80 in packages/radio/radio.test.ts

View workflow job for this annotation

GitHub Actions / build

Replace `'[part="control"]'` with `⏎↹↹'[part="control"]',⏎↹`
if (!control) {
throw new Error("Expected radio control element to exist");
throw new Error('Expected radio control element to exist');

Check failure on line 82 in packages/radio/radio.test.ts

View workflow job for this annotation

GitHub Actions / build

Replace `'Expected·radio·control·element·to·exist'` with `"Expected·radio·control·element·to·exist"`
}

radio.click();
Expand All @@ -105,11 +103,9 @@

await radio.updateComplete;

const control = radio.shadowRoot?.querySelector(
".control",
) as HTMLElement | null;
const control = radio.shadowRoot?.querySelector('[part="control"]') as HTMLElement | null;

Check failure on line 106 in packages/radio/radio.test.ts

View workflow job for this annotation

GitHub Actions / build

Replace `'[part="control"]'` with `⏎↹↹'[part="control"]',⏎↹`
if (!control) {
throw new Error("Expected radio control element to exist");
throw new Error('Expected radio control element to exist');

Check failure on line 108 in packages/radio/radio.test.ts

View workflow job for this annotation

GitHub Actions / build

Replace `'Expected·radio·control·element·to·exist'` with `"Expected·radio·control·element·to·exist"`
}

const bgSwatch = document.createElement("div");
Expand Down
16 changes: 13 additions & 3 deletions packages/radio/radio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ import { styles as radioStyles } from "./radio-styles";

/**
* @parent w-radio-group
*
* ## Implementation Note
* This component uses a custom implementation with role="radio" via ElementInternals
* rather than a native <input type="radio">. This provides full styling control while
* maintaining proper ARIA semantics and form integration.
*
* TODO: Checkbox uses a hidden native <input type="checkbox"> for historical reasons,
* but could be simplified to match this cleaner ElementInternals-only approach.
* Consider aligning both components in a future major version after assessing
* backwards compatibility implications.
*/
export class WarpRadio extends FormControlMixin(LitElement) {
static styles = [hostStyles, reset, radioStyles];
Expand Down Expand Up @@ -400,9 +410,9 @@ export class WarpRadio extends FormControlMixin(LitElement) {

render() {
return html`
<div class="wrapper" tabindex="${this._internalTabIndex}">
<div part="control" class="control"></div>
<slot part="label" class="label"></slot>
<div part="base" tabindex="${this._internalTabIndex}">
<div part="control"></div>
<slot part="label"></slot>
</div>
`;
}
Expand Down
Loading