Affordance

Visual indication of how an element can be interacted with.

Save changes

With affordance cues — same element, same behaviour, different visual hinting

html

<div class="uif-aff-stage" data-element="button">
  <button type="button" class="uif-aff-btn uif-aff-btn--cued" style="--accent:#A8E6C1;">Save changes</button>
  <span class="uif-aff-switch uif-aff-switch--cued" style="--accent:#A8E6C1;"><i></i></span>
  <a href="#" class="uif-aff-link uif-aff-link--cued" style="--accent:#A8E6C1;">Save changes</a>
  <p class="uif-aff-caption">With affordance cues &mdash; same element, same behaviour, different visual hinting</p>
</div>
<style>
.uif-aff-stage { width: 100%; max-width: 320px; padding: 22px; background: #FAF7F2; border: 1px solid #24143022; border-radius: 12px; font: 13px system-ui, sans-serif; color: #241430aa; text-align: center; }
.uif-aff-btn, .uif-aff-switch, .uif-aff-link { display: none; }
[data-element="button"] .uif-aff-btn { display: inline-block; font: 600 14px system-ui, sans-serif; padding: 10px 18px; border-radius: 8px; cursor: pointer; color: #241430; background: transparent; border: none; }
.uif-aff-btn--cued { background: var(--accent); border: 1px solid transparent; box-shadow: 0 3px 0 rgba(36,20,48,.25); }
[data-element="toggle-switch"] .uif-aff-switch { display: inline-block; width: 44px; height: 24px; border-radius: 999px; background: #24143022; position: relative; }
.uif-aff-switch--cued { background: var(--accent); box-shadow: inset 0 1px 3px rgba(36,20,48,.3); }
.uif-aff-switch i { position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; border-radius: 999px; background: #fff; display: block; }
.uif-aff-switch--cued i { left: 22px; box-shadow: 0 1px 2px rgba(36,20,48,.4); }
[data-element="link"] .uif-aff-link { display: inline-block; color: #241430; text-decoration: none; font-weight: 600; }
.uif-aff-link--cued { color: var(--accent); text-decoration: underline; }
.uif-aff-caption { margin: 16px 0 0; }
</style>

tailwind

<div class="w-full max-w-[320px] p-[22px] bg-[#FAF7F2] border border-[#24143022] rounded-xl font-sans text-sm text-[#241430]/70 text-center" data-element="button">
  <button type="button" class="tw-aff-btn tw-aff-btn-cued hidden font-semibold px-4.5 py-2.5 rounded-lg cursor-pointer text-[#241430] bg-transparent border-0" style="--accent:#A8E6C1;">Save changes</button>
  <span class="tw-aff-switch tw-aff-switch-cued hidden w-11 h-6 rounded-full bg-[#24143022] relative" style="--accent:#A8E6C1;"><i class="absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white block"></i></span>
  <a href="#" class="tw-aff-link tw-aff-link-cued hidden text-[#241430] no-underline font-semibold" style="--accent:#A8E6C1;">Save changes</a>
  <p class="mt-4">With affordance cues &mdash; same element, same behaviour, different visual hinting</p>
</div>
<style>
[data-element="button"] .tw-aff-btn { display: inline-block; }
.tw-aff-btn-cued { background: var(--accent); box-shadow: 0 3px 0 rgba(36,20,48,.25); }
[data-element="toggle-switch"] .tw-aff-switch { display: inline-block; }
.tw-aff-switch-cued { background: var(--accent); box-shadow: inset 0 1px 3px rgba(36,20,48,.3); }
.tw-aff-switch-cued i { margin-left: 20px; box-shadow: 0 1px 2px rgba(36,20,48,.4); }
[data-element="link"] .tw-aff-link { display: inline-block; }
.tw-aff-link-cued { color: var(--accent); text-decoration: underline; }
</style>

react

export function Affordance({
  cues = true,
  elementType = "button",
  label = "Save changes",
  accent = "#A8E6C1",
}) {
  return (
    <div style={{ width: "100%", maxWidth: 320, padding: 22, background: "#FAF7F2", border: "1px solid #24143022", borderRadius: 12, fontFamily: "system-ui, sans-serif", fontSize: 13, color: "#241430aa", textAlign: "center" }}>
      {elementType === "button" && (
        <button type="button" style={{ font: "600 14px system-ui, sans-serif", padding: "10px 18px", borderRadius: 8, cursor: "pointer", color: "#241430", background: cues ? accent : "transparent", border: "none", boxShadow: cues ? "0 3px 0 rgba(36,20,48,.25)" : "none" }}>{label}</button>
      )}
      {elementType === "toggle-switch" && (
        <span style={{ display: "inline-block", width: 44, height: 24, borderRadius: 999, background: cues ? accent : "#24143022", position: "relative", boxShadow: cues ? "inset 0 1px 3px rgba(36,20,48,.3)" : "none" }}>
          <i style={{ position: "absolute", top: 2, left: cues ? 22 : 2, width: 20, height: 20, borderRadius: 999, background: "#fff", display: "block", boxShadow: cues ? "0 1px 2px rgba(36,20,48,.4)" : "none" }} />
        </span>
      )}
      {elementType === "link" && (
        <a href="#" style={{ color: cues ? accent : "#241430", textDecoration: cues ? "underline" : "none", fontWeight: 600 }}>{label}</a>
      )}
      <p style={{ margin: "16px 0 0" }}>{cues ? "With" : "Without"} affordance cues &mdash; same element, same behaviour, different visual hinting</p>
    </div>
  );
}