{"id":32,"date":"2026-04-03T02:28:20","date_gmt":"2026-04-03T02:28:20","guid":{"rendered":"https:\/\/vanianettleford.com\/?p=32"},"modified":"2026-04-03T02:28:20","modified_gmt":"2026-04-03T02:28:20","slug":"when-correct-ui-isnt-the-best-ux-rethinking-a-login-button","status":"publish","type":"post","link":"https:\/\/vanianettleford.com\/?p=32","title":{"rendered":"When \u201cCorrect UI\u201d Isn\u2019t the Best UX: Rethinking a Login Button"},"content":{"rendered":"\n<p>In theory, UI should always reflect the correct state.<\/p>\n\n\n\n<p>In practice, that\u2019s not always the best user experience.<\/p>\n\n\n\n<p>I recently worked on what seemed like a small issue\u2014a login\/logout button flickering between <strong>\u201cLog in\u201d<\/strong> and <strong>\u201cLog out.\u201d<\/strong> But it turned into a deeper discussion about performance, correctness, and what actually matters in user experience.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Problem<\/h2>\n\n\n\n<p>We had a header button that showed:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\u201cLog in\u201d<\/strong> when the user is logged out<\/li>\n\n\n\n<li><strong>\u201cLog out\u201d<\/strong> when the user is authenticated<\/li>\n<\/ul>\n\n\n\n<p>But on page load, it briefly showed the wrong state.<\/p>\n\n\n\n<p>Why?<\/p>\n\n\n\n<p>Because authentication was resolved on the client.<\/p>\n\n\n\n<p>The sequence looked like this:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Initial render \u2192 assume logged out \u2192 show \u201cLog in\u201d<\/li>\n\n\n\n<li>Auth hydrates \u2192 user is actually logged in \u2192 update to \u201cLog out\u201d<\/li>\n<\/ol>\n\n\n\n<p>On a fast connection, this flicker is barely noticeable.<\/p>\n\n\n\n<p>On a slow connection?<br>It could take up to twenty seconds in some cases<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Initial Instinct: Fix the UI<\/h2>\n\n\n\n<p>The obvious solution was:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Don\u2019t render the button or disable the button until we know the auth state.<\/p>\n<\/blockquote>\n\n\n\n<p>Technically, this gives you correct UI.<\/p>\n\n\n\n<p>But it introduces a new problem:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>You\u2019re now <strong>blocking interaction on auth resolution<\/strong><\/li>\n\n\n\n<li>On slow networks, the button may be unusable for a long time which could lead to user frustration clicks<\/li>\n<\/ul>\n\n\n\n<p>So the question became:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Are we optimizing for correctness\u2014or usability?<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Why We Didn\u2019t Solve This on the Server<\/h2>\n\n\n\n<p>At this point, the obvious \u201cperfect\u201d solution was:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Render the correct auth state on the server.<\/p>\n<\/blockquote>\n\n\n\n<p>In theory, that would eliminate the flicker entirely.<\/p>\n\n\n\n<p>We even explored it.<\/p>\n\n\n\n<p>The idea was to convert the header into a <strong>server component<\/strong> so we could read authentication state (via cookies) during SSR and render the correct UI immediately.<\/p>\n\n\n\n<p>But in practice, it wasn\u2019t that simple.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The Constraints<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The header is used across multiple applications as a <strong>client component<\/strong><\/li>\n\n\n\n<li>We&#8217;re using Next.js for our framework and it requires <strong>client components for children<\/strong><\/li>\n\n\n\n<li>Moving to server rendering would require <strong>significant architectural changes<\/strong><\/li>\n<\/ul>\n\n\n\n<p>We considered passing auth state down as props from the server.<\/p>\n\n\n\n<p>But that introduced a bigger issue:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Every consuming application would now need to manually handle authentication state and integrate with our auth provider.<\/p>\n<\/blockquote>\n\n\n\n<p>That\u2019s not a small change\u2014that\u2019s a platform-level shift.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">A Key Insight<\/h2>\n\n\n\n<p>Someone pointed out something that shifted the conversation:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Any client-side solution doesn\u2019t remove the delay. It just hides it.<\/p>\n<\/blockquote>\n\n\n\n<p>That forced us to rethink the goal.<\/p>\n\n\n\n<p>Instead of asking:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\u201cHow do we make the UI always correct?\u201d<\/p>\n<\/blockquote>\n\n\n\n<p>We asked:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\u201cWhat actually happens if the UI is temporarily wrong?\u201d<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Solution: Optimize for Behavior<\/h2>\n\n\n\n<p>We changed the button from a pure action to a <strong>link to the login page<\/strong>.<\/p>\n\n\n\n<p>And we let the login page handle the logic.<\/p>\n\n\n\n<p>Now:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Logged-out user clicks \u2192 goes to login page (expected)<\/li>\n\n\n\n<li>Logged-in user clicks \u2192 immediately redirected back (already authenticated)<\/li>\n<\/ul>\n\n\n\n<p>Meanwhile:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>If the user stays on the page, the button eventually updates to the correct state<\/li>\n\n\n\n<li>\u201cLog out\u201d continues to behave as expected<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Why This Works<\/h2>\n\n\n\n<p>We shifted our priority:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Before<\/h3>\n\n\n\n<p>Guarantee <strong>correct UI immediately<\/strong><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">After<\/h3>\n\n\n\n<p>Guarantee <strong>correct behavior always<\/strong><\/p>\n\n\n\n<p>That distinction matters.<\/p>\n\n\n\n<p>Because from the user\u2019s perspective:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The system never breaks<\/li>\n\n\n\n<li>The path forward is always valid<\/li>\n\n\n\n<li>There are no dead ends or confusing states<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Tradeoffs We Accepted<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The label may briefly be incorrect<\/li>\n\n\n\n<li>The UI becomes <strong>eventually consistent<\/strong>, not immediately accurate<\/li>\n<\/ul>\n\n\n\n<p>But in exchange, we get:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Immediate interactivity<\/li>\n\n\n\n<li>Resilience to slow connections<\/li>\n\n\n\n<li>Simpler implementation without server-side auth rendering<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Engineering Is About Constraints<\/h2>\n\n\n\n<p>This wasn\u2019t really about a login button.<\/p>\n\n\n\n<p>It was about how we think as engineers.<\/p>\n\n\n\n<p>It\u2019s easy to chase:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Perfect UI<\/li>\n\n\n\n<li>Immediate correctness<\/li>\n\n\n\n<li>Ideal conditions<\/li>\n<\/ul>\n\n\n\n<p>But real systems operate under constraints:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Network latency<\/li>\n\n\n\n<li>Asynchronous data<\/li>\n\n\n\n<li>Imperfect timing<\/li>\n\n\n\n<li>Multi-app shared components<\/li>\n<\/ul>\n\n\n\n<p>The better question is:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>What\u2019s the worst outcome if this is temporarily wrong?<\/p>\n<\/blockquote>\n\n\n\n<p>In our case:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>A redirect that resolves instantly<\/p>\n<\/blockquote>\n\n\n\n<p>That\u2019s a safe failure.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Final Thought<\/h2>\n\n\n\n<p>Not every problem needs to be solved at the UI layer.<\/p>\n\n\n\n<p>Sometimes the better solution is to make the system <strong>forgiving<\/strong> instead of <strong>perfect<\/strong>.<\/p>\n\n\n\n<p>Because users don\u2019t care if your state is technically correct at every millisecond.<\/p>\n\n\n\n<p>They care that things <strong>work<\/strong>.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>Welcome to my Engineering Lab\u2014where small bugs turn into <strong>better systems thinking<\/strong>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n","protected":false},"excerpt":{"rendered":"<p>In theory, UI should always reflect the correct state. In practice, that\u2019s not always the&hellip;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[],"class_list":["post-32","post","type-post","status-publish","format-standard","hentry","category-engineering-lab"],"_links":{"self":[{"href":"https:\/\/vanianettleford.com\/index.php?rest_route=\/wp\/v2\/posts\/32","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/vanianettleford.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/vanianettleford.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/vanianettleford.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/vanianettleford.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=32"}],"version-history":[{"count":1,"href":"https:\/\/vanianettleford.com\/index.php?rest_route=\/wp\/v2\/posts\/32\/revisions"}],"predecessor-version":[{"id":33,"href":"https:\/\/vanianettleford.com\/index.php?rest_route=\/wp\/v2\/posts\/32\/revisions\/33"}],"wp:attachment":[{"href":"https:\/\/vanianettleford.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=32"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vanianettleford.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=32"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vanianettleford.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=32"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}