Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Mon, 27 Oct 2025 23:31:51 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.3 225069128 chrome-devtools-mcp https://frontendmasters.com/blog/chrome-devtools-mcp/ https://frontendmasters.com/blog/chrome-devtools-mcp/#respond Mon, 27 Oct 2025 23:31:50 +0000 https://frontendmasters.com/blog/?p=7539 I’m no expert here, but I understand an “MCP server” as a way to make an AI system a bit “smarter” by having more context and capabilities. I find AI coding agents pretty darn smart already particularly when they have your entire codebase and your instruction for context. But if you’re using it to build a website, it’s just gotta take your word for it if they code isn’t doing what it thinks it should be doing in the browser. By attaching chrome-devtools-mcp to the AI tool, it can load up the site and get all that context to work with as well, like the DOM, network activity, and console messages. I think that’s pretty huge.

]]>
https://frontendmasters.com/blog/chrome-devtools-mcp/feed/ 0 7539
Choosing the Right Model in Cursor https://frontendmasters.com/blog/choosing-the-right-model-in-cursor/ https://frontendmasters.com/blog/choosing-the-right-model-in-cursor/#respond Wed, 10 Sep 2025 15:09:34 +0000 https://frontendmasters.com/blog/?p=7083 A number of the big players are coming out with their own AI coding assistants (e.g., OpenAI’s Codex, Anthropic’s Claude Code, and Google Gemini CLI). However, one of the advantages of using a third-party tool like Cursor is that you have the option to choose from a wide selection of models. The downside—of course—is that, like Uncle Ben would always say, “With great power comes great responsibility.”

Cursor doesn’t just give you a single AI model and call it a day—it hands you a buffet. You’ve got heavy hitters like OpenAI’s GPT series (now including the newly-released GPT-5), Anthropic’s Claude models (including the shiny new Opus 4.1), Google’s Gemini, along with Cursor’s own hosted options and even local models you can run on your machine.

Different models excel in different areas, and selecting wisely has a significant impact on quality, latency, and cost. Think of it like picking the right guitar for the gig—you could play metal riffs on a nylon-string classical, but wouldn’t you rather have the right tool for the job?

A Word on “Auto” Mode

Cursor also offers Auto mode, which will pick a model for you based on the complexity of your query and current server reliability. It’s like autopilot—but if you care about cost or predictability, it’s worth picking models manually. Cursor’s documentation describes it as selecting “the premium model best fit for the immediate task” and “automatically switch[ing] models” when output quality or availability dips. In practice, it’s a reliability‑first, hands‑off default so you can keep coding without thinking about providers.

Use Auto when you want to stay in flow and avoid babysitting model choice. It’s especially handy for day‑to‑day edits, smaller refactors, explanation/QA over the codebase, and any situation where provider hiccups would otherwise force you to switch models manually. Because Auto can detect degraded performance and hop to a healthier model, it reduces stalls during outages or rate‑limit blips. 

Auto is also a good “first try” when you’re unsure which model style fits—Cursor’s guidance explicitly calls it a safe default. If you later notice the conversation needs a different behavior (more initiative vs. tighter instruction‑following), you can switch and continue. But, with that said, let’s dive into the differences between the models themselves for those situations where you want to take control of the wheel.

Nota bene: A lot of evaluating how “good” a model is for a given task is a subjective art. So, for this post, we’re going to be juggling a careful balance between my own experience and a requisite amount of reading other people’s hot takes on Reddit so that you don’t have to subject yourself to that.

Claude Models (Sonnet, Opus, Opus 4.1)

Claude has become a fan favorite in Cursor, especially for frontend work, UI/UX refactoring, and code simplification. I will say, I like to think that I am pretty good at this whole front-end engineering schtick, but even sometimes, I am impressed.

  • Claude 3.5 Sonnet – Often the “default choice” for coding tasks. It’s fast, reliable, and has a knack for simplifying messy code without losing nuance.
  • Claude 4 Opus – Anthropic’s flagship for deep reasoning. Excellent for architectural planning and critical refactors, though slower and pricier.
  • Claude 4.1 Opus – The newest version, with sharper reasoning and longer context windows. This is the model you pull out when you’re dealing with a sprawling repo or thorny system design and you want answers that feel almost like a senior architect wrote them.

Trade-off: Claude models are sometimes cautious—they’ll decline tasks that a GPT model might at least attempt. But the output is usually more focused and aligned with best practices. I’ve also noticed that Claude has a tendency to get side-tracked and work on other tangentially-related tasks that I didn’t explicitly ask for. That said, I’m guilty of this too.

GPT Models (GPT-3.5, GPT-4, GPT-4o, o3, GPT-5)

OpenAI’s GPT line has been the workhorse of AI coding.

  • GPT-3.5 – Blazing fast and cheap, perfect for boilerplate generation and small tasks.
  • GPT-4 / GPT-4o – Solid all-rounders. Great for logic-heavy work, nuanced refactors, and design patterns. GPT-4o is especially nice as a “daily driver” because it balances cost, speed, and capability.
  • o3 – A variant tuned for better reasoning and structured answers. Handy for debugging or step-by-step problem solving.
  • GPT-5 – The new heavyweight. Think GPT-4 but with significantly deeper reasoning, longer context, and a much better grasp of codebases at scale. It’s particularly strong at handling multi-file architectural changes and design discussions. If GPT-4 was like working with a diligent senior dev, GPT-5 feels closer to having a staff engineer who can keep the whole system in their head.

Trade-off: GPT models sometimes get “lazy”—they’ll sketch a partial solution instead of finishing the job. But when you want factual grounding or logic-intensive brainstorming, they’re hard to beat. GPT-5 in particular tends to go slower and check in more often. So, it’s a bit more of a hands-on experience than the Claude models. That said, given Claude’s tendency to go on side quests, I am not sure this is a bad thing. GPT-5 will often do the bare minimum but then come to you with suggestions for what it ought to do next and I find myself either agreeing or choosing a subset of its suggestions.

