{"id":8870,"date":"2026-03-09T13:31:47","date_gmt":"2026-03-09T18:31:47","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=8870"},"modified":"2026-03-09T13:31:48","modified_gmt":"2026-03-09T18:31:48","slug":"the-enforced-accessibility-of-the-geolocation-element","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/the-enforced-accessibility-of-the-geolocation-element\/","title":{"rendered":"The Enforced Accessibility of the Geolocation Element"},"content":{"rendered":"\n<p>There&#8217;s a <code>&lt;geolocation><\/code> element in HTML now. Looks like <a href=\"https:\/\/developer.chrome.com\/blog\/geolocation-html-element\">Chrome led it up<\/a> and got it into Chrome first. Now we&#8217;re in the ol&#8217; \ud83e\udd37\u200d\u2640\ufe0f state on when we&#8217;ll get it elsewhere. But the process certainly involved other browser makers, so that&#8217;s good.<\/p>\n\n\n\n<p><a href=\"https:\/\/www.matuzo.at\/blog\/2026\/geolocation-element\">Manuel Matuzovi\u0107 has a good intro blog post<\/a>. <\/p>\n\n\n\n<p class=\"learn-more\">The element doesn&#8217;t behave right within an <code>&lt;iframe><\/code> so embedding a demo here doesn&#8217;t make sense. You can <a href=\"https:\/\/natural-wave-lizard.codepen.app\/\">see a small demo here<\/a>, and the <a href=\"https:\/\/codepen.io\/editor\/chriscoyier\/pen\/019cbf34-d065-7049-8dcc-a25099c9debe\">code is here<\/a>.<\/p>\n\n\n\n<p>Here&#8217;s what I think you should know:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>It&#8217;s a <code>&lt;button&gt;<\/code> with an enforced design. It&#8217;s got a map icon and text that says &#8220;Use location&#8221; (or &#8220;Use precise location&#8221; if you use <code>accuracymode=\"precise\"<\/code>)<\/li>\n\n\n\n<li>Clicking the button will,\n<ul class=\"wp-block-list\">\n<li>if access is granted, do a geolocation and fire a <code>location<\/code> event.<\/li>\n\n\n\n<li>if access needs to be granted, it will ask first, then act accordingly.<\/li>\n\n\n\n<li>if access has already been denied, a new prompt will remind you it&#8217;s been denied, and give you a chance to grant it.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>The last bullet point above is crucial. It allows you to &#8220;recover&#8221; from a denied-permission state in a way that was previously impossible. <\/li>\n\n\n\n<li>You can use it progressive-enhancement style by putting an actual <code>&lt;button><\/code> inside with event handlers that go through a flow where you aren&#8217;t 100% sure if you have granted permissions. Or <a href=\"https:\/\/github.com\/WICG\/PEPC\/tree\/main\/polyfills\/geolocation\">polyfill<\/a> it.<\/li>\n\n\n\n<li>It enforces a variety of accessibility requirements quite strictly.<\/li>\n<\/ul>\n\n\n\n<p>It&#8217;s that last one we can dig into a little here, as I find it quite interesting. I&#8217;m not sure if we&#8217;ve had an element in HTML that behaves quite like this before. <\/p>\n\n\n\n<p class=\"learn-more\">I don&#8217;t think accessibility itself is actually the motivation behind these rules. It&#8217;s actually about security and the danger of &#8220;tricking&#8221; people into exposing their geolocation when they may not want to. For example: &#8220;Want 100 free Robux? Click here, then click Allow on the next pop-up.&#8221; It can&#8217;t totally stop that, but it can try.<\/p>\n\n\n\n<p>The enforced-accessibility behavior comes in several forms:<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">You can&#8217;t change the text or the icon<\/h2>\n\n\n\n<p>As far as I can tell, anyway! The content is in a user-agent Shadow Root and there isn&#8217;t any <code>part<\/code> attributes or anything for styling access.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"336\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/03\/Screenshot-2026-03-09-at-8.24.22-AM.png?resize=1024%2C336&#038;ssl=1\" alt=\"Code snippet showing a geolocation permission request with HTML structure and SVG icon.\" class=\"wp-image-8885\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/03\/Screenshot-2026-03-09-at-8.24.22-AM.png?resize=1024%2C336&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/03\/Screenshot-2026-03-09-at-8.24.22-AM.png?resize=300%2C99&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/03\/Screenshot-2026-03-09-at-8.24.22-AM.png?resize=768%2C252&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/03\/Screenshot-2026-03-09-at-8.24.22-AM.png?w=1364&amp;ssl=1 1364w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>The normal things that penetrate the Shadow DOM still will, though. Like the <code>color<\/code> still sets the <code>color<\/code> and the SVG icon is set to <code>fill: currentColor;<\/code> so the icon color will change along with the text.<\/p>\n\n\n\n<p>But! <\/p>\n\n\n\n<p>You do get automatic localization, which is <em>really nice.<\/em> Whatever the <code>lang<\/code> attribute is set to in that area of the DOM, you&#8217;ll get text in the corrrect language.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">There is various CSS you just <em>can&#8217;t use<\/em>.<\/h2>\n\n\n\n<p>Some CSS you try to apply to a <code>&lt;geolocation&gt;<\/code> button will simply be ignored.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">geolocation<\/span> {\n  <span class=\"hljs-comment\">\/* NOPE *\/<\/span>\n  <span class=\"hljs-attribute\">translate<\/span>: <span class=\"hljs-number\">100px<\/span> <span class=\"hljs-number\">100px<\/span>;\n  <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">scale<\/span>(<span class=\"hljs-number\">0<\/span>);\n  <span class=\"hljs-attribute\">opacity<\/span>: <span class=\"hljs-number\">0.75<\/span>;\n  <span class=\"hljs-attribute\">filter<\/span>: <span class=\"hljs-built_in\">opacity<\/span>(<span class=\"hljs-number\">0<\/span>);\n  <span class=\"hljs-attribute\">inline-size<\/span>: <span class=\"hljs-number\">2px<\/span>;\n  <span class=\"hljs-attribute\">clip-path<\/span>: <span class=\"hljs-built_in\">inset<\/span>(<span class=\"hljs-number\">50%<\/span>);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>I don&#8217;t know if that&#8217;s comprehensive, but the point is, if you try to write some CSS for this button and it doesn&#8217;t work, it&#8217;s probably on purpose. There is some conflicting information, like the Chrome post says 2D translates are allowed, but in practice, they are not.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">There is some CSS that is fenced.<\/h2>\n\n\n\n<p>These are pretty strange! <\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">geolocation<\/span> {\n  <span class=\"hljs-comment\">\/* Actually capped at between -0.65px and 2.6px *\/<\/span>\n  <span class=\"hljs-attribute\">letter-spacing<\/span>: <span class=\"hljs-number\">10em<\/span>; \n\n  <span class=\"hljs-comment\">\/* Actually capped at between 0 and 6.5px *\/<\/span>\n  <span class=\"hljs-attribute\">word-spacing<\/span>: <span class=\"hljs-number\">10em<\/span>;\n\n  <span class=\"hljs-comment\">\/* Allowed, but the minimum is content size *\/<\/span>\n  <span class=\"hljs-attribute\">block-size<\/span>: <span class=\"hljs-number\">1px<\/span>;\n  <span class=\"hljs-attribute\">height<\/span>: <span class=\"hljs-number\">1px<\/span>;\n\n  <span class=\"hljs-comment\">\/* Allowed, but the minimum is content size *\/<\/span>\n  <span class=\"hljs-attribute\">inline-size<\/span>: <span class=\"hljs-number\">1px<\/span>;\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">1px<\/span>;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Perhaps the strangest one is <code>font-size<\/code> in that it&#8217;s capped but <em>also<\/em> has functionality limits.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">geolocation<\/span> {\n  <span class=\"hljs-comment\">\/* Allowed *\/<\/span>\n  <span class=\"hljs-attribute\">font-size<\/span>: <span class=\"hljs-number\">50px<\/span>;\n\n  <span class=\"hljs-comment\">\/* Forces minimum size of 8px and stops working *\/<\/span>\n  <span class=\"hljs-attribute\">font-size<\/span>: <span class=\"hljs-number\">1px<\/span>;\n\n  <span class=\"hljs-comment\">\/* Allowed, but stops working. *\/<\/span>\n  <span class=\"hljs-attribute\">font-size<\/span>: <span class=\"hljs-number\">12px<\/span>;\n\n  <span class=\"hljs-comment\">\/* Minimum size to work *\/<\/span>\n  <span class=\"hljs-attribute\">font-size<\/span>: <span class=\"hljs-number\">13px<\/span>;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>I also note that <code>font-size: 1px;<\/code> actually <em>does<\/em> render when the button is in an <code>&lt;iframe><\/code>, so uhhhh, whatever you wanna make of that.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">There is some CSS that is allowed, but then disables the button.<\/h2>\n\n\n\n<p>Like <code>font-size<\/code> above, there is other CSS you can apply that is allowed (renders) but then makes the button just not work. By not work, I specifically mean it will not trigger <code>location<\/code> events.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">geolocation<\/span> {\n  <span class=\"hljs-attribute\">background<\/span>: white;\n  <span class=\"hljs-attribute\">color<\/span>: white;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>That succeeds in hiding the button from view (on a white page), but if you find and click it, it won&#8217;t work.<\/p>\n\n\n\n<p>This is quite easy to happen! For instance:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">geolocation<\/span> {\n  <span class=\"hljs-comment\">\/* Failure state *\/<\/span>\n  <span class=\"hljs-attribute\">color<\/span>: orange;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The color <code>orange<\/code> (with a white background) is not enough contrast to be acceptable. <\/p>\n\n\n\n<p>This does trigger an &#8220;issue&#8221; in Chrome DevTools. It&#8217;s not a JavaScript error so you won&#8217;t see it in the console, it comes up in the Issues area.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1014\" height=\"966\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/03\/Screenshot-2026-03-09-at-11.22.53-AM.png?resize=1014%2C966&#038;ssl=1\" alt=\"Error message indicating geolocation element activation issue due to invalid style, with instructions for resolving it.\" class=\"wp-image-8890\" style=\"aspect-ratio:1.0496935990524743;width:400px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/03\/Screenshot-2026-03-09-at-11.22.53-AM.png?w=1014&amp;ssl=1 1014w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/03\/Screenshot-2026-03-09-at-11.22.53-AM.png?resize=300%2C286&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/03\/Screenshot-2026-03-09-at-11.22.53-AM.png?resize=768%2C732&amp;ssl=1 768w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\">It doesn&#8217;t tell you what those styling restrictions <em>are<\/em>, but I guess you can guess and test.<\/figcaption><\/figure>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\">It&#8217;s kinda weird.<\/h2>\n\n\n\n<p>It&#8217;s quite weird how there is all this CSS that it&#8217;s happy to forcibly rein in for you. But then, other CSS just allows it through and disables the button. Or, I should say, &#8220;just makes not work&#8221;, because the button does not present itself in the accessibility tree as disabled. <\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>It&#8217;s a strange situation where some CSS is disallowed, some is allowed but breaks the button, and some is capped.<\/p>\n","protected":false},"author":1,"featured_media":8894,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[7,460,31],"class_list":["post-8870","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-css","tag-geolocation","tag-html"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/03\/geolocation-button.jpg?fit=2000%2C1200&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/8870","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=8870"}],"version-history":[{"count":11,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/8870\/revisions"}],"predecessor-version":[{"id":8895,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/8870\/revisions\/8895"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/8894"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=8870"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=8870"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=8870"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}