Skip to content

The CSS / Utility hybrid approach with Tailwind v4

The CSS / Utility hybrid approach with Tailwind v4

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Just recently, the progress on the next major version of Tailwind CSS was open-sourced, along with a preview post. This is very exciting news to me, and I love the direction they’ve taken with this release. Tailwind can be a pretty polarizing topic among developers, but I believe that we can find some common ground and live in harmony. In this post, we’ll look at how the CSS + Tailwind hybrid approach is getting an upgrade in v4 and some of the patterns that we can take advantage of to provide a stellar styling experience on our websites.

The hybrid approach

It doesn’t matter what your personal take on Tailwind is, the fact is utility classes are an incredibly useful pattern for styling websites with CSS. I’m not sold on the hard-line, all-or-nothing approach that Tailwind has always recommended. Having a class string that extends way off the screen on an ultra-wide monitor is not pretty. Combining traditional CSS classes with utility classes helps cut down on the styling noise in your markup and provides some advantages when applied correctly. This post will cover some useful patterns and best practices for using CSS and Tailwind in harmony

Tailwind going “CSS-native”

In the v4 introduction post, they state

A major goal of Tailwind CSS v4.0 is making the framework feel CSS-native, and less like a JavaScript library.

I love that they are heading in this direction, and I’m imagining Tailwind as the toolkit for styling websites with CSS. It is going to replace the plethora of tools from the past and provide support for all the modern niceties like:

  • Importing CSS files with @import
  • Vendor prefixing (no more autoprefixr)
  • Improved browser support for modern features like CSS nesting and media query ranges.

This means less worry for us as developers. We can write our code and build our sites using the modern features that the platform provides without sweating all the other details and edge cases. The talented folks at Tailwind and the open-source community have our backs.

Tailwind v4 CSS configuration

The biggest step in the “CSS-native” direction and the feature that I’m most excited about is the CSS configuration. This is great for embracing CSS and Tailwind as one instead of two factions at battle. If you’ve been using Tailwind exclusively for a long time like I have, you might be out of touch with all the great features and tools that CSS provides out of the box now. This is a great opportunity to get back to learning and using the platform.

In Tailwind v4 we do our configuration and theming with CSS variables. To get an idea of what this looks like check out the default theme on GitHub. In our configuration we can override any values from the default theme or extend it with new values.

@import "tailwindcss";

@theme {
  --color-*: initial;

  --color-gray-50: #f8fafc;
  --color-gray-100: #f1f5f9;
  --color-gray-200: #e2e8f0;

  --color-green-800: #3f6212;
  --color-green-900: #365314;
  --color-green-950: #1a2e05;
}

All the values from the theme are exposed as CSS variables. We can reference them in our CSS files or directly in our utility classes.

.badge--red {
	background-color: var(--color-red-50);
	color: var(--color-red-600);
}
<div class="p-[calc(var(--spacing-6)-1px)]">
  <!-- ... -->
</div>

Best practices for combining CSS with utility classes

I can understand why Tailwind recommends using strictly utility classes. It’s easy to shoot yourself in the foot and run into the issues that utility classes set out to solve in the first place. I believe if you follow some general guidelines you can safely have the best of both worlds.

Create focused/specialized classes

CSS tends to get messy when we use classes that are overly specific with styles that apply to just one or a small number of use cases.

Take a calendar where the days are custom checkboxes. You might have some pretty specialized styles for this bit of UI.

.calendar-date {
	color: var(--color-gray-300);
	background-color: var(--color-gray-800);

	&:hover, 
	&:has(input[type="checkbox"]:focus) {
		background-color: var(--color-gray-700);
	}

	& input[type="checkbox"]:checked + time {
		background-color: var(--color-indigo-600);
		color: var(--color-white);
	}
}

Notice that we just add some specific styles that apply to this use case. This gives us the flexibility to keep composing our styles with utility classes.

Some things are better left to utilities

The best way to avoid wandering too far off the path of enlightenment is to stick with utility classes for certain (most?) things. Spacing and layouting are examples of things that are probably best left to utility classes. These typically vary all across a page, and encoding them in a class will make it a lot less flexible.

A code smell to look out for: If you have abstracted something to a class and you end up overriding certain aspects of its styles in different places. Those styles should probably be utility classes.

Classes as components

Tailwind has always recommended using a component system for your UI components so that your utility classes are all defined in a single place. You don’t want to have to edit 50 different files to make a change to the style of your buttons across your website. I’ve found that defining things that might be simple “components” like buttons, badges, links, etc as classes works just as well in most cases.