Gemini Models (Gemini 2.5 Pro)

Google’s Gemini slots in nicely for certain tasks: complex design, deep bug-hunting, and rapid completions. It’s more of a specialist tool—less universal than Claude or GPT, but very effective when you hit the right workload. Historically, one of the perks of Gemini is that it had a massive context window (around 2 million tokens). In the months since it was released, however, other models have caught up—namely Opus and GPT-5. Even Sonnet 4 now rocks a 1 million token context window.

I typically find myself using Gemini for research tasks. “Hey Gemini, look over my code base and come up with some suggestions for how I can make my tests less flaky and go write them to this file.” Its large context window makes it great for these kinds of tasks. It’s no slouch in your day-to-day coding tasks either. I just typically find myself reaching for something lighter—and cheaper.

DeepSeek Coder

Cursor also offers DeepSeek Coder, a leaner, cost-effective option hosted directly by Cursor. It’s good for troubleshooting and analysis, and useful if you want more privacy and predictable costs. That said, it doesn’t quite match the top-tier frontier models for heavy generative work. 

Local Models (LLaMa2 Derivatives, etc.)

Sometimes you just need to keep everything on your own machine. Cursor supports local models, which are slower and less powerful but guarantee maximum privacy. These shine if you’re working with highly sensitive code or under strict compliance requirements. This is not my area of expertise. Mainly because my four-year-old MacBook can’t run these models at the same speed as one of OpenAI’s datacenters can.

Model Selection Strategy

Here are some general heuristics I’ve found useful:

  • For small stuff (boilerplate, stubs, quick utilities): GPT-4o or a local model keeps things fast and cheap.
  • For day-to-day coding: Claude Sonnet 4 and GPT-4.1  are solid defaults. They balance reliability with performance. Gemini 2.5 Flash is also a strong contender in this department.
  • For heavy lifting (large refactors, architecture, critical business logic): GPT-5 or Claude Opus 4.1 are the power tools. They’re not cheap, but often it costs less to get it right the first time. What I’ll typically do is have them write their plan to a Markdown file, review it, and then let a lighter weight model take over from there.
  • When stuck: Swap models. If Claude hesitates, try GPT. If GPT spins in circles, Claude often cuts to the chase. This is not a super scientific approach, but it’s wildly effective—or at least it feels that way.
  • Privacy first: Use local models or Cursor-hosted DeepSeek when your code should never leave your machine. I’ve traditionally worked on open-source stuff. So, this hasn’t been a huge concern of mine, personally.

Editor’s note: If you really want to level up with your AI coding skills, you should go from here right to Steve’s course: Cursor & Claude Code: Professional AI Setup.

Evaluating New Models

New models drop all of the time, which raises the question: How should you think about evaluating a new model release to see if it’s a good fit for your workflow?

Capability—Can it actually ship fixes in your codebase, not just talk about them? Reasoning‑forward models like OpenAI’s o3 and hybrid “thinking” models like Claude 3.7 Sonnet are pitched for deeper analysis; use them when you expect layered reasoning or ambiguous requirements. 

Behavior—Does it take initiative or wait for explicit instructions? Cursor’s model guide groups “thinking models” (e.g., o3, Gemini 2.5 Pro) versus “non‑thinking models” (e.g., Claude‑4‑Sonnet, GPT‑4.1) and spells out when each style helps. Assertive models are great for exploration and refactors; obedient models shine on surgical edits. 

Context—Do you need a lot of context right now? If you’re touching broad cross‑cutting concerns, enable Max Mode on models that support 1M‑token windows and observe whether plan quality improves enough to justify the slower, pricier runs. Having a bigger context window isn’t always a good thing. Regardless of what the model’s maximum context window size is, the more you load into that window, the longer it’s going to take to process all of those tokens. Generally speaking, having the right context is way better than having more context.

Cost and reliability—Cursor bills at provider API rates; Auto exists to keep you moving when a provider hiccups. New models often carry different throughput/price curves—compare under your real workload, not just benchmarks. Cost is a tricky thing to evaluate because if a model costs more per token, but can accomplish the task in few tokens, it might end up being a bit cheaper when all is said and done.

Here is my pseudo-scientific guide for kicking the tires on a new model.

  1. Freeze variables. Use the same branch, same repo state, and the same prompt for each run. Turn Auto off when you’re pinning a candidate so you’re not measuring routing noise. Cursor’s guide confirms Auto isn’t task‑aware and excludes o3—so when you test o3 or any very new model, pin it. 
  2. Pick three task archetypes. Choose one surgical edit, one bug‑hunt, and one broader refactor. That trio exposes obedience, reasoning, and context behavior in a single pass. Cursor’s “modes” page clarifies that Agent can run commands and do multi‑file edits—ideal for these trials. 
  3. As Peter Drucker (or John Doerr, but I digress)  used to say: Measure what matters. For each task and model, record: did tests pass; how much did it modify; did it follow constraints; how many agent tool calls and shell runs; and wall‑clock duration. Cursor’s headless CLI can stream structured events that include the chosen model and per‑request timing—perfect for quick logging.

Repeat this process with Max Mode if the model you’re evaluating advertises giant context. You’re testing whether the larger window yields better plans or just slower ones.

Wrapping Up

Model choice in Cursor isn’t just about “which AI is best”—it’s about matching the right tool to the task. Claude excels at simplifying and clarifying, GPT shines at reasoning and factual grounding, Gemini offers design chops, and local models guard your privacy.

