Agent Wranglers

UXD-5Recommended

Give yes and no the same weight

Show every option of a decision as the same kind of control, on the same screen, with the same size, font and colour contrast. No option hides behind a link, a second layer or a scroll.

How to do it

A bright "Accept" beside a grey "settings" link steers people without a word of untruth. Regulators in the EU and the FTC in the US both name it as deceptive design. Use the same button component for every option, and let only the label differ. Keep the options in a fixed order, so the user's habit does not get used against them. Put reject on the first layer of a consent banner, as Store nothing extra without a yes requires. Paid extras start unticked, as Never add paid extras through a pre-ticked box says.

Examples

TypeScript example

import { expect, test } from '@playwright/test'

const MAX_SIZE_DIFFERENCE_PX = 4

test('consent options have equal weight', async ({ page }) => {
  await page.goto('https://shop.example.com/')
  const options = page.locator('[data-decision="consent"] [data-option]')
  const looks = await options.evaluateAll(nodes => nodes.map(node => {
    const style = getComputedStyle(node)
    const box = node.getBoundingClientRect()
    return { tag: node.tagName, width: box.width, height: box.height, font: `${style.fontSize} ${style.fontWeight}`, color: style.color, background: style.backgroundColor, visible: box.bottom <= innerHeight }
  }))
  for (const look of looks) {
    expect(look.tag).toBe(looks[0].tag)
    expect(Math.abs(look.width - looks[0].width)).toBeLessThanOrEqual(MAX_SIZE_DIFFERENCE_PX)
    expect(Math.abs(look.height - looks[0].height)).toBeLessThanOrEqual(MAX_SIZE_DIFFERENCE_PX)
    expect([look.font, look.color, look.background]).toEqual([looks[0].font, looks[0].color, looks[0].background])
    expect(look.visible).toBe(true)
  }
})

Python example

from playwright.sync_api import Page

MAX_SIZE_DIFFERENCE_PX = 4
READ_LOOKS = """nodes => nodes.map(node => {
  const style = getComputedStyle(node), box = node.getBoundingClientRect()
  return { tag: node.tagName, width: box.width, height: box.height, font: style.fontSize + ' ' + style.fontWeight,
           color: style.color, background: style.backgroundColor, visible: box.bottom <= innerHeight }
})"""

def test_consent_options_have_equal_weight(page: Page) -> None:
    page.goto("https://shop.example.com/")
    looks = page.locator('[data-decision="consent"] [data-option]').evaluate_all(READ_LOOKS)
    first = looks[0]
    for look in looks:
        assert look["tag"] == first["tag"]
        assert abs(look["width"] - first["width"]) <= MAX_SIZE_DIFFERENCE_PX
        assert abs(look["height"] - first["height"]) <= MAX_SIZE_DIFFERENCE_PX
        assert (look["font"], look["color"], look["background"]) == (first["font"], first["color"], first["background"])
        assert look["visible"]

How to check

A browser test opens each decision dialog and reads every option. All options are the same element type, and their widths and heights differ by at most 4 pixels. Their font size, font weight and text contrast ratio are equal. All of them are visible without scrolling.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/ux-design/deceptive-design/equal-choices">Agent Wranglers UX and deceptive design playbook</a>. In code you write, add a comment next to it with the same link.