You can use Tailwind CSS with the @apply directive or traditional class definitions for this:

.badge {
	 @apply inline-flex items-center gap-x-1.5 rounded-md py-0.5 text-sm/5 font-medium sm:text-xs/5;
}

You can even bring the BEM naming convention back to compose variants on top of base component styles. (or name them however you’d like!)

.badge--yellow {
	@apply bg-yellow-400/20 text-yellow-700 ;
}

Now we can easily define badges in our markup:

<span class="badge badge--yellow">Warning</span>

Modern CSS and other interesting utility hybrid patterns

I think a lot of us are realizing that modern CSS is pretty great. There are a ton of awesome new features gaining support across major browsers, like CSS nesting, container queries, and more! More and more people are also coming around to the utility of utility classes. If you want to dive deeper and see other interesting approaches and patterns when combining modern CSS with utility classes, check out the post: Modern CSS patterns in Campfire.

Summary

I’m looking forward to the Tailwind CSS configuration and other awesome updates coming in v4. Using Tailwind doesn’t have to be a one-size-fits-all approach. If you want to write some CSS, I say go for it! Hopefully, some of the points in this post will lead to success if you decide to do so.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

Exploring Open Props and its Capabilities cover image

Exploring Open Props and its Capabilities

Exploring Open Props and its Capabilities With its intuitive approach and versatile features, Open Props empowers you to create stunning designs easily. It has the perfect balance between simplicity and power. Whether you're a seasoned developer or just starting, Open Props makes styling websites a breeze. Let's explore how Open Props can help your web development workflow. What is Open Props Open Props is a CSS library that packs a set of CSS variables for quickly creating consistent components using “Sub-Atomic” Styles. These web design tokens are crafted to help you get great-looking results from the start using consistent naming conventions and providing lots of possibilities out-of-the-box. At the same time, it's customizable and can be gradually adopted. Installing open props There are many ways to get started with open props, each with its advantages. The library can be imported from a CDN or installed using npm. You can import all or specific parts of it, and for greater control of what's bundled or not, you can use PostCSS to include only the variables you used. From Zero to Open Props Let's start with the simplest way to test and use open props. I'll create a simple HTML file with some structure, and we'll start from there. Create an index.html file. ` Edit the content of your HTML file. In this example, we’ll create a landing page containing a few parts: a hero section, a section for describing the features of a service, a section for the different pricing options available and finally a section with a call to action. We’ll start just declaring the document structure. Next we’ll add some styles and finally we’ll switch to using open props variables. ` To serve our page, we could just open the file, but I prefer to use serve, which is much more versatile. To see the contents of our file, let's serve our content. ` This command will start serving our site on port 3000. Our site will look something like this: Open-props core does not contain any CSS reset. After all, it’s just a set of CSS variables. This is a good start regarding the document structure. Adding open props via CDN Let's add open-props to our project. To get started, add: ` This import will make the library's props available for us to use. This is a set of CSS variables. It contains variables for fonts, colors, sizes, and many more. Here is an excerpt of the content of the imported file: ` The :where pseudo-class wraps all the CSS variables declarations, giving them the least specificity. That means you can always override them with ease. This imported file is all you need to start using open props. It will provide a sensible set of variables that give you some constraints in terms of what values you can use, a palette of colors, etc. Because this is just CSS, you can opt-out by not using the variables provided. I like these constraints because they can help with consistency and allow me to focus on other things. At the same time, you can extend this by creating your own CSS variables or just using any value whenever you want to do something different or if the exact value you want is not there. We should include some styles to add a visual hierarchy to our document. Working with CSS variables Let's create a new file to hold our styles. ` And add some styles to it. We will be setting a size hierarchy to headings, using open props font-size variables. Additionally, gaps and margins will use the size variables. ` We can explore these variables further using open-props’ documentation. It's simple to navigate (single page), and consistent naming makes it easy to learn them. Trying different values sometimes involves changing the number at the end of the variable name. For example: font-size-X, where X ranges from 0 to 8 (plus an additional 00 value). Mapped to font-sizes from 0.5rem up to 3.5rem. If you find your font is too small, you can add 1 to it, until you find the right size. Colors range from 0-12: –red-0 is the lightest one (rgb(255, 245, 245)) while –red-12 is the darkest (rgb(125, 26, 26)). There are similar ranges for many properties like font weight, size (useful for padding and margins), line height and shadows, to name a few. Explore and find what best fits your needs. Now, we need to include these styles on our page. ` Our page looks better now. We could keep adding more styles, but we'll take a shortcut and add some defaults with Open Props' built in normalized CSS file. Besides the core of open props (that contains the variables) there’s an optional normalization file that we can use. Let's tweak our recently added styles.css file a bit. Let’s remove the rules for headings. Our resulting css will now look like this. ` And add a new import from open-props. ` Open props provides a normalization file for our CSS, which we have included. This will establish a nice-looking baseline for our styles. Additionally, it will handle light/dark mode based on your preferences. I have dark mode set and the result already looks a lot better. Some font styles and sizes have been set, and much more. More CSS Variables Let's add more styles to our page to explore the variables further. I'd like to give the pricing options a card style. Open Props has a section on borders and shadows that we can use for this purpose. I would also like to add a hover effect to these cards. Also, regarding spacing, I want to add more margins and padding when appropriate. ` With so little CSS added and using many open props variables for sizes, borders, shadows, and easing curves, we can quickly have a better-looking site. Optimizing when using the CDN Open props is a pretty light package; however, using the CDN will add more CSS than you'll probably use. Importing individual parts of these props according to their utility is possible. For example, import just the gradients. ` Or even a subset of colors ` These are some options to reduce the size of your app if using the CDN. Open Props with NPM Open Props is framework agnostic. I want my site to use Vite. Vite is used by many frameworks nowadays and is perfect to show you the next examples and optimizations. ` Let's add a script to our package.json file to start our development server. ` Now, we can start our application on port 5173 (default) by running the following command: ` Your application should be the same as before, but we will change how we import open props. Stop the application and remove the open-props and normalize imports from index.html. Now in your terminal install the open-props package from npm. ` Once installed, import the props and the normalization files at the beginning of your styles.css file. ` Restart your development server, and you should see the same results. Optimizing when using NPM Let's analyze the size of our package. 34.4 kb seems a bit much, but note that this is uncompressed. When compressed with gzip, it's closer to 9 kb. Similar to what we did when using the CDN, we can add individual sections of the package. For example in our CSS file we could import open-props/animations or open-props/sizes. If this concerns you, don't worry; we can do much better. JIT Props To optimize our bundled styles, we can use a PostCSS plugin called posts-jit-props. This package will ensure that we only ship the props that we are using. Vite has support for PostCSS, so setting it up is straightforward. Let's install the plugin: ` After the installation finishes, let's create a configuration file to include it. ` The content of your file should look like this: ` Finally, remove the open-props/style import from styles.css. Remember that this file contains the CSS variables we will add "just in time". Our page should still look the same, but if we analyze the size of our styles.css file again, we can see that it has already been reduced to 13.2kb. If you want to know where this size is coming from, the answer is that Open Props is adding all the variables used in the normalize file + the ones that we require in our file. If we were to remove the normalize import, we would end up with much smaller CSS files, and the number of props added just in time would be minimal. Try removing commenting it out (the open-props/normalize import) from the styles.css file. The page will look different, but it will be useful to show how just the props used are added. 2.4kB uncompressed. That's a lot less for our example. If we take a quick look at our generated file, we can see the small list of CSS variables added from open props at the top of our file (those that we use later on the file). Open props ships with tons of variables for: - Colors - Gradients - Shadows - Aspect Ratios - Typography - Easing - Animations - Sizes - Borders - Z-Index - Media Queries - Masks You probably won't use all of these but it's hard to tell what you'll be using from the beginning of a project. To keep things light, add what you need as you go, or let JIT handle it for you. Conclusion Open props has much to offer and can help speed your project by leveraging some decisions upfront and providing a sensible set of predefined CSS Variables. We've learned how to install it (or not) using different methods and showcased how simple it is to use. Give it a try!...