And with GPT-5 and Opus 4.1 now in the mix, we’re entering a phase where models can reason about your codebase almost like a human teammate. The trick is knowing when to bring in the heavy artillery and when a lighter model will do the job faster and cheaper.

]]>
https://frontendmasters.com/blog/choosing-the-right-model-in-cursor/feed/ 0 7083
Getting Started with Cursor https://frontendmasters.com/blog/getting-started-with-cursor/ https://frontendmasters.com/blog/getting-started-with-cursor/#respond Mon, 08 Sep 2025 15:07:56 +0000 https://frontendmasters.com/blog/?p=7062 I don’t love the term “vibe coding,” but I also don’t like doing tedious things.

Over the last few months, we’ve seen a number of AI-driven development tools. Cursor is probably the most well-known at this point. But big players are starting to come out with their own like OpenAI’s Codex, Anthropic’s Claude Code, Google Gemini CLI, and Amazon’s Kiro.

Think of Cursor as Visual Studio Code’s ambitious younger cousin—the one who not only borrows your syntax highlighting but also brings a full brain along for the ride—and is also a fork of its bigger cousin. In fact, if you weren’t looking closely, you could be forgiven for confusing it with Visual Studio Code.

I should note that Microsoft has also been racing to add Cursor-like features to Visual Studio Code and a lot of what we’re going to talk about here can also apply to Copilot in Visual Studio Code as well.

Editors note: If you really want to level up with your AI coding skills, you should go from here right to Steve’s course Cursor & Claude Code: Professional AI Setup.

Getting Set Up: The Familiar On-Ramp

If you’ve ever installed Visual Studio Code, you already know the drill. Download, install, run. Cursor smooths the landing with a one-click migration from VS Code—your extensions, themes, settings, and keybindings slide right over. Suddenly, you’re in a new editor that looks a lot like home but has some wild new tricks up its sleeve.

Once you’re settled, Cursor gives you a few ways to work with it:

  • Inline Edit (Cmd/Ctrl+K) – Highlight some code, and then tell Cursor what you want to happen (e.g. “Refactor this function to use async/await”), and watch Cursor suggest a tidy diff right in front of your eyes. Nothing sneaky—just a controlled, color-coded change you can approve or toss if it’s not what you had in mind.
  • AI Chat (Cmd/Ctrl+L) – This is like ChatGPT, but it knows your codebase. It hands out along-side your editor panes. Ask why a component is behaving weirdly, brainstorm ideas, or generate new code blocks. By default, it sees the current file, but you can widen its gaze to the whole repo with @codebase.
  • The Agent (Cmd/Ctrl+I) – For the big jobs. Describe a goal (“Add authentication with GitHub and Google”), and Cursor will plan the steps, touch multiple files, and even run commands—always asking before it does anything dangerous. This is where you go from “pair programmer” to “project collaborator.”

Some Inspiration for the Quick Editor

The inline editor is Cursor’s scalpel—it’s sharp, precise, and surprisingly versatile once you start leaning on it. A few of my favorite quick tricks:

  • Refactor without the tedium: Highlight a callback hell nightmare, hit Cmd/Ctrl+K, and ask Cursor to rewrite it with async/await. Boom—cleaner code in seconds.
  • Generate boilerplate: Tired of writing the same prop-type interfaces or test scaffolding? Select a stub, tell Cursor what you need, and let it flesh things out.
  • Convert styles on the fly: Need to move from plain CSS to Tailwind or from Tailwind to inline styles? Cursor can handle the translation with a single instruction.
  • Explain before you change: Select a gnarly function and just ask Cursor “explain this.” You’ll get a quick natural-language breakdown before deciding what to tweak.
  • Add guardrails: Highlight a function and say, “Add input validation with Zod,” or “Throw if the input is null.” Cursor will patch in the safety nets.

These tricks work best when you’re hyper-specific with what you want. Think of it less like a magic wand and more like a super helpful, pair-programming buddy who thrives on clear, concrete instructions. That’s the scalpel. But Cursor also gives you bigger hammers when you need them.

Getting the Most Out of the Chat and Agent

As I alluded too above, Chat (Cmd/Ctrl+L) is for conversation and exploration. It’s best for asking “why” or “what if” questions, brainstorming, or generating code you’ll shape yourself. I use this all of the time to think through various approaches before I write any code. I treat it like a co-worker that I’m bouncing ideas off of—except I don’t have to interrupt them.

  • Keep prompts specific (“Explain how this hook manages state across renders” beats “Explain this”).
  • Pull in the right context with @files or @codebase so answers stay grounded in your project.
  • Use it as a sounding board before you start refactoring—it’ll surface tradeoffs you might miss.

The Agent (Cmd/Ctrl+I) is for execution. Think of it as delegating work to a teammate who follows your plan:

  • Start with a high-level description, then ask the agent to generate a step-by-step plan before running anything.
  • Approve changes incrementally—don’t green-light a sweeping set of edits unless you’ve reviewed the plan.
  • Pair it with tests and Git. Strong test coverage makes it easy to validate the agent’s work, and frequent commits let you roll back if things get messy.
  • Use it for repetitive or cross-file tasks—things that would normally take you 20 minutes of hunt-and-peck are often solved in one go.

Here are some examples of things you might choose to toss at an agent:

  • “Add authentication with GitHub and Google using Supabase. Show me the plan first.”
  • “Migrate all class-based components in @components to functional components with hooks.”
  • “Convert this component to use Tailwind classes instead of inline styles.”

In short: chat is your whiteboard, agent is your task runner. Bounce ideas in chat, then graduate to the agent when you’re ready to automate.

Why Context Is Everything

In a large enough code base, you’re not going to be able to keep the entire thing in your head at any given time—and Cursor can’t either. In fact, this is probably one of the few places where you have an edge over an LLM—for now.

