A foundational overview of how to build color-adaptive, responsive, and accessible `<button>` components.


![Adam Argyle](https://web.dev/images/authors/adamargyle.jpg) Adam Argyle [X](https://twitter.com/argyleink) [GitHub](https://github.com/argyleink) [Homepage](https://nerdy.dev)

<br />

In this post I want to share my thoughts on how to build a color-adaptive,
responsive, and accessible `<button>` element.
[Try the demo](https://gui-challenges.web.app/buttons/dist/) and [view the
source](https://github.com/argyleink/gui-challenges)
Buttons are interacted with via keyboard and mouse in the light and dark themes.

If you prefer video, here's a YouTube version of this post:

[Video](https://www.youtube.com/watch?v=aJNq-b1zlog)

## Overview


Browser Support

- ![Chrome: 1.](data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='-10 -10 276 276'%3E%3ClinearGradient id='a' x1='145' x2='34' y1='253' y2='61' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%231e8e3e'/%3E%3Cstop offset='1' stop-color='%2334a853'/%3E%3C/linearGradient%3E%3ClinearGradient id='b' x1='111' x2='222' y1='254' y2='62' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%23fcc934'/%3E%3Cstop offset='1' stop-color='%23fbbc04'/%3E%3C/linearGradient%3E%3ClinearGradient id='c' x1='17' x2='239' y1='80' y2='80' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%23d93025'/%3E%3Cstop offset='1' stop-color='%23ea4335'/%3E%3C/linearGradient%3E%3Ccircle cx='128' cy='128' r='64' fill='%23fff'/%3E%3Cpath fill='url(%23a)' d='M96 183a64 64 0 0 1-23-23L17 64a128 128 0 0 0 111 192l55-96a64 64 0 0 1-87 23Z'/%3E%3Cpath fill='url(%23b)' d='M192 128a64 64 0 0 1-9 32l-55 96A128 128 0 0 0 239 64H128a64 64 0 0 1 64 64Z'/%3E%3Ccircle cx='128' cy='128' r='52' fill='%231a73e8'/%3E%3Cpath fill='url(%23c)' d='M96 73a64 64 0 0 1 32-9h111a128 128 0 0 0-222 0l56 96a64 64 0 0 1 23-87Z'/%3E%3C/svg%3E)
- ![Edge: 12.](data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='24' height='24' viewBox='0 0 27600 27600'%3E%3ClinearGradient id='A' gradientUnits='userSpaceOnUse'/%3E%3ClinearGradient id='B' x1='6870' x2='24704' y1='18705' y2='18705' xlink:href='%23A'%3E%3Cstop offset='0' stop-color='%230c59a4'/%3E%3Cstop offset='1' stop-color='%23114a8b'/%3E%3C/linearGradient%3E%3ClinearGradient id='C' x1='16272' x2='5133' y1='10968' y2='23102' xlink:href='%23A'%3E%3Cstop offset='0' stop-color='%231b9de2'/%3E%3Cstop offset='.16' stop-color='%231595df'/%3E%3Cstop offset='.67' stop-color='%230680d7'/%3E%3Cstop offset='1' stop-color='%230078d4'/%3E%3C/linearGradient%3E%3CradialGradient id='D' cx='16720' cy='18747' r='9538' xlink:href='%23A'%3E%3Cstop offset='.72' stop-opacity='0'/%3E%3Cstop offset='.95' stop-opacity='.53'/%3E%3Cstop offset='1'/%3E%3C/radialGradient%3E%3CradialGradient id='E' cx='7130' cy='19866' r='14324' gradientTransform='matrix(.14843 -.98892 .79688 .1196 -8759 25542)' xlink:href='%23A'%3E%3Cstop offset='.76' stop-opacity='0'/%3E%3Cstop offset='.95' stop-opacity='.5'/%3E%3Cstop offset='1'/%3E%3C/radialGradient%3E%3CradialGradient id='F' cx='2523' cy='4680' r='20243' gradientTransform='matrix(-.03715 .99931 -2.12836 -.07913 13579 3530)' xlink:href='%23A'%3E%3Cstop offset='0' stop-color='%2335c1f1'/%3E%3Cstop offset='.11' stop-color='%2334c1ed'/%3E%3Cstop offset='.23' stop-color='%232fc2df'/%3E%3Cstop offset='.31' stop-color='%232bc3d2'/%3E%3Cstop offset='.67' stop-color='%2336c752'/%3E%3C/radialGradient%3E%3CradialGradient id='G' cx='24247' cy='7758' r='9734' gradientTransform='matrix(.28109 .95968 -.78353 .22949 24510 -16292)' xlink:href='%23A'%3E%3Cstop offset='0' stop-color='%2366eb6e'/%3E%3Cstop offset='1' stop-color='%2366eb6e' stop-opacity='0'/%3E%3C/radialGradient%3E%3Cpath id='H' d='M24105 20053a9345 9345 0 01-1053 472 10202 10202 0 01-3590 646c-4732 0-8855-3255-8855-7432 0-1175 680-2193 1643-2729-4280 180-5380 4640-5380 7253 0 7387 6810 8137 8276 8137 791 0 1984-230 2704-456l130-44a12834 12834 0 006660-5282c220-350-168-757-535-565z'/%3E%3Cpath id='I' d='M11571 25141a7913 7913 0 01-2273-2137 8145 8145 0 01-1514-4740 8093 8093 0 013093-6395 8082 8082 0 011373-859c312-148 846-414 1554-404a3236 3236 0 012569 1297 3184 3184 0 01636 1866c0-21 2446-7960-8005-7960-4390 0-8004 4166-8004 7820 0 2319 538 4170 1212 5604a12833 12833 0 007684 6757 12795 12795 0 003908 610c1414 0 2774-233 4045-656a7575 7575 0 01-6278-803z'/%3E%3Cpath id='J' d='M16231 15886c-80 105-330 250-330 566 0 260 170 512 472 723 1438 1003 4149 868 4156 868a5954 5954 0 003027-839 6147 6147 0 001133-850 6180 6180 0 001910-4437c26-2242-796-3732-1133-4392-2120-4141-6694-6525-11668-6525-7011 0-12703 5635-12798 12620 47-3654 3679-6605 7996-6605 350 0 2346 34 4200 1007 1634 858 2490 1894 3086 2921 618 1067 728 2415 728 2952s-271 1333-780 1990z'/%3E%3Cuse fill='url(%23B)' xlink:href='%23H'/%3E%3Cuse fill='url(%23D)' opacity='.35' xlink:href='%23H'/%3E%3Cuse fill='url(%23C)' xlink:href='%23I'/%3E%3Cuse fill='url(%23E)' opacity='.4' xlink:href='%23I'/%3E%3Cuse fill='url(%23F)' xlink:href='%23J'/%3E%3Cuse fill='url(%23G)' xlink:href='%23J'/%3E%3C/svg%3E)
- ![Firefox: 1.](data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 512 512'%3E%3Cdefs%3E%3CradialGradient id='ff-b' cx='428.5' cy='55.1' r='501' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='.1' stop-color='%23ffbd4f'/%3E%3Cstop offset='.2' stop-color='%23ffac31'/%3E%3Cstop offset='.3' stop-color='%23ff9d17'/%3E%3Cstop offset='.3' stop-color='%23ff980e'/%3E%3Cstop offset='.4' stop-color='%23ff563b'/%3E%3Cstop offset='.5' stop-color='%23ff3750'/%3E%3Cstop offset='.7' stop-color='%23f5156c'/%3E%3Cstop offset='.8' stop-color='%23eb0878'/%3E%3Cstop offset='.9' stop-color='%23e50080'/%3E%3C/radialGradient%3E%3CradialGradient id='ff-c' cx='245.4' cy='259.9' r='501' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='.3' stop-color='%23960e18'/%3E%3Cstop offset='.3' stop-color='%23b11927' stop-opacity='.7'/%3E%3Cstop offset='.4' stop-color='%23db293d' stop-opacity='.3'/%3E%3Cstop offset='.5' stop-color='%23f5334b' stop-opacity='.1'/%3E%3Cstop offset='.5' stop-color='%23ff3750' stop-opacity='0'/%3E%3C/radialGradient%3E%3CradialGradient id='ff-d' cx='305.8' cy='-58.6' r='363' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='.1' stop-color='%23fff44f'/%3E%3Cstop offset='.3' stop-color='%23ffdc3e'/%3E%3Cstop offset='.5' stop-color='%23ff9d12'/%3E%3Cstop offset='.5' stop-color='%23ff980e'/%3E%3C/radialGradient%3E%3CradialGradient id='ff-e' cx='190' cy='390.8' r='238.6' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='.3' stop-color='%233a8ee6'/%3E%3Cstop offset='.5' stop-color='%235c79f0'/%3E%3Cstop offset='.7' stop-color='%239059ff'/%3E%3Cstop offset='1' stop-color='%23c139e6'/%3E%3C/radialGradient%3E%3CradialGradient id='ff-f' cx='252.2' cy='201.3' r='126.5' gradientTransform='matrix(1 0 0 1 -48 31)' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='.2' stop-color='%239059ff' stop-opacity='0'/%3E%3Cstop offset='.3' stop-color='%238c4ff3' stop-opacity='.1'/%3E%3Cstop offset='.8' stop-color='%237716a8' stop-opacity='.5'/%3E%3Cstop offset='1' stop-color='%236e008b' stop-opacity='.6'/%3E%3C/radialGradient%3E%3CradialGradient id='ff-g' cx='239.1' cy='34.6' r='171.6' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%23ffe226'/%3E%3Cstop offset='.1' stop-color='%23ffdb27'/%3E%3Cstop offset='.3' stop-color='%23ffc82a'/%3E%3Cstop offset='.5' stop-color='%23ffa930'/%3E%3Cstop offset='.7' stop-color='%23ff7e37'/%3E%3Cstop offset='.8' stop-color='%23ff7139'/%3E%3C/radialGradient%3E%3CradialGradient id='ff-h' cx='374' cy='-74.3' r='732.2' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='.1' stop-color='%23fff44f'/%3E%3Cstop offset='.5' stop-color='%23ff980e'/%3E%3Cstop offset='.6' stop-color='%23ff5634'/%3E%3Cstop offset='.7' stop-color='%23ff3647'/%3E%3Cstop offset='.9' stop-color='%23e31587'/%3E%3C/radialGradient%3E%3CradialGradient id='ff-i' cx='304.6' cy='7.1' r='536.4' gradientTransform='rotate(84 303 4)' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%23fff44f'/%3E%3Cstop offset='.1' stop-color='%23ffe847'/%3E%3Cstop offset='.2' stop-color='%23ffc830'/%3E%3Cstop offset='.3' stop-color='%23ff980e'/%3E%3Cstop offset='.4' stop-color='%23ff8b16'/%3E%3Cstop offset='.5' stop-color='%23ff672a'/%3E%3Cstop offset='.6' stop-color='%23ff3647'/%3E%3Cstop offset='.7' stop-color='%23e31587'/%3E%3C/radialGradient%3E%3CradialGradient id='ff-j' cx='235' cy='98.1' r='457.1' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='.1' stop-color='%23fff44f'/%3E%3Cstop offset='.5' stop-color='%23ff980e'/%3E%3Cstop offset='.6' stop-color='%23ff5634'/%3E%3Cstop offset='.7' stop-color='%23ff3647'/%3E%3Cstop offset='.9' stop-color='%23e31587'/%3E%3C/radialGradient%3E%3CradialGradient id='ff-k' cx='355.7' cy='124.9' r='500.3' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='.1' stop-color='%23fff44f'/%3E%3Cstop offset='.2' stop-color='%23ffe141'/%3E%3Cstop offset='.5' stop-color='%23ffaf1e'/%3E%3Cstop offset='.6' stop-color='%23ff980e'/%3E%3C/radialGradient%3E%3ClinearGradient id='ff-a' x1='446.9' y1='76.8' x2='47.9' y2='461.8' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='.1' stop-color='%23fff44f'/%3E%3Cstop offset='.1' stop-color='%23ffe847'/%3E%3Cstop offset='.2' stop-color='%23ffc830'/%3E%3Cstop offset='.4' stop-color='%23ff980e'/%3E%3Cstop offset='.4' stop-color='%23ff8b16'/%3E%3Cstop offset='.5' stop-color='%23ff672a'/%3E%3Cstop offset='.5' stop-color='%23ff3647'/%3E%3Cstop offset='.7' stop-color='%23e31587'/%3E%3C/linearGradient%3E%3ClinearGradient id='ff-l' x1='442.1' y1='74.8' x2='102.6' y2='414.3' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='.2' stop-color='%23fff44f' stop-opacity='.8'/%3E%3Cstop offset='.3' stop-color='%23fff44f' stop-opacity='.6'/%3E%3Cstop offset='.5' stop-color='%23fff44f' stop-opacity='.2'/%3E%3Cstop offset='.6' stop-color='%23fff44f' stop-opacity='0'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M479 166c-11-25-32-52-49-60a249 249 0 0 1 25 73c-27-68-73-95-111-155a255 255 0 0 1-8-14 44 44 0 0 1-4-9 1 1 0 0 0 0-1 1 1 0 0 0-1 0c-60 35-81 101-83 134a120 120 0 0 0-66 25 71 71 0 0 0-6-5 111 111 0 0 1-1-58c-25 11-44 29-58 44-9-12-9-52-8-60l-8 4a175 175 0 0 0-24 21 210 210 0 0 0-22 26 203 203 0 0 0-32 73l-1 2-2 15a229 229 0 0 0-4 34v1a240 240 0 0 0 477 40l1-9c5-41 0-84-15-121zM202 355l3 1-3-1zm55-145zm198-31z' fill='url(%23ff-a)'/%3E%3Cpath d='M479 166c-11-25-32-52-49-60 14 26 22 53 25 72v1a207 207 0 0 1-206 279c-113-3-212-87-231-197-3-17 0-26 2-40-2 11-3 14-4 34v1a240 240 0 0 0 477 40l1-9c5-41 0-84-15-121z' fill='url(%23ff-b)'/%3E%3Cpath d='M479 166c-11-25-32-52-49-60 14 26 22 53 25 72v1a207 207 0 0 1-206 279c-113-3-212-87-231-197-3-17 0-26 2-40-2 11-3 14-4 34v1a240 240 0 0 0 477 40l1-9c5-41 0-84-15-121z' fill='url(%23ff-c)'/%3E%3Cpath d='m362 195 1 1a130 130 0 0 0-22-29C266 92 322 5 331 0c-60 35-81 101-83 134l9-1c45 0 84 25 105 62z' fill='url(%23ff-d)'/%3E%3Cpath d='M257 210c-1 6-22 26-29 26-68 0-80 41-80 41 3 35 28 64 57 79l4 2 7 3a107 107 0 0 0 31 6c120 6 143-143 57-186 22-4 45 5 58 14-21-37-60-62-105-62l-9 1a120 120 0 0 0-66 25l17 16c16 16 58 33 58 35z' fill='url(%23ff-e)'/%3E%3Cpath d='M257 210c-1 6-22 26-29 26-68 0-80 41-80 41 3 35 28 64 57 79l4 2 7 3a107 107 0 0 0 31 6c120 6 143-143 57-186 22-4 45 5 58 14-21-37-60-62-105-62l-9 1a120 120 0 0 0-66 25l17 16c16 16 58 33 58 35z' fill='url(%23ff-f)'/%3E%3Cpath d='m171 151 5 3a111 111 0 0 1-1-58c-25 11-44 29-58 44 1 0 36 0 54 11z' fill='url(%23ff-g)'/%3E%3Cpath d='M18 261a242 242 0 0 0 231 197 207 207 0 0 0 206-279c8 56-20 110-64 146-86 71-169 43-186 31l-3-1c-50-24-71-70-67-110-42 0-57-35-57-35s38-28 89-4c46 22 90 4 90 4 0-2-42-19-58-35l-17-16a71 71 0 0 0-6-5l-5-3c-18-11-52-11-54-11-9-12-9-51-8-60l-8 4a175 175 0 0 0-24 21 210 210 0 0 0-22 26 203 203 0 0 0-32 73c0 1-9 38-5 57z' fill='url(%23ff-h)'/%3E%3Cpath d='M341 167a130 130 0 0 1 22 29 46 46 0 0 1 4 3c55 50 26 121 24 126 44-36 72-90 64-146-27-68-73-95-111-155a255 255 0 0 1-8-14 44 44 0 0 1-4-9 1 1 0 0 0 0-1 1 1 0 0 0-1 0c-9 5-65 92 10 167z' fill='url(%23ff-i)'/%3E%3Cpath d='M367 199a46 46 0 0 0-4-3l-1-1c-13-9-36-18-58-15 86 44 63 193-57 187a107 107 0 0 1-31-6 131 131 0 0 1-11-5c17 12 99 39 186-31 2-5 31-76-24-126z' fill='url(%23ff-j)'/%3E%3Cpath d='M148 277s12-41 80-41c7 0 28-20 29-26s-44 18-90-4c-51-24-89 4-89 4s15 35 57 35c-4 40 16 85 67 110l3 1c-29-15-54-44-57-79z' fill='url(%23ff-k)'/%3E%3Cpath d='M479 166c-11-25-32-52-49-60a249 249 0 0 1 25 73c-27-68-73-95-111-155a255 255 0 0 1-8-14 44 44 0 0 1-4-9 1 1 0 0 0 0-1 1 1 0 0 0-1 0c-60 35-81 101-83 134l9-1c45 0 84 25 105 62-13-9-36-18-58-14 86 43 63 192-57 186a107 107 0 0 1-31-6 131 131 0 0 1-11-5l-3-1 3 1c-29-15-54-44-57-79 0 0 12-41 80-41 7 0 28-20 29-26 0-2-42-19-58-35l-17-16a71 71 0 0 0-6-5 111 111 0 0 1-1-58c-25 11-44 29-58 44-9-12-9-52-8-60l-8 4a175 175 0 0 0-24 21 210 210 0 0 0-22 26 203 203 0 0 0-32 73l-1 2-2 15a279 279 0 0 0-4 34v1a240 240 0 0 0 477 40l1-9c5-41 0-84-15-121zm-24 13z' fill='url(%23ff-l)'/%3E%3C/svg%3E)
- ![Safari: 1.](data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='24' height='24' viewBox='195 190 135 135'%3E%3Cdefs%3E%3ClinearGradient id='s-a' x1='132.6' x2='134.4' y1='111.7' y2='-105.3' xlink:href='%23s-b'%3E%3Cstop offset='0' stop-color='%23d2d2d2' /%3E%3Cstop offset='.5' stop-color='%23f2f2f2' /%3E%3Cstop offset='1' stop-color='%23fff' /%3E%3C/linearGradient%3E%3ClinearGradient id='s-b' gradientUnits='userSpaceOnUse' /%3E%3ClinearGradient id='s-c' x1='65.4' x2='67.4' y1='115.7' y2='17.1' xlink:href='%23s-b'%3E%3Cstop offset='0' stop-color='%23005ad5' /%3E%3Cstop offset='.2' stop-color='%230875f0' /%3E%3Cstop offset='.3' stop-color='%23218cee' /%3E%3Cstop offset='.6' stop-color='%2327a5f3' /%3E%3Cstop offset='.8' stop-color='%2325aaf2' /%3E%3Cstop offset='1' stop-color='%2321aaef' /%3E%3C/linearGradient%3E%3ClinearGradient id='s-d' x1='158.7' x2='176.3' y1='96.7' y2='79.5' xlink:href='%23s-b'%3E%3Cstop offset='0' stop-color='%23c72e24' /%3E%3Cstop offset='1' stop-color='%23fd3b2f' /%3E%3C/linearGradient%3E%3CradialGradient id='s-i' cx='-69.9' cy='69.3' r='54' gradientTransform='matrix(.9 -.01 .04 2.72 -9 -120)' xlink:href='%23s-b'%3E%3Cstop offset='0' stop-color='%2324a5f3' stop-opacity='0' /%3E%3Cstop offset='1' stop-color='%231e8ceb' /%3E%3C/radialGradient%3E%3CradialGradient id='s-j' cx='109.3' cy='13.8' r='93.1' gradientTransform='matrix(-.02 1.1 -1.04 -.02 137 -115)' xlink:href='%23s-b'%3E%3Cstop offset='0' stop-opacity='0' /%3E%3Cstop offset='1' stop-color='%235488d6' stop-opacity='0' /%3E%3Cstop offset='1' stop-color='%235d96eb' /%3E%3C/radialGradient%3E%3C/defs%3E%3Crect width='220' height='220' x='22' y='-107' fill='url(%23s-a)' ry='49' transform='matrix(.57 0 0 .57 187 256)' /%3E%3Cg transform='translate(194 190)'%3E%3Ccircle cx='67.8' cy='67.7' fill='url(%23s-c)' paint-order='stroke fill markers' r='54' /%3E%3Ccircle cx='-69.9' cy='69.3' fill='url(%23s-i)' transform='translate(138 -2)' r='54' /%3E%3C/g%3E%3Cellipse cx='120' cy='14.2' fill='url(%23s-j)' rx='93.1' ry='93.7' transform='matrix(.58 0 0 .58 192 250)' /%3E%3Cg transform='matrix(.58 0 0 .57 197 182)'%3E%3Cpath fill='%23cac7c8' d='M46 192h1l72-48-7-9-66 57Z' /%3E%3Cpath fill='%23fbfffc' d='M46 191v1l66-57-7-9-59 65Z' /%3E%3Cpath fill='url(%23s-d)' d='m119 144-7-9 66-57-59 66Z' /%3E%3Cpath fill='%23fb645c' d='m105 126 7 9 66-57-1-1-72 49Z' /%3E%3C/g%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-miterlimit='1' stroke-width='1.3' d='m287 278 3-2m-12-17 8-2m-8-3h4m-4-13 8 2m-8 3h4m-1-13 7 3m-4-11 7 4m-2-11 6 6m0-12 6 7m1-11 4 6m4-10 3 7m5-9 2 7m15-7-1 7m10-5-3 7m11-4-4 7m11-2-5 6m16 7-7 4m10 4-7 3m10 6-8 1m8 16-8-2m5 10-7-3m4 11-7-4m2 11-6-5m0 11-5-6m-2 11-4-7m-4 11-3-8m-6 10-1-8m-16 8 2-8m-10 5 3-7m-11 4 4-7m-11 2 5-6m-8 3 3-3m4 8 2-3m5 8 2-4m6 7 1-4m8 5v-4m8 4v-4m9 3-1-4m9 1-2-4m9 0-2-4m9-2-3-3m8-4-3-2m8-5-4-2m7-6-4-1m5-8h-4m4-8h-4m3-9-4 1m1-9-4 2m-1-9-3 2m-2-9-3 3m-4-8-2 3m-5-8-2 4m-6-6-1 3m-8-5v4m-8-4v4m-9-2 1 3m-9 0 2 3m-9 1 2 3m-9 2 3 3m-8 4 3 2m-8 5 4 2m-7 6 4 1m-4 25 4-1m-2 5 7-3m-6 7 4-2m-2 6 7-4m-13-21h8m41-41v-8m0 99v-8m49-42h-8' transform='translate(-65 8)' /%3E%3C/svg%3E)

[Source](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/button)

<br />

The
[`<button>`](https://developer.mozilla.org/docs/Web/HTML/Element/button)
element is built for user interaction. Its `click` event triggers from keyboard,
mouse, touch, voice, and more, with [smart rules about its
timing](https://css-tricks.com/when-a-click-is-not-just-a-click/). It also comes
with some default styles in each browser, so you can use them directly without
any customization. Use [`color-scheme`](https://web.dev/articles/color-scheme) to opt
into browser-provided light and dark buttons too.
<iframe allow="camera; clipboard-read; clipboard-write; encrypted-media; geolocation; microphone; midi;" loading="lazy" src="https://codepen.io/web-dot-dev/embed/PoQqQaW?height=500&amp;theme-id=light&amp;default-tab=result&amp;editable=true" style="height: 100%; width: 100%; border: 0;" title="Pen PoQqQaW by web-dot-dev on Codepen"></iframe>

There are also [different types of
buttons](https://developer.mozilla.org/docs/Web/HTML/Element/button#attr-type),
each shown in the preceding Codepen embed. A `<button>` without a type will
adapt to being within a `<form>`, changing to the submit type.

    <!-- buttons -->
    <button></button>
    <button type="submit"></button>
    <button type="button"></button>
    <button type="reset"></button>

    <!-- button state -->
    <button disabled></button>

    <!-- input buttons -->
    <input type="button" />
    <input type="file">

In this month's [GUI Challenge](https://github.com/argyleink/gui-challenges),
each button will get styles to help visually differentiate their intent. [Reset
buttons](https://developer.mozilla.org/docs/Web/HTML/Element/input/reset)
will have warning colors since they're destructive, and [submit
buttons](https://developer.mozilla.org/docs/Web/HTML/Element/input/submit)
will get blue accent text so they appear slightly more promoted than regular
buttons.
![Preview of the final set of all button types, shown in a form and not in a form, with nice additions for icon buttons and customized buttons.](https://web.dev/static/articles/building/a-button-component/image/preview-the-final-set-a-f0edd66ff9463.png) Preview of the final set of all button types, shown in a form and not in a form, with nice additions for icon buttons and customized buttons

> [!NOTE]
> **Note:** The buttons have always acted in their own distinct ways, so why not style them to support their behavior?

Buttons also have [pseudo classes](https://web.dev/learn/css/pseudo-classes)
for CSS to use for styling. These classes provide CSS hooks into customizing the
feel of the button: [`:hover`](https://web.dev/learn/css/pseudo-classes#hover)
for when a mouse is over the button,
[`:active`](https://web.dev/learn/css/pseudo-classes#active) for when a mouse
or keyboard is pressing, and
[`:focus`](https://web.dev/learn/css/pseudo-classes#focus_focus-within_and_focus-visible) or
[`:focus-visible`](https://web.dev/learn/css/pseudo-classes#focus_focus-within_and_focus-visible)
for assisting in assistive technology styling.

    button:hover {}
    button:active {}
    button:focus {}
    button:focus-visible {}

![Preview of the final set of all button types in the dark theme.](https://web.dev/static/articles/building/a-button-component/image/preview-the-final-set-a-65448775506f6.png) Preview of the final set of all button types in the dark theme

## Markup

In addition to the button types provided by the HTML specification, I've added a
button with an icon and a button with a custom class `btn-custom`.

    <button>Default</button>
    <input type="button" value="<input>"/>
    <button>
      <svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
        <path d="..." />
      </svg>
      Icon
    </button>
    <button type="submit">Submit</button>
    <button type="button">Type Button</button>
    <button type="reset">Reset</button>
    <button disabled>Disabled</button>
    <button class="btn-custom">Custom</button>
    <input type="file">

Then, for testing, each button is placed inside of a form. This way I can ensure
styles are updated appropriately for the default button, which behaves as a
submit button. I also switch the icon strategy, from inline SVG to a masked SVG,
to ensure both work equally well.

    <form>
      <button>Default</button>
      <input type="button" value="<input>"/>
      <button>Icon <span data-icon="cloud"></span></button>
      <button type="submit">Submit</button>
      <button type="button">Type Button</button>
      <button type="reset">Reset</button>
      <button disabled>Disabled</button>
      <button class="btn-custom btn-large" type="button">Large Custom</button>
      <input type="file">
    </form>

The matrix of combinations is pretty overwhelming at this point. Between button
types, pseudo-classes, and being in or out of a form, there are over 20
combinations of buttons. It's a good thing CSS can help us articulate each of
them clearly!

## Accessibility

Button elements are naturally accessible but there are a few common
enhancements.

### Hover and focus together

I like to group `:hover` and `:focus` together with the `:is()` functional
pseudo selector. This helps ensure my interfaces always consider keyboard
and assistive technology styles.

    button:is(:hover, :focus) {
      ...
    }

Try a [demo](https://codepen.io/argyleink/pen/powMQgx)!

### Interactive focus ring

I like to animate the focus ring for keyboard and assistive technology users. I
accomplish this by animating the outline away from the button by 5px, but only
when the button is not active. This creates an effect that makes the focus ring
shrink back to the button size when pressed.

    :where(button, input):where(:not(:active)):focus-visible {
      outline-offset: 5px;
    }

### Ensuring passing color contrast

There are at least four different color combinations across light and dark that
need consideration of color contrast: button, submit button, reset button, and
disabled button. [VisBug](https://a.nerdy.dev/gimme-visbug) is used here to
inspect and show all their scores at once:

### Hiding icons from folks who can't see

When creating an icon button, the icon should provide visual support to the
button text. This also means the icon is not valuable to someone with sight
loss. Fortunately the browser provides a way to hide items from screen-reader
technology so people with sight loss aren't bothered with decorative button
images:

    <button>
      <svg ... aria-hidden="true">...</svg>
      Icon Button
    </button>

![Chrome DevTools showing the accessibility tree for the button. The tree ignores the button image because it has aria-hidden set to true.](https://web.dev/static/articles/building/a-button-component/image/chrome-devtools-showing-4f9517c567e4c.png) Chrome DevTools showing the accessibility tree for the button. The tree ignores the button image because it has aria-hidden set to true

## Styles

In this next section, I first establish a custom property system for managing
the adaptive styles of the button. With those custom properties I can begin to
select elements and customize their appearance.

### An adaptive custom property strategy

The custom property strategy used in this GUI Challenge is very similar to that
used in [building a color scheme](https://web.dev/articles/building/a-color-scheme). For
an adaptive light and dark color system, a custom property for each theme is
defined and named accordingly. Then a single custom property is used to hold the
current value of the theme and is assigned to a CSS property. Later, the single
custom property can be updated to a different value, then updating the button
style.

    button {
      --_bg-light: white;
      --_bg-dark: black;
      --_bg: var(--_bg-light);

      background-color: var(--_bg);
    }

    @media (prefers-color-scheme: dark) {
      button {
        --_bg: var(--_bg-dark);
      }
    }

> [!NOTE]
> **Note:** The underscore convention is from Lea Verou in this post [Custom properties with defaults](https://lea.verou.me/2021/10/custom-properties-with-defaults/).

What I like is that the light and dark themes are declarative and clear. The
indirection and abstraction are offloaded into the `--_bg` custom property,
which is now the only "reactive" property; `--_bg-light` and `--_bg-dark` are
static. It's also clear to read that the light theme is the default theme and
dark is only applied conditionally.

### Preparing for design consistency

#### The shared selector

The following selector is used to target all the various types of buttons and is
a bit overwhelming at first. [`:where()`](https://web.dev/articles/css-is-and-where) is
used so customizing the button requires no specificity. Buttons are often
adapted for alternative scenarios and the `:where()` selector ensures that task
is easy. Inside `:where()`, each button type is selected, including the
`::file-selector-button`, which [can't be
used](https://developer.mozilla.org/docs/Web/CSS/:is#is_does_not_select_pseudo-elements)
inside of `:is()` or `:where()`.

    :where(
      button,
      input[type="button"],
      input[type="submit"],
      input[type="reset"],
      input[type="file"]
    ),
    :where(input[type="file"])::file-selector-button {
      ...
    }

All the custom properties will be scoped inside this selector. Time to review
all the custom properties! There are quite a few custom properties used in this
button. I'll describe each group as we go, then share the dark and reduced
motion contexts at the end of the section.

#### Button accent color

Submit buttons and icons are a great place for a pop of color:

    --_accent-light: hsl(210 100% 40%);
    --_accent-dark: hsl(210 50% 70%);
    --_accent: var(--_accent-light);

#### Button text color

Button text colors aren't white or black, they're darkened or lightened versions
of `--_accent` using
[`hsl()`](https://developer.mozilla.org/docs/Web/CSS/color_value/hsl) and
sticking to the hue `210`:

    --_text-light: hsl(210 10% 30%);
    --_text-dark: hsl(210 5% 95%);
    --_text: var(--_text-light);

#### Button background color

Button backgrounds follow the same `hsl()` pattern except for the light theme
buttons---those are set to white so their surface makes them appear close to the
user, or in front of other surfaces:

    --_bg-light: hsl(0 0% 100%);
    --_bg-dark: hsl(210 9% 31%);
    --_bg: var(--_bg-light);

#### Button background well

This background color is for making a surface appear behind other surfaces,
useful for the background of the file input:

    --_input-well-light: hsl(210 16% 87%);
    --_input-well-dark: hsl(204 10% 10%);
    --_input-well: var(--_input-well-light);

#### Button padding

The spacing around the text in the button is done using the
[`ch`](https://developer.mozilla.org/docs/Learn/CSS/Building_blocks/Values_and_units)
unit, a relative length to the font size. This becomes critical when large
buttons are able to simply bump up the `font-size` and button scales
proportionally:

    --_padding-inline: 1.75ch;
    --_padding-block: .75ch;

#### Button border

The button border radius is stashed into a custom property so the file input can
match the other buttons. The border colors follow the established adaptive color
system:

    --_border-radius: .5ch;

    --_border-light: hsl(210 14% 89%);
    --_border-dark: var(--_bg-dark);
    --_border: var(--_border-light);

#### Button hover highlight effect

These properties establish a size property for transitioning on interaction, and
the highlight color follows the adaptive color system. We'll cover how these
interact later in this post, but ultimately these are used for a `box-shadow`
effect:

    --_highlight-size: 0;

    --_highlight-light: hsl(210 10% 71% / 25%);
    --_highlight-dark: hsl(210 10% 5% / 25%);
    --_highlight: var(--_highlight-light);

#### Button text shadow

Each button has a subtle text shadow style. This helps the text sit on top of
the button, improving legibility and adding a nice layer of presentation polish.

    --_ink-shadow-light: 0 1px 0 var(--_border-light);
    --_ink-shadow-dark: 0 1px 0 hsl(210 11% 15%);
    --_ink-shadow: var(--_ink-shadow-light);

#### Button icon

Icons are the size of two characters thanks to the relative length `ch` unit
again, which will help the icon scale proportionally to the button text. The
icon color leans on the `--_accent-color` for an adaptive and within-theme
color.

    --_icon-size: 2ch;
    --_icon-color: var(--_accent);

> [!NOTE]
> **Note:** By having its own property, customizing the icon color can be done without changing the accent color.

#### Button shadow

For shadows to properly adapt to light and dark, they need to both shift their
color and opacity. Light theme shadows are best when they are subtle and tinted
towards the surface color they overlay. Dark theme shadows need to be darker and
more saturated so they can overlay darker surface colors.

    --_shadow-color-light: 220 3% 15%;
    --_shadow-color-dark: 220 40% 2%;
    --_shadow-color: var(--_shadow-color-light);

    --_shadow-strength-light: 1%;
    --_shadow-strength-dark: 25%;
    --_shadow-strength: var(--_shadow-strength-light);

With adaptive colors and strengths I can assemble two depths of shadows:

    --_shadow-1: 0 1px 2px -1px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 9%));

    --_shadow-2: 
      0 3px 5px -2px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 3%)),
      0 7px 14px -5px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 5%));

Furthermore, to give the buttons a slightly 3D appearance, a `1px` box-shadow
creates the illusion:

    --_shadow-depth-light: 0 1px var(--_border-light);
    --_shadow-depth-dark: 0 1px var(--_bg-dark);
    --_shadow-depth: var(--_shadow-depth-light);

#### Button transitions

Following the pattern for adaptive colors, I create two static properties to
hold the design system options:

    --_transition-motion-reduce: ;
    --_transition-motion-ok:
      box-shadow 145ms ease,
      outline-offset 145ms ease
    ;
    --_transition: var(--_transition-motion-reduce);

> [!NOTE]
> **Note:** In early designs of the buttons, I animated `border-color`, which was in the `--_transition-motion-reduce` property. I later removed it but left the pattern in case I changed my mind again.

#### All properties together in the selector

All custom properties in a selector

<br />

```css
:where(
  button,
  input[type="button"],
  input[type="submit"],
  input[type="reset"],
  input[type="file"]
),
:where(input[type="file"])::file-selector-button {
  --_accent-light: hsl(210 100% 40%);
  --_accent-dark: hsl(210 50% 70%);
  --_accent: var(--_accent-light);

--_text-light: hsl(210 10% 30%);
  --_text-dark: hsl(210 5% 95%);
  --_text: var(--_text-light);

--_bg-light: hsl(0 0% 100%);
  --_bg-dark: hsl(210 9% 31%);
  --_bg: var(--_bg-light);

--_input-well-light: hsl(210 16% 87%);
  --_input-well-dark: hsl(204 10% 10%);
  --_input-well: var(--_input-well-light);

--_padding-inline: 1.75ch;
  --_padding-block: .75ch;

--_border-radius: .5ch;
  --_border-light: hsl(210 14% 89%);
  --_border-dark: var(--_bg-dark);
  --_border: var(--_border-light);

--_highlight-size: 0;
  --_highlight-light: hsl(210 10% 71% / 25%);
  --_highlight-dark: hsl(210 10% 5% / 25%);
  --_highlight: var(--_highlight-light);

--_ink-shadow-light: 0 1px 0 hsl(210 14% 89%);
  --_ink-shadow-dark: 0 1px 0 hsl(210 11% 15%);
  --_ink-shadow: var(--_ink-shadow-light);

--_icon-size: 2ch;
  --_icon-color-light: var(--_accent-light);
  --_icon-color-dark: var(--_accent-dark);
  --_icon-color: var(--accent, var(--_icon-color-light));

--_shadow-color-light: 220 3% 15%;
  --_shadow-color-dark: 220 40% 2%;
  --_shadow-color: var(--_shadow-color-light);
  --_shadow-strength-light: 1%;
  --_shadow-strength-dark: 25%;
  --_shadow-strength: var(--_shadow-strength-light);
  --_shadow-1: 0 1px 2px -1px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 9%));
  --_shadow-2: 
    0 3px 5px -2px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 3%)),
    0 7px 14px -5px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 5%))
  ;

--_shadow-depth-light: hsl(210 14% 89%);
  --_shadow-depth-dark: var(--_bg-dark);
  --_shadow-depth: var(--_shadow-depth-light);

--_transition-motion-reduce: ;
  --_transition-motion-ok:
    box-shadow 145ms ease,
    outline-offset 145ms ease
  ;
  --_transition: var(--_transition-motion-reduce);
}
```

<br />

![Default buttons show in light and dark theme side by side.](https://web.dev/static/articles/building/a-button-component/image/default-buttons-show-lig-7bf42c00f7a7f.png)

#### Dark theme adaptations

The value of the `-light` and `-dark` static props pattern becomes clear when
the dark theme props are set:

    @media (prefers-color-scheme: dark) {
      :where(
        button,
        input[type="button"],
        input[type="submit"],
        input[type="reset"],
        input[type="file"]
      ),
      :where(input[type="file"])::file-selector-button {
        --_bg: var(--_bg-dark);
        --_text: var(--_text-dark);
        --_border: var(--_border-dark);
        --_accent: var(--_accent-dark);
        --_highlight: var(--_highlight-dark);
        --_input-well: var(--_input-well-dark);
        --_ink-shadow: var(--_ink-shadow-dark);
        --_shadow-depth: var(--_shadow-depth-dark);
        --_shadow-color: var(--_shadow-color-dark);
        --_shadow-strength: var(--_shadow-strength-dark);
      }
    }

Not only does this read well, but consumers of these custom buttons can use the
bare props with confidence that they'll adapt appropriately to user preferences.

#### Reduced motion adaptations

If motion is OK with this visiting user, assign `--_transition` to
`var(--_transition-motion-ok)`:

    @media (prefers-reduced-motion: no-preference) {
      :where(
        button,
        input[type="button"],
        input[type="submit"],
        input[type="reset"],
        input[type="file"]
      ),
      :where(input[type="file"])::file-selector-button {
        --_transition: var(--_transition-motion-ok);
      }
    }

#### A few shared styles

Buttons and inputs need to have their fonts set to `inherit` so they match the
rest of the page fonts; otherwise they'll be styled by the browser. This also
applies to `letter-spacing`. Setting `line-height` to `1.5` sets the letter box
size to give the text some space above and below:

    :where(
      button,
      input[type="button"],
      input[type="submit"],
      input[type="reset"],
      input[type="file"]
    ),
    :where(input[type="file"])::file-selector-button {
      /* ...CSS variables */

      font: inherit;
      letter-spacing: inherit;
      line-height: 1.5;
      border-radius: var(--_border-radius);
    }

![Screenshot showing buttons after the preceding styles have been applied.](https://web.dev/static/articles/building/a-button-component/image/screenshot-showing-button-a1282c3c8e3d6.png)

### Styling buttons

#### Selector adjustment

The selector `input[type="file"]` is not the button part of the input, the
pseudo-element `::file-selector-button` is, so I've removed `input[type="file"]`
from the list:

    :where(
      button,
      input[type="button"],
      input[type="submit"],
      input[type="reset"],
      input[type="file"]
    ),
    :where(input[type="file"])::file-selector-button {
      
    }

#### Cursor and touch adjustments

First I style the cursor to the `pointer` style, which helps the button indicate
to mouse users that it's interactive. Then I add `touch-action: manipulation` to
make clicks not need to wait and observe a potential double click, making the
buttons feel faster:

    :where(
      button,
      input[type="button"],
      input[type="submit"],
      input[type="reset"]
    ),
    :where(input[type="file"])::file-selector-button {
      cursor: pointer;
      touch-action: manipulation;
    }

> [!NOTE]
> **Note:** `touch-action: manipulation` isn't required if the HTML document has `<meta name="viewport" content="width=device-width">`. Learn more on Chrome Developers [here](https://developer.chrome.com/blog/300ms-tap-delay-gone-away).

#### Colors and borders

Next I customize the font size, background, text, and border colors, using some
of the adaptive custom properties established earlier:

    :where(
      button,
      input[type="button"],
      input[type="submit"],
      input[type="reset"]
    ),
    :where(input[type="file"])::file-selector-button {
      ...

      font-size: var(--_size, 1rem);
      font-weight: 700;
      background: var(--_bg);
      color: var(--_text);
      border: 2px solid var(--_border);
    }

![Screenshot showing buttons after the preceding styles have been applied.](https://web.dev/static/articles/building/a-button-component/image/screenshot-showing-button-d9b0e2e653324.png)

#### Shadows

The buttons have some great techniques applied. The
[`text-shadow`](https://developer.mozilla.org/docs/Web/CSS/text-shadow) is
adaptive to light and dark, creating a pleasing subtle appearance of the button
text sitting nicely on top of the background. For the
[`box-shadow`](https://developer.mozilla.org/docs/Web/CSS/box-shadow),
three shadows are assigned. The first, `--_shadow-2`, is a regular box shadow.
The second shadow is a trick to the eye that makes the button appear to be
beveled up a little bit. The last shadow is for the hover highlight, initially
at a size of 0, but it will be given a size later and transitioned so it appears
to grow from the button.

    :where(
      button,
      input[type="button"],
      input[type="submit"],
      input[type="reset"]
    ),
    :where(input[type="file"])::file-selector-button {
      ...

      box-shadow: 
        var(--_shadow-2),
        var(--_shadow-depth),
        0 0 0 var(--_highlight-size) var(--_highlight)
      ;
      text-shadow: var(--_ink-shadow);
    }

![Screenshot showing buttons after the preceding styles have been applied.](https://web.dev/static/articles/building/a-button-component/image/screenshot-showing-button-8873a5486ea2d.png)

#### Layout

I gave the button a [flexbox](https://web.dev/learn/css/flexbox) layout,
specifically an `inline-flex` layout that will fit its content. I then center
the text, and vertically and horizontally align children to the
[center](https://web.dev/articles/centering-in-css). This will help icons and other
button elements to align properly.

    :where(
      button,
      input[type="button"],
      input[type="submit"],
      input[type="reset"]
    ),
    :where(input[type="file"])::file-selector-button {
      ...

      display: inline-flex;
      justify-content: center;
      align-items: center;
      text-align: center;
    }

![Screenshot showing buttons after the preceding styles have been applied.](https://web.dev/static/articles/building/a-button-component/image/screenshot-showing-button-b3757925a4b09.png)

#### Spacing

For button spacing, I used
[`gap`](https://developer.mozilla.org/docs/Web/CSS/gap) to keep siblings
from touching and [logical
properties](https://web.dev/learn/css/logical-properties) for padding so button
spacing works for all text layouts.

    :where(
      button,
      input[type="button"],
      input[type="submit"],
      input[type="reset"]
    ),
    :where(input[type="file"])::file-selector-button {
      ...

      gap: 1ch;
      padding-block: var(--_padding-block);
      padding-inline: var(--_padding-inline);
    }

![Screenshot showing buttons after the preceding styles have been applied.](https://web.dev/static/articles/building/a-button-component/image/screenshot-showing-button-1ded046f0b00f.png)

#### Touch and mouse UX

This next section is mostly for touch users on mobile devices. The first
property,
[`user-select`](https://developer.mozilla.org/docs/Web/CSS/user-select),
is for all users; it prevents text highlighting the button text. This is mostly
noticeable on touch devices when a button is tapped and held and the operating
system highlights the text of the button.

I've generally found this is not the user experience with buttons in built-in
apps, so I disable it by setting `user-select` to none. Tap highlight colors
([`-webkit-tap-highlight-color`](https://developer.mozilla.org/docs/Web/CSS/-webkit-tap-highlight-color))
and operating system context menus
([`-webkit-touch-callout`](https://developer.mozilla.org/docs/Web/CSS/-webkit-touch-callout))
are other very web-centric button features that aren't aligned with general
button user expectations, so I remove them as well.

    :where(
      button,
      input[type="button"],
      input[type="submit"],
      input[type="reset"]
    ),
    :where(input[type="file"])::file-selector-button {
      ...

      user-select: none;
      -webkit-tap-highlight-color: transparent;
      -webkit-touch-callout: none;
    }

#### Transitions

The adaptive `--_transition` variable is assigned to the
[transition](https://web.dev/learn/css/transitions) property:

    :where(
      button,
      input[type="button"],
      input[type="submit"],
      input[type="reset"]
    ),
    :where(input[type="file"])::file-selector-button {
      ...

      transition: var(--_transition);
    }

Upon hover, while the user is not actively pressing, adjust the shadow highlight
size to give it a nice focus appearance that appears to grow from within the
button:

    :where(
      button,
      input[type="button"],
      input[type="submit"],
      input[type="reset"]
    ):where(:not(:active):hover) {
      --_highlight-size: .5rem;
    }

> [!NOTE]
> **Note:** The `::file-selector-button` is omitted from the selector because the effect is not able to be seen on it.

Upon focus, increase the focus outline offset from the button, also giving it a
nice focus appearance that appears to grow from within the button:

    :where(button, input):where(:not(:active)):focus-visible {
      outline-offset: 5px;
    }

#### Icons

For handling icons, the selector has an added `:where()` selector for direct SVG
children or elements with the custom attribute `data-icon`. The icon size is set
with the custom property using inline and block logical properties. Stroke color
is set, as well as a
[`drop-shadow`](https://developer.mozilla.org/docs/Web/CSS/filter-function/drop-shadow)
to match the `text-shadow`. `flex-shrink` is set to `0` so the icon is never
squished. Lastly, I select lined icons and I assign those styles here with
`fill: none` and `round` line caps and line joins:

    :where(
      button,
      input[type="button"],
      input[type="submit"],
      input[type="reset"]
    ) > :where(svg, [data-icon]) {
      block-size: var(--_icon-size);
      inline-size: var(--_icon-size);
      stroke: var(--_icon-color);
      filter: drop-shadow(var(--_ink-shadow));

      flex-shrink: 0;
      fill: none;
      stroke-linecap: round;
      stroke-linejoin: round;
    }

![Screenshot showing buttons after the preceding styles have been applied.](https://web.dev/static/articles/building/a-button-component/image/screenshot-showing-button-18293637a1a8a.png)

#### Customizing submit buttons

I wanted submit buttons to have a slightly promoted appearance, and I achieved
this by making the text color of the buttons the accent color:

    :where(
      [type="submit"], 
      form button:not([type],[disabled])
    ) {
      --_text: var(--_accent);
    }

![Screenshot showing buttons after the preceding styles have been applied.](https://web.dev/static/articles/building/a-button-component/image/screenshot-showing-button-24526cbea293c.png)

#### Customize reset buttons

I wanted reset buttons to have some built-in warning signs to alert users of
their potentially destructive behavior. I also chose to style the light theme
button with more red accents than the dark theme. The customization is done by
changing the appropriate light or dark underlying color, and the button will
update the style:

    :where([type="reset"]) {
      --_border-light: hsl(0 100% 83%);
      --_highlight-light: hsl(0 100% 89% / 20%);
      --_text-light: hsl(0 80% 50%);
      --_text-dark: hsl(0 100% 89%);
    }

I also thought it'd be nice for the focus outline color to match the accent of
red. The text color adapts a dark red to a light red. I make the outline color
match this with the keyword
[`currentColor`](https://web.dev/learn/css/color#color_keywords):

    :where([type="reset"]):focus-visible {
      outline-color: currentColor;
    }

![Screenshot showing buttons after the preceding styles have been applied.](https://web.dev/static/articles/building/a-button-component/image/screenshot-showing-button-381d936fc8b9e.png)

#### Customize disabled buttons

It's all too common for disabled buttons to have poor color contrast during the
attempt to subdue the disabled button so it appears less active. I tested each
color set and made sure they passed, nudging the HSL lightness value until the
score passed in DevTools or VisBug.

    :where(
      button,
      input[type="button"],
      input[type="submit"],
      input[type="reset"]
    )[disabled] {
      --_bg: none;
      --_text-light: hsl(210 7% 40%);
      --_text-dark: hsl(210 11% 71%);

      cursor: not-allowed;
      box-shadow: var(--_shadow-1);
    }

![Screenshot showing buttons after the preceding styles have been applied.](https://web.dev/static/articles/building/a-button-component/image/screenshot-showing-button-de3280ea6e01b.png)

#### Customizing file input buttons

The file input button is a container for a span and a button. CSS is able to
style the input container a little bit, as well as the nested button, but not
the span. The container is given `max-inline-size` so it won't grow larger than
it needs to, while `inline-size: 100%` will allow itself to shrink and fit
containers smaller than it is. The background color is set to an adaptive color
that is darker than other surfaces, so it looks behind the file selector button.

    :where(input[type="file"]) {
      inline-size: 100%;
      max-inline-size: max-content;
      background-color: var(--_input-well);
    }

The file selector button and input type buttons are specifically given
`appearance: none` to remove any browser-provided styles that weren't
overwritten by the other button styles.

    :where(input[type="button"]),
    :where(input[type="file"])::file-selector-button {
      appearance: none;
    }

Lastly, margin is added to the `inline-end` of the button to push the span text
away from the button, creating some space.

    :where(input[type="file"])::file-selector-button {
      margin-inline-end: var(--_padding-inline);
    }

![Screenshot showing buttons after the preceding styles have been applied.](https://web.dev/static/articles/building/a-button-component/image/screenshot-showing-button-eb2bd23c8b6b1.png)

#### Special dark theme exceptions

I gave the primary action buttons a darker background for higher contrasting
text, giving them a slightly more promoted appearance.

    @media (prefers-color-scheme: dark) {
      :where(
        [type="submit"],
        [type="reset"],
        [disabled],
        form button:not([type="button"])
      ) {
        --_bg: var(--_input-well);
      }
    }

![Screenshot showing buttons after the preceding styles have been applied.](https://web.dev/static/articles/building/a-button-component/image/screenshot-showing-button-8f40467bba8e.png)

### Creating variants

For fun, and because it's practical, I chose to show how to create a few
variants. One variant is very vibrant, similar to how primary buttons often
look. Another variant is large. The last variant has a gradient-filled icon.

#### Vibrant button

To achieve this button style, I overwrote the base props directly with blue
colors. While this was quick and easy, it removes the adaptive props and looks
the same in both light and dark themes.

    .btn-custom {
      --_bg: linear-gradient(hsl(228 94% 67%), hsl(228 81% 59%));
      --_border: hsl(228 89% 63%);
      --_text: hsl(228 89% 100%);
      --_ink-shadow: 0 1px 0 hsl(228 57% 50%);
      --_highlight: hsl(228 94% 67% / 20%);
    }

![Custom button is shown in light and dark. It is very vibrant blue like typical primary action buttons tend to be.](https://web.dev/static/articles/building/a-button-component/image/custom-button-is-shown-l-5ea539773e647.png)

> [!NOTE]
> **Note:** If I wanted to build an adaptive primary action button, I would overwrite `--_bg-light` and `--_bg-dark` instead of `--_bg`. Overwriting these variables allows the base property to swap between the new defined light and dark props.

#### Large button

This style of button is achieved by modifying the `--_size` custom property.
Padding and other space elements are relative to this size, scaling along
proportionally with the new size.

    .btn-large {
      --_size: 1.5rem;
    }

![Large button is shown next to the custom button, about 150 times larger.](https://web.dev/static/articles/building/a-button-component/image/large-button-is-shown-nex-8956de8d41466.png)

#### Icon button

This icon effect doesn't have anything to do with our button styles, but it does
show how to achieve it with just a few CSS properties, and how well the button
handles icons that aren't inline SVG.

    [data-icon="cloud"] {
      --icon-cloud: url("https://api.iconify.design/mdi:apple-icloud.svg") center / contain no-repeat;

      -webkit-mask: var(--icon-cloud);
      mask: var(--icon-cloud);
      background: linear-gradient(to bottom, var(--_accent-dark), var(--_accent-light));
    }

![A button with an icon is shown in light and dark themes.](https://web.dev/static/articles/building/a-button-component/image/a-button-an-icon-is-show-864a3c7a8a8c5.png)

## Conclusion

Now that you know how I did it, how would you‽ 🙂

Let's diversify our approaches and learn all the ways to build on the web.

Create a demo, [tweet me](https://twitter.com/argyleink) links, and I'll add it
to the community remixes section below!

## Community remixes

*Nothing to see here yet.*

### Resources

- [Source code](https://github.com/argyleink/gui-challenges/tree/main/buttons) on Github