CSS Container Queries, what are they? cover image

CSS Container Queries, what are they?

CSS Container queries, what are they? Intro Media queries have always been crucial to building web applications. They help make our apps more accessible and easier to use and ensure we reach most of our audience. Media queries have been essential in frontend development to create unique user interfaces. But now, there’s something new: Container queries. In this blog post, we’ll explore what Container queries are, how they differ from media queries, and why they’re so amazing. So, let’s get started! Refresh on Media queries Media queries have been available in browsers for a long time, but they didn’t become popular until around 2010 when mobile devices started to take off. Media queries let us add specific styles based on the type of device, like screens or printers. This is especially helpful for creating modern, responsive apps. A simple use of Media queries would be changing, for example, a paragraph's font size when the screen width is less than a specific number. ` In this simple example, when the browser’s viewport width is less or equal to 400px, the font size changes to 8px. Notice how straightforward the syntax is: we start with the keyword @media, followed by the type of device it should apply to. In this case, we use screen so it doesn’t affect users who print the page—if you don’t add anything, then it falls back to the default, which is “all” including both print and screen. Then we specify a media feature, in this case, the width. Container queries Container queries are similar to Media queries. Their main function is to apply styles under certain conditions. The difference is that instead of listening to the viewport of the browser, it listens to a container size. Let’s see this example: In the above layout, we have a layout with a sidebar and three cards as the content. Using Media queries we could listen to the viewport width and change the layout depending on a specific width. Like so: ` That’s acceptable, but it requires us to constantly monitor the layout. For example, if we added another sidebar on the right (really weird, but let’s imagine that this is a typical case), our layout would become more condensed: We would need to change our media queries and adjust their range in this situation. Wouldn’t it be better to check the card container’s width and update its styles based on that? That way, we wouldn’t need to worry about if the layout changes, and that’s precisely what container queries are made for! First, to define the container we are going to listen to, we are going to add a new property to our styles: ` The .container class is the one in which our cards reside. By adding the property `container-type, ' we now define this class as a container we want to listen to. We said inline-size as the value to query based on the inline dimensions of the container because we just want to listen to the element's width. The value of container-type will depend on your use case. If you want to listen to both width and height, then size will be a better fit for you. You can also have normal as your container-type value, which means the element won’t act as a query container at all. This is handy if you need to revert to the default behavior. Next, to define our query, we use the new @container CSS at-rule: ` Notice that it is really similar to how we define our Media queries. Now, if we look at the same screen, we will see the following: This is very powerful because we can now style each component with its own rules without changing the rules based on the layout changes. The @container will affect all the defined containers in the scope; we might not want that. We can define the name of our container to specify that we only want to listen to that in specific: ` We can also have a shorthand to define our container and its name: ` Container query length units Container query lengths are similar to the viewport-percentage length units like vh or vw units, but instead of being relative to the viewport, they are to the dimensions of the query container. We have different units, each relative to different dimensions of the container: - cqw: 1% of a query container's width - cqh: 1% of a query container's height - cqi: 1% of a query container's inline size - cqb: 1% of a query container's block size - cqmin: The smaller value of either cqi or cqb - cqmax: The larger value of either cqi or cqb In our example, we could use them to define the font size of our cards: ` Using these units alone isn’t recommended because they’re percentage-based and can have a value we don’t want. Instead, it’s better to use a dynamic range. Using the max function, we can set 2 values and always pick the highest one. Conclusion Container queries bring a fresh and powerful approach to web design but are not meant to replace Media queries. I think their real power shines when used together. Media queries often require constant adjustments as your layout evolves. Container queries, however, let you style individual components based on their dimensions, making the designs more flexible and easier to manage. Adding a new component or rearranging elements won’t force us to rewrite our media queries. Instead, each component handles its styling, leading to cleaner and more organized code. Please note that, as of writing this blog post, they aren’t compatible with all browsers yet. Take a look at this table from caniuse.com: A good fallback strategy for this, when hitting an unsupported browser would be the use of the @support rule, which allows you to apply styles only if the browser supports the CSS feature. For example: ` Ensure your media queries are good enough to keep everything responsive and user-friendly when the condition is unmet. Thank you for reading! Enjoy the extra flexibility that container queries bring to your web designs. Check out a live demo to see it in action. Happy styling!...