If you’re looking to get the most out of Cursor and other tools, then managing context is the name of the game. Sure, Cursor can index your code base, but sometimes that can be too much of a good thing. If you want to get the most out of a tool like Cursor, then you’re going to want to pull in the specific parts of your code base that you want it to know about. Otherwise, it’s hard to blame it if it starts heading off in a direction that you didn’t expect. If you didn’t explain what you wanted or give the necessary context to a human, it’s unlikely that they’re going to have what they need in order to be successful and Cursor is no different. Without context is like a smart intern working blindfolded. It might guess, it might improvise, and sometimes it invents nonsense (“hallucinations” is the fancy term). Feed it the right context, though, and Cursor becomes sharp, fast, and eerily helpful.

Context does a few magical things:

  • Cuts down on guesswork.
  • Keeps answers specific to your code instead of generic boilerplate.
  • Helps the AI reason about structure and dependencies instead of flailing.
  • Saves tokens, which means you save money.

Your job is to do the Big Brain Thinking™ about the overall big picture and then give Cursor the context it needs in order to do the tedious grunt work.

How Cursor Handles Context

Cursor is not leaving you high-and-dry in this regard. It has some built-in smarts: it grabs the current file, recently viewed files, edit history, compiler errors, and even semantic search results. It will follow your dependency graph and get read the first little bit of every file in order to get a sense of what it does.

But the real control comes from explicit context management.

  • @Files / @Folders – Point Cursor to exact code.
  • @Symbols – Zero in on a function, class, or hook.
  • @Docs – Pull in external documentation (yours or the framework’s).
  • @Web – Do a live web search mid-chat.
  • @Git – Bring in commit history or diffs.
  • @Linter Errors – Hand Cursor your error messages so it can fix them.
  • @Past Chats – Keep long conversations coherent.

That’s just the tactical layer. For strategy, Cursor gives you rules and Notepads.

  • .cursor/rules live in your repo, version-controlled, shaping Cursor’s behavior: “Always use React Query,” “Prefer async/await,” “Don’t leave TODO comments.” Think of them as your project’s constitution.
  • Notepads are like sticky notes on steroids—bundles of prompts, docs, and references you can inject whenever needed. They’re local, but great for organizing reusable prompts or team knowledge.

Notepads allow you to keep little snippets of information that you can reference at any time and pull into context—without having to type the same things over and over.

Here is an example of some rules to guide Cursor towards writing TypeScript and/or JavaScript in a way that aligns with your—or my, in this case—preferences:

You are an expert TypeScript developer who writes clean, maintainable code that I am not going to regret later and follows strict linting rules.

- Use nullish coalescing (`??`) and optional chaining (`?.`) operators appropriately

- Prefix unused variables with underscore (e.g., \_unusedParam)

# JavaScript Best Practices

- Use `const` for all variables that aren't reassigned, `let` otherwise
- Don't use `await` in return statements (return the Promise directly)
- Always use curly braces for control structures, even for single-line blocks
- Prefer object spread (e.g. `{ ...args }`) over `Object.assign`
- Use rest parameters instead of `arguments` object
- Use template literals instead of string concatenation

# Import Organization

- Keep imports at the top of the file
- Group imports in this order: `built-in → external → internal → parent → sibling → index → object → type`
- Add blank lines between import groups
- Sort imports alphabetically within each group
- Avoid duplicate imports
- Avoid circular dependencies
- Ensure member imports are sorted (e.g., `import { A, B, C } from 'module'`)

# Console Usage

- Console statements are allowed but should be used judiciously

Best Practices for Keeping Cursor Sharp

The one thing that I’ve learned from using Cursor every day for a few months now is that all of those Best Practices® that you know you’re supposed to do but you might’ve gotten sloppy with in the past? They’re extra important these days. For example, the better your tests are, the easier it is for Cursor to validate whether or not it successfully accomplished a task—and didn’t cause a regression in the process. It’s one thing to manually test your own code over and over, but it’s extra sobering to have to manually test code that you didn’t write. The better your Git etiquette is, the easier it will be to roll back to a known good state in the event that something goes off the rails.

  • Review before you merge. Always. The AI is good, but it’s not omniscient.
  • Commit early and often. Git is still your real safety net.
  • Be precise in prompts. “Make this more efficient” is vague. “Replace recursion with iteration to avoid stack overflow” is crisp.
  • Break it down. Ask Cursor to outline a plan before making changes.
  • Iterate. Think of it like a dialogue, not a vending machine.
  • Mind your open files. The fewer distractions, the better Cursor performs.
  • Keep files lean. Under 500 lines helps Agent mode stay accurate.
  • Stay private when you need to. Ghost Mode ensures nothing leaves your machine.

Wrapping Up

Cursor isn’t just an editor with AI bolted on. With proper context management, it becomes a thoughtful coding partner that amplifies your strengths, fills in gaps, and accelerates the mundane parts of software development. Used well, it’s less about “asking AI to code for me” and more about orchestrating an intelligent partner in your workflow.

TL;DR: The more precisely you guide Cursor, the more it feels like it really understands your project—and that’s when the magic happens.

]]>
https://frontendmasters.com/blog/getting-started-with-cursor/feed/ 0 7062
The return of tech specs https://frontendmasters.com/blog/the-return-of-tech-specs/ https://frontendmasters.com/blog/the-return-of-tech-specs/#respond Thu, 04 Sep 2025 15:08:18 +0000 https://frontendmasters.com/blog/?p=7080 Nicholas C. Zakas:

I’m confident that going forward, software engineers will need to relearn how to create detailed tech specs for complex changes. It’s also likely that AI will help write and review these specs before implementing them. It’s time to embrace tech specs again because they can be a key to advancing your career.

Tech (or Design) documents are a good idea no matter what, and it’s interesting that AI tools are what is making them more popular again. But not surprising. Everybody works best with lots of context and a plan.

]]>
https://frontendmasters.com/blog/the-return-of-tech-specs/feed/ 0 7080
Out-of-your-face AI https://frontendmasters.com/blog/out-of-your-face-ai/ https://frontendmasters.com/blog/out-of-your-face-ai/#respond Sun, 01 Jun 2025 15:29:05 +0000 https://frontendmasters.com/blog/?p=6007 A very interesting aspect of the AI smashing its way into every software product known to man, is how it’s integrated. What does it look like? What does it do? Are we allowed to control it? UX patterns are evolving around this. In coding tools, I’ve felt the bar being turned up on “anticipate what I’m doing and offer help”. Personally I’ve gone from, hey that’s nice thanks to woah woah woah, chill out, you’re getting in my way.

I’m sure this will be fascinating to watch for a long time to come. For example, “Subtle Mode” in Zed gets AI more “out of your face” and if you want to see a suggestion, you press a button. I love that idea. But I also understand Kojo Osei’s point here: there should be no AI button.

]]>
https://frontendmasters.com/blog/out-of-your-face-ai/feed/ 0 6007
Cloudflare AutoRAG https://frontendmasters.com/blog/cloudflare-autorag/ https://frontendmasters.com/blog/cloudflare-autorag/#respond Wed, 21 May 2025 15:56:52 +0000 https://frontendmasters.com/blog/?p=5936 I enjoyed this video from Kristian Freeman from Cloudflare on building something quickly with their AutoRAG feature. RAG (Retrieval-Augmented Generation), as I understand it, means that you’re going to ask an AI model a question, but you want that answer informed by a whole corpus of documents. As in, ask the question “how do I animate a line like it’s being drawn?” and have it be informed by every single blog post I’ve ever written. This feature means you can chuck those blog posts in an R2 bucket and with a bit of setup it does the rest. Feels like how this stuff ought to work.

]]>
https://frontendmasters.com/blog/cloudflare-autorag/feed/ 0 5936
ChatGPT and the proliferation of obsolete and broken solutions to problems we hadn’t had for over half a decade before its launch https://frontendmasters.com/blog/chatgpt-and-old-and-broken-code/ https://frontendmasters.com/blog/chatgpt-and-old-and-broken-code/#comments Tue, 20 May 2025 15:57:01 +0000 https://frontendmasters.com/blog/?p=5808 It was a lovely day on the internet when someone asked how to CSS animated gradient text like ChatGPT’s “Searching the web” and promptly got an answer saying “Have you tried asking ChatGPT? Here’s what it told me!” – well, maybe not these exact words, but at least it rhymes.

Both the question and this answer have since been deleted. But we still have the chat link that got posted in the answer and we’re going to look into it.

Screenshot of ChatGPT Generated Code
screenshot of the code produced by ChatGPT