Increasing development velocity with Cursor cover image

Increasing development velocity with Cursor

If you’re a developer, you’ve probably heard of Cursor by now and have either tried it out or are just curious to learn more about it. Cursor is a fork of VSCode with a ton of powerful AI/LLM-powered features added on. For around $20/month, I think it’s the best value in the AI coding space. Tech giants like Shopify and smaller companies like This Dot Labs have purchased Cursor subscriptions for their developers with the goal of increased productivity. I have been using Cursor heavily for a few months now and am excited to share how it’s impacted me personally. In this post, we will cover some of the basic features, use cases, and I’ll share some tips and tricks I’ve learned along the way. If you love coding and building like me, I hope this post will help you unleash some of the superpowers Cursor’s AI coding features make possible. Let’s jump right in! Cursor 101 The core tools of the Cursor tool belt are Autocomplete, Ask, and Agent. Feature: Autocomplete The first thing that got me hooked was Autocomplete. It just worked so much better than the tools I had used previously, like GitHub Copilot. It was quicker and smarter, and I could immediately notice the amount of keystrokes that it was saving me. This feature is great because it doesn’t really require any work or skilled prompting from the user. There are a couple of tricks for getting a little bit more out of it that I will share later, but for now, just enjoy the ride! Feature: Ask If you’ve interacted with AI/LLMs before, like ChatGPT - this is what the Ask feature is. It’s just a chat feature you can easily provide context to from your code base and choose which Model to chat with. This feature is best suited for just asking more general questions that you might have queried Google or Stack Overflow for in the past. It’s also good for planning how to implement a feature you’re working on. After chatting or planning, you can switch directly to Agent mode to pick up and take action on something you were cooking up in Ask mode. Here’s an example of planning a simple tic-tac-toe game implementation using the Ask feature: Feature: Agent Agent mode lets the AI model take the wheel and write code, make edits, or take other similar actions on your code base. The goal is that you can write prompts and give instructions, and the Agent can generate the code and build features or even entire applications for you. With great power comes great responsibility. Agents are a feature where the more you put into them, the more you get out. The more skilled you become in using them by providing better prompts and including the right context, you will continue to get better results. The AI doesn’t always get it right, but the fact that the models and the users are both getting better is exciting. Throughout this post, I will share the best use cases, tips, and tricks I have found using Cursor Agent. Here’s an example using the Agent to execute the implementation details of the tic-tac-toe game we planned using Ask: Core Concept: Context After understanding the features and the basics of prompting, context is the most important thing for getting the best results out of Cursor. In Cursor and in general, whenever you’re prompting a chat or an agent, you want to make sure that it has all the relevant information that it needs to provide an answer or result. Cursor, by default, always has some context of your code. It indexes your code base and usually keeps the open buffer in the context window at the very least. At the top left of the Ask or Agent panel, there is an @ button, and next to that are badges for all the current items that have been explicitly added to the context for the current session. The @ button has a dropdown that allows you to add files, folders, web links, past chats, git commits, and more to the context. Before you prompt, always make sure you add the relevant content it needs as context so that it has everything it needs to provide the best response. Settings and Rules Cursor has its own settings page, which you can access through Cursor → Settings → Cursor Settings. This is where you log in to your account, manage various features, and enable or disable models. In the General section, there is an option for Privacy Mode. This is one setting in particular I recommend enabling. Aside from that, just explore around and see what’s available. Models The model you use is just as important as your prompt and the context that you provide. Models are the underlying AI/LLM used to process your input. The most well-known is GPT-4o, the default model for ChatGPT. There are a lot of different models available, and Cursor provides access to most of them out of the box. Model pricing A lot of the most common models, like GPT-4o or Sonnet 3.5/3.7, are included in your Cursor subscription. Some models like o1 and Sonnet 3.7 MAX are considered premium models, and you will be billed for usage for these. Be sure to pay attention to which models you are using so you don’t get any surprise bills. Choosing a Model Some models are better suited for certain tasks than others. You can configure which models are enabled in the Cursor Settings. If you are planning out a big feature or trying to solve some complex logic issue, you may want to use one of the thinking models, like o1, o3-mini, or Deep Seek R1. For most coding tasks and as a good default, I recommend using Sonnet 3.5 or 3.7. The great thing about Cursor is that you have the options available right in your editor. The most important piece of advice that I can give in this post is to keep trying things out and experimenting. Try out different models for different tasks, get a feel for it, and find what works for you. Use cases Agents and LLM models are still far from perfect. That being said, there are already a lot of tasks they are very good at. The more effective you are with these tools, the more you will be able to get done in a shorter amount of time. Generating test cases Have some code that you would like unit tested? Cursor is very good at generating test cases and assertions for your code. The fewer barriers there are to testing a piece of code, the better the result you will get. So, try your best to write code that is easily testable! If testing the code requires some mocks or other pieces to work, do your best to provide it the context and instructions it needs before writing the tests. Always review the test cases! There could be errors or test cases that don’t make sense. Most of the time, it will get you pretty close to where you want to be. Here’s an example of using the Agent mode to install packages for testing and generate unit tests for the tic-tac-toe game logic: Generating documentation This is another thing we know AI models are good at - summarizing large chunks of information. Make sure it has the context of whatever you want to document. This one, in particular, is really great because historically, keeping documentation up to date is a rare and challenging practice. Here’s an example of using the Agent mode to generate documentation for the tic-tac-toe game: Code review There are a lot of up-and-coming tools outside of Cursor that can handle this. For example, GitHub now has Copilot integrated in pull requests for code reviews. It’s never a bad idea to have whatever change set you’re looking to commit reviewed and inspected before pushing it up to the remote, though. You can provide your unstaged changes or even specific commits as context to a Cursor Ask or Agent prompt. Getting up to speed in a new code base Being able to query a codebase with the power of LLM’s is truly fantastic. It can be a great help to get up to speed in a large new codebase quickly. Some example prompts: > Please provide an overview of this project and how to get started developing with it > I need to make some changes to the way that notifications are grouped in the UI, please provide a detailed analysis and pseudo code outlining how the grouping algorithm works If you have a question about the code base, ask Cursor! Refactoring Refactoring code in a code base is a much quicker process in Cursor. You can execute refactors depending on their scope in a couple of distinct ways. For refactors that don’t span a lot of files or are less complex, you can probably get away with just using the autocomplete. For example, if you make a change to something in a file and there are several instances of the same pattern following, the autocomplete will quickly pick up on this and help you tab through the changes. If you switch to another file, this information will still be in context and can be continued most of the time. For larger refactors spanning several files, using the Agent feature will most likely be the quickest way to get it done. Add all the files you plan to make changes to the Agent tab’s context window. Provide specific instructions and/or a basic example of how to execute the refactor. Let the Agent work, if it doesn’t get it exactly right initially, you can always give it corrections in a follow-up prompt. Generating new code/features This is the big promise of AI agents and the one with the most room for mixed results. My main recommendation here is to keep experimenting. Keep learning to prompt more effectively, compare results from different models, and pay attention to the results you get from each use case. I personally get the best results building new features in small, focused chunks of work. It can also be helpful to have a dialog with the Ask feature first to plan out the feature's details that the Agent can follow up on and implement. If there are existing patterns in your codebase for accomplishing certain things, provide this information in your prompts and make sure to add the relevant code to the context. For example, if you’re adding a new form to the web page and you have other similar forms that handle validation and making back-end calls in the same way, Cursor can base the code for the new feature on this. Example prompt: Generate a form for creating a new post, follow similar patterns from the create user profile form, and look to the post schema for the fields that should be included. Remember that you can always follow up with additional prompts if you aren’t quite happy with the results of the first.. If the results are close but need to be adjusted in some way, let the agent know in the next prompt. You may find that for some things, it just doesn’t do well yet. Mentally note these things and try to get to a place where you can intuit when to reach for the Agent feature or just write some of the code the old-fashioned way. Tips and tricks The more you use Cursor, the more you will find little ways to get more out of it. Here are some of the tips and patterns that I find particularly useful in my day-to-day work. Generating UI with screenshots You can attach images to your prompts that the models can understand using computer vision. To the left of the send button, there is a little button to attach an image from your computer. This functionality is incredibly useful for generating UI code, whether you are giving it an example UI as a reference for generating new UI in your application or providing a screenshot of existing UI in your application and prompting it to change details in reference to the image. Cursor Rules Cursor Rules allow you to add additional information that the LLM models might need to provide the best possible experience in your codebase. You can create global rules as well as project-specific ones. An example use case is if your project has some updated dependency with newer APIs than the one on which the LLM has been trained. I ran into this when adding Tailwind v4 to a project; the models are always generating code based on Tailwind v3 or earlier. Here’s how we can add a rules file to handle this use case: ` If you want to see some more examples, check out the awesome-cursorrules repository. Summary Learn to use Cursor and similar tools to enhance your development process. It may not give you actual superpowers, but it may feel like it. All the features and tools we’ve covered in this post come together to provide an amazing experience for developing all types of software and applications....

This Dot AI Field Notes - Anatomy of a Coding Harness cover image

This Dot AI Field Notes - Anatomy of a Coding Harness

A coding agent is not magic, it’s a loop. We call this a harness. The harness is a deterministic layer of code that wraps an LLM. Claude Code is a harness. Codex is a harness. Pi is a harness. The harness, on initialization, provides to the LLM a system prompt defining all tools the harness implements for the LLM. Without the harness, you cannot read or modify files on the user’s local filesystem without them having to copy-and-pasting by hand. The harness is the final place where engineers can customize how coding agents do work before the LLM takes over. Think of the LLM as a train and the harness as the rails the train rides on. Below… one full task executed by a harness, traced step by step....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co