This is the code that ChatGPT spat out in text format:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Text Color Loading Animation</title>
    <style>
      body {
        background-color: #111;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
      }

      .loading-text {
        font-size: 3rem;
        font-weight: bold;
        background: linear-gradient(90deg, #00f, #0ff, #00f);
        background-size: 200% auto;
        background-clip: text;
        -webkit-background-clip: text;
        color: transparent;
        -webkit-text-fill-color: transparent;
        animation: shimmer 2s linear infinite;
      }

      @keyframes shimmer {
        0% {
          background-position: -100% 0;
        }
        100% {
          background-position: 100% 0;
        }
      }
    </style>
  </head>
  <body>
    <div class="loading-text">Loading...</div>
  </body>
</html>

Now you may be thinking: what’s the problem with this code, anyway? If you copy-paste into CodePen, it does produce the desired result, doesn’t it?

Well, we also get the exact same result if we replace this CSS:

.loading-text {
  font-size: 3rem;
  font-weight: bold;
  background: linear-gradient(90deg, #00f, #0ff, #00f);
  background-size: 200% auto;
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  animation: shimmer 2s linear infinite;
}

@keyframes shimmer {
  0% {
    background-position: -100% 0;
  }
  100% {
    background-position: 100% 0;
  }
}

with this CSS:

.loading-text {
  font-size: 3rem;
  font-weight: bold;
  background: linear-gradient(90deg, #00f, #0ff, #00f) -100%/ 200%;
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  animation: shimmer 2s linear infinite;
}

@keyframes shimmer {
  to {
    background-position: 100%;
  }
}

You might think the ChatGPT solution includes some fallbacks that maybe make it work in more browsers. You’d be wrong. There are exactly zero browsers in which the ChatGPT solution isn’t broken, but the alternative above is. Zero! Not a single one. In all of the (very old) browsers where the alternative above breaks, the ChatGPT solution breaks too.

The history of gradient text solutions and what ChatGPT gets wrong

Let’s go some 15 years back in time. I discovered CSS in August 2009, just as it was getting new shiny features like transforms and gradients. One of the first tricks I came across online in early 2010 was precisely this — creating image text in general and CSS gradient text in particular.

The declaration I ditched completely from what ChatGPT generated was:

-webkit-text-fill-color: transparent;

This answer only included -webkit-text-fill-color, though I’ve seen versions of this circulating online that use:

-webkit-text-fill-color: transparent;
        text-fill-color: transparent;

There is no such thing as text-fill-color. There isn’t even a standard spec for it. It’s just something that was implemented in WebKit with a prefix almost 2 decades ago and then became used enough on the web that other browsers had to support it too. With the -webkit- prefix. So the prefixed version is the only one that has ever been implemented in any browser.

While WebKit introduced this property alongside -webkit-text-stroke, its usage would end up far exceeding that of -webkit-text-stroke. This is something that started about 15 years ago when -webkit-text-fill-color became a common tactic for making text transparent only in WebKit browsers. Precisely for image text in general and then a lot more often for gradient text in particular.

At the time, clipping backgrounds to text was only supported in WebKit browsers via the (back then) non-standard -webkit-background-clip: text. If we wanted to get a visible gradient text this way, we had to be able to see through the actual text on top of the clipped background. Problem was, back in 2010, if we set color to transparent, then we’d get:

  • Gradient text as desired in browsers supporting both -webkit-background-clip: text and CSS gradients
  • No visible text and a gradient rectangle in browsers supporting CSS gradients, but not -webkit-background-clip: text (in 2010, this would have been the case for Firefox)
  • Nothing visible at all in browsers not supporting CSS gradients (remember the first IE version to support CSS gradients was IE10, which only came out in 2012; plus Opera still had its own engine back then and wouldn’t get CSS gradients for another year)
Visual representation of the three cases described above.
the state of things in 2010

So given the only browsers we could get gradient text in at the time were WebKit browsers, it made sense to restrict both setting a gradient background and making the text transparent to WebKit browsers. At the time, all gradients were prefixed, so that made the first part easier. For the second part, the solution was to use -webkit-text-fill-color to basically override color just for WebKit browsers.

So to get an aqua to blue gradient text with a decent in-between blue text fallback in non-supporting browsers, the code we had to write in 2010 looked like this:

color: #07f;
background: 
  -webkit-gradient(linear, 0 0, 100% 0, 
    color-stop(0, #0ff), color-stop(1, #00f));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent

Note that at the time, this was seen as a temporary WebKit-only solution, as other options for getting such gradient text were being discussed. The “temporary” solution stuck, I guess. And so it’s now in the spec.

And yes, that is a different gradient syntax (which, by the way, is still supported to this day in Chrome). The syntax for CSS gradients went through multiple iterations. After this one, we had another -webkit- prefixed version, a lot more similar to what we have today, but still not the same. If you’re curious, you can see a bit more about the support timeline in my 10 year old rant about using gradient generators without understanding the code they spit out and without having a clue how much of it is really necessary. Because before we had AI chatbots spitting out too much needless CSS for the desired result, we had all these CSS3 generators doing pretty much the same! Like a song says, oh, it was different… and yet the same!

Using -webkit-text-fill-color to override color just for WebKit browsers, we got:

Visual representation of possible results in 2010, when using -webkit-text-fill-color: transparent to allow seeing through the text in WebKit browsers. If all -webkit- prefixed properties were supported, we got a left to right, aqua to blue gradient text. If none were supported, we got an in-between blue fallback. If -webkit-background-clip: text was not supported while -webkit-gradient and -webkit-text-fill-color were, then we got no visible text on a left to right, aqua to blue gradient rectangle. If -webkit-text-fill-color was supported and set the text to transparent, but -webkit-gradient wasn't, then nothing would show up. In this final case, the support status for -webkit-background-clip: text was irrelevant.
the state of things in 2010

Better, I guess, but not ideal.

Since support for -webkit-text-fill-color came before for CSS gradients, that left a gap where the text would be made transparent, but there would be no gradient to be clipped to (making the support status for -webkit-background-clip: text irrelevant). In this case, there would be no visible text and no gradient… just nothing but empty space.

I first thought that anyone who’d be using one of these browsers that would always be the first to support new and shiny things would also care about keeping them updated, right? Right?

Wrong! Some months later, I went to a job interview. I’ve always been a show-off, I’m like a fish in the water when live coding, so I jumped on the chance to actually get in front of a computer and impress with all the cool things I could do thanks to the new CSS features. They were using a Chrome version that was quite a bit behind the then current one. CSS gradients did not work.

I started using Modernizr to test for CSS gradient support after that. We didn’t have @supports back then, so Modernizr was the way to go for many years.

Then there was the Android problem – CSS gradients being supported, but not -webkit-background-clip: text, though support tests returned false positives. Since I didn’t have a smartphone and I don’t think I even knew anyone who had one at the time, I couldn’t test if any fix found on the internet would work and I don’t recall anyone ever complaining, so I confess I never even bothered with trying to cover this case.

Then things started to get even funnier.

In 2011, WebKit browsers started supporting a newer, different gradient syntax. Still with a prefix, still different from the standard one. Most notably, angles were going in the opposite direction and the gradient start was offset by 90° relative to the current version, meaning the  gradient start was taken to be at 3 o’clock, rather than at 12 o’clock like in the case of the current standard one.

This really caught on. Prefixes were meant to solve a problem, but a lot of developers skipped reading the instructions on the box, so they wrote code with either too many prefixed versions of the same properties or, more commonly, not enough… usually just the WebKit one. Which meant other browsers started considering supporting the -webkit- prefix too.

There was a lot written about it at the time, but basically what this meant was that Opera first implemented -webkit- prefixes in 2012 and then switched away from Presto altogether a year later, at around the same time Blink was announced. Another year later, IE added the -webkit- prefix too. This would carry over to Edge and then Edge would also move to Blink. And then in 20152016, Firefox also implemented some non-standard -webkit- prefixed properties and mapped some other -webkit- prefixed properties to their -moz- or standard equivalents.

What this meant was that for a while, we had support for -webkit- prefixed gradients in non-WebKit browsers… but not support for -webkit-text-fill-color-webkit-background-clip: text, so the following code:

color: #07f;
background: -webkit-linear-gradient(left, #0ff, #00f);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent

would for example produce a mid blue text on an aqua to blue gradient rectangle in Opera in-between starting to support the -webkit- prefix and switching to Blink. Not very readable, right?

The text 'Loading...' in an in-between blue on top of a left to right, aqua to blue gradient. The contrast between the text and the background is poor. It could at most pass AA for large text (above 18pt or bold above 14pt) and AA for user interface components and graphical objects on the left end, but would fail WCAG 2.0 and 2.1 everywhere else.
bad contrast

So the solution for that was to add another fully transparent -o- prefixed gradient after the -webkit- one:

color: #07f;
background: -webkit-linear-gradient(left, #0ff, #00f);
background: -o-linear-gradient(transparent, transparent);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent

Quite the mess indeed.

Since 2016, all browsers except Opera Mini have supported clipping backgrounds to text. Given the lack of support for clipping backgrounds to text was why we avoided setting color to transparent, what’s the point of still using -webkit-text-fill-color now?

You might say support. Indeed, there are people stuck on old browsers without the option to update because they are stuck on old operating systems which they cannot upgrade because of the age of hardware. I live in Romania and not only do I know people still using Windows XP on computers from the ’90s, I have seen that some public institutions still use Windows XP too. Newer browser versions don’t work there… but even so, the last browser versions that support Windows XP all support clipping backgrounds to text. And with automatic updates, all I’ve seen have been warnings about being unable to update to a newer version, not browsers stuck even further back.

Even if they were that far back, the ChatGPT solution sets color to transparent alongside -webkit-text-fill-color. The whole point of using -webkit-text-fill-color before clipping backgrounds to text became cross-browser was to avoid setting color to transparent, which ChatGPT isn’t doing because it’s dumping that declaration in there too. So it’s not improving support, it’s just adding redundant code. Or breaking the solution for a problem that had become obsolete over half a decade before ChatGPT was launched. Whichever you prefer.

In any case, the ChatGPT code is what we call “struţocămilă” in Romanian – an impossible animal that’s half ostrich, half camel.

A strange hybrid animal with the body of an ostrich and the head and neck of a camel.
even when it came to creating this wonder, AI couldn’t do a better job than a human in Photoshop

Not to mention today we have something far better for handling such situations: @supports!

I get the extending support argument for not setting background-clip: text in the shorthand or even unprefixed. Though now supported unprefixed and in the shorthand in the current versions of all major desktop and mobile browsers, support doesn’t extend to the last browser versions on Windows XP, 7 or 8.

That being said, if manually including both the prefixed and standard versions of a property, please always include the unprefixed one last. There are very rare situations where having the prefixed version of a property override the standard one might make sense for getting around some weird bug, but that’s definitely not the case here.

On background-size

Another thing you may have noticed is the ChatGPT solution sets background-size separately, not in the shorthand and that it uses two values, not just one, the second one being auto.

There is a historical (although also completely pointless today) reason behind this too. 15 years ago, when we first got this pure CSS gradient text technique, most browsers didn’t yet support setting background-size in the shorthand. So it was set separately. But that has not been an issue for over a decade.

Another issue with background-size was that initially, WebKit browsers implemented an earlier draft of the spec where a missing second value was taken to be equal to the first value, rather than auto, which in the case of gradients means the height of the box specified by background-origin (by default the padding-box). However, in the case of a horizontal gradient like the one we have here, the second background-size value is completely irrelevant, regardless of the background-position. Whether it’s 200% (duplicating the first value) or auto (100% of the padding-box height in this case), the visual result is always exactly the same for horizontal gradients.

So there is no reason why setting it in the shorthand with a single value wouldn’t produce the exact same result. And that has been the case for over a decade.

Finessing things

Similar to how we can omit the second background-size value, we can also omit it for background-position. The default is 50%, but any other value produces the exact same result in the case of any gradient covering the entire height of the background-origin box. And in the case of a horizontal gradient like we have here, it wouldn’t matter even if we had a different background-size height.

We can also easily omit one of the two end keyframes and set its background-position in the shorthand. Then the missing end keyframe gets generated out of there. This is not a new feature, I’ve been using this for over a decade.

Then I’m not a fan of those body styles. Firstly, without zeroing its default margin, setting its height to 100vh creates a scrollbar. Secondly, it can also be problematic even when setting margin: 0. Just don’t do it and do this instead, it has better support than dvh:

html, body { display: grid }

html { height: 100% }

body { background: #222 }

.loading-text { place-self: center }

Finally, default fonts might be ugly, so let’s go for a prettier one. We could also make it scale with the viewport within reasonable limits.

font: 900 clamp(2em, 10vw, 10em) exo, sans-serif

Here’s a CodePen demo:

This is not just ChatGPT

ChatGPT is in the title because it was what got used in this case. But this “dump in old popular solutions and sprinkle in some modern CSS to create a grotesque hybrid” is not unique to ChatGPT.

I saw Gemini spit out this monstruosity (that doesn’t even produce the desired result) just a day earlier:

screenshot

It makes sense. Older solutions have had more time to become more popular and they’re often at the top in search results too. But that doesn’t necessarily mean they’re still the best choice today. At least when you’re looking at an article or a Stack Overflow answer, you can check the date. An AI solution might link to resources (and in one instance, I discovered it was my own 12-year old, obsolete StackOverflow answer that was being referenced), but if it doesn’t or if you don’t check the resources, then you can never know just how outdated a technique might be. Or how badly it got messed up on the way.

]]>
https://frontendmasters.com/blog/chatgpt-and-old-and-broken-code/feed/ 6 5808
Revenge of the junior developer https://frontendmasters.com/blog/revenge-of-the-junior-developer/ https://frontendmasters.com/blog/revenge-of-the-junior-developer/#respond Fri, 04 Apr 2025 15:53:06 +0000 https://frontendmasters.com/blog/?p=5532 Steve Yegge makes the prediction in Revenge of the junior developer that this current wave of AI “agents” that help us code with more capability than just type ahead suggestions and refactorings, like file creation, command line usage, and more, is just the fourth wave of six. The fifth is an individual developer managing multiple agents (a “cluster”) which gives way to “fleets” of agents in the sixth.

The revenge part? Junior developers, less stuck in their ways, will be quicker to adapt to these new ways of working. I think it’s interesting to think about, but my experience, the value of someone who deeply knows how to understand a system and fix and prevent problems, traits that define a senior developer, will remain as valuable as ever.

]]>
https://frontendmasters.com/blog/revenge-of-the-junior-developer/feed/ 0 5532
Comparing local large language models for alt-text generation https://frontendmasters.com/blog/comparing-local-large-language-models-for-alt-text-generation/ https://frontendmasters.com/blog/comparing-local-large-language-models-for-alt-text-generation/#respond Mon, 17 Mar 2025 17:21:14 +0000 https://frontendmasters.com/blog/?p=5415 Dries Buytaert:

I have 10,000 photos on my website. About 9,000 have no alt-text. I’m not proud of that, and it has bothered me for a long time.

Going back and hand-writing alt for 9,000 images isn’t a job that most of us can fit into our lives and I empathize. Are computers up for the task finally? Rather than pick a model and wire it up and do it, Dries wanted to do some testing and pick the best option. The answer isn’t perfectly clear, but there are some decent options and other forward thinking ideas here.

]]>
https://frontendmasters.com/blog/comparing-local-large-language-models-for-alt-text-generation/feed/ 0 5415
HTML & CSS for a One-Time Password Input https://frontendmasters.com/blog/html-css-for-a-one-time-password-input/ https://frontendmasters.com/blog/html-css-for-a-one-time-password-input/#comments Wed, 05 Feb 2025 22:11:57 +0000 https://frontendmasters.com/blog/?p=5067 You know those One Time Password inputs? The UI is typically 4 or 6 numbers with individual inputs. Just from today…

Here’s the UI the Safeway app uses in the Pharmacy section to log in.
Here’s how Substack authenticates.

Brad Frost was blogging about them recently. They certainly have some issue! Here’s one he spells out that I agree with wholeheartedly:

I don’t like the pattern where each digit is its own text box. It’s an affordance that’s supposed to make things clearer, but it doesn’t (for me at least). Can I paste? Where do I paste? Is my paste going to carry over into all of the little boxes? Half the time there’s a dash in the code; does that get included?

It’s awfully tricky to get right, considering the user confusion that can happen before you’re interacting with those little boxes. And once you are, the experience better be awfully accommodating.

A while back I read an article by Phuoc Nguyen about them called Build an OTP input field. I’d say all-in-all, Phuoc did a good job. The design and user experience was considered, like using the arrow keys to move between the inputs and handling “paste”. I’d say accessibility too but I feel like this is complicated enough of an interaction I can’t personally vouch for that.

But I’m also also like — damn — that’s complicated. That’s a lot of JavaScript code. Why is this so hard? And what would happen without JavaScript? Seems like it would be a pretty gnarly experience. A particular thing that makes it hard is making each character a separate <input /> in the HTML.

<div class="otp">
    <input type="text" maxlength="1" />
    <input type="text" maxlength="1" />
    <input type="text" maxlength="1" />
    <input type="text" maxlength="1" />
</div>

That complicates validation, input, pasting, accessibility, navigation… literally everything.

And then I was like… why can’t this just be one input? The rectangles behind the numbers is just visual theater. Just a bit of trendy decoration. It’s just a styling concern, not a semantic, usability, or any other concern.

So I was like… I’m just gonna make those rectangles background-images and see if that works. So I built a demo, but it had a flaw: as you typed the last character, the value would kinda slide one direction and look bad. You can see it here.

But I posed the challenge in our ShopTalk Discord and others had some ideas. Josh Collingsworth had an idea where you could cover up some area at the end and prevent the movement issue (the yellow block would be white or whatever covers up properly). Alex Fimion did it a smidge cleaner by covering the last bit with background-image instead of a pseudo-element. Here’s that:

Is that better than the 4-inputs approach?

I’m giving an only-slightly-hesitant thumbs up 👍. My hesitation is that in order for this to look right, there is a lot of “magic number” usage. That is, numbers that are just visually tweaked by hand to make it all work, and based on finicky things like font metrics (which might change over time and with different fonts) rather than hard foundational layout.

So let’s call this a pretty good take. I think when you consider the HTML used alone you can see using a one-input approach feels best:

<input
  required
  type="text"
  autocomplete="one-time-code"
  inputmode="numeric"
  maxlength="4"
  pattern="\d{4}"
>

In fact, if I started having problems with the look of the “rectangles behind the numbers” approach, I’d just make it a big ol’ single input without the individual rectangles. Like I said, I feel those are just something of a design trend anyway.

What does AI want to do?

Just as a fun little exercise, I used the very generic prompt create a 4 digit PIN input UI with zero rounds of feedback/iteration across a number of different scaffolding tools.

v0 used TSX and four individual inputs and tried to help with dealing with the complex UX. It worked decently once, then two more tries it tried using shadcn and some toast library and all kinds of stuff and just failed to run at all.
Copilot wanted four individual inputs and helped with the moving to the next input after any character typed, but none of the other issues.
Cascade (in Windsurf) went with a single input (!) and got the HTML pretty decent.
Bolt.new used a React/TSX/Tailwind approach with four inputs and handled pasting, input moving, etc pretty nicely.

I’m fairly confident that if you provided a more elaborate prompt with more specific requirements and were willing to go through rounds of iteration, you could get what you want out of these tools. I just found it interesting that by default, based on the code they were trained on, that what you get tends to focus on using multiple inputs, not to mention a heap of tools you don’t ask for.

]]>
https://frontendmasters.com/blog/html-css-for-a-one-time-password-input/feed/ 4 5067