Skip to content

Understanding CSS Gradients

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.

Color gradients can help improve the design aesthetic of a site. But it's not always so easy to write the CSS to make them work the way you want. There are plenty of tools online for visually building gradients and getting back the CSS to implement them. But those tools won't help as much with debugging or tweaking things at the margin. It's worth having a basic idea of how gradients work so you can manually adjust them yourself.

The most basic linear gradient

A linear gradient smoothly transitions between colors in a single direction. So if you had a box, and you wanted to start with black and transition to white, you could do it like this:

background: linear-gradient(black, white);

Here, you've created a CSS class and set the background of any element with that class to have a linear gradient. This gradient will start with the first color, black, and smoothly interpolate to white.

Linear gradient direction

Right now, the top edge of the element will be black and the bottom edge will be white. If we wanted to make the left edge black and the right edge white, we could add a direction specifying that.

-   background: linear-gradient(black, white);
+   background: linear-gradient(to right, black, white);

Now, we're telling the gradient to go from left to right. You can use directions like to top, to top right, to right, to bottom right, etc. When not specified, the default direction is to bottom.

You can also specify direction more granularly with other units. You can use degrees (deg), gradians (grad), radians (rad), or turns (turn). Our to right example could be rewritten as 90deg, 100grad, 1.5708rad, or 0.25turn.

Linear gradient colors

This gradient is...really boring. It is not at all something we'd want to use. Let's pick some better colors!

-   background: linear-gradient(to right, black, white);
+   background: linear-gradient(to right, red, green, blue);

Ok, this gradient is pretty bad too. But at least it has color!

These colors are called color stops. If you want another color in the gradient, just add another color stop. By default, the color stops are distributed evenly across the gradient. But color stops can also accept a position that specify where along the gradient that color should be.

-   background: linear-gradient(to right, red, green, blue);
+   background: linear-gradient(to right, red, green 40%, blue);

By specifying 40% after green, we've set green to appear 40% along the way through the gradient rather than at the default midpoint. I've used percentages here, but you can also use specific units like pixels.

Color stops with positions can also be used to create striped patterns like so:

-   background: linear-gradient(to right, red, green 40%, blue);
+   background: linear-gradient(to right, red 20%, green 20% 40%, blue 40%);

Here, our first color red is solid from the start until 20% along the gradient. Then, green immediately takes over at 20%, and goes until 40%. Finally, blue takes over at 40%, and continues to the end of the gradient. The first color stop is assumed to begin at 0%, and the last is assumed to end at 100%.

Combining gradients

Using colors with transparency, and background blend modes, you can overlay different gradients to achieve a variety of color effects.

background:
  linear-gradient(to right, rgba(200, 40, 10, 1), rgba(230, 40, 10, 0)),
  linear-gradient(to left, hsl(220, 80%, 45%, 1), hsl(220, 80%, 45%, 0)),
  linear-gradient(0.5turn, rgba(20, 230, 30, 1), rgba(20, 230, 20, 0));
background-blend-mode: difference;

Repeating gradients

Let's say you want to create a repeating striped line pattern. Creating all of the necessary color stops would be impractical. But that's where repeating-linear-gradient comes in! Define the necesary color stops once, and watch it tile across the element.

background: repeating-linear-gradient(
  135deg,
  hsl(180, 60%, 70%) 0 10px, /* a 10px-wide blue stripe */
  hsl(0, 0%, 95%) 10px 20px /* a 10px-wide white stripe */
)

Radial gradients

The humble linear gradient transitions colors along a single direction, like an ocean wave moving toward the shore. But the radial gradient is like a ripple in a pond, moving outward in all directions from a single starting point.

background: radial-gradient(black, white);

Notice the default behavior of this basic radial-gradient. The point from which the transition begins defaults to the center of the element. The shape it makes is not circular, but elliptical; it is expanding and contracting to match the shape of the element. Color stops work the same way that they do with linear gradients.

Radial gradients take an optional shape, size, and position before their color stops.

Circle or ellipse

Radial gradients default to an elliptical shape to stretch and fit the element they are part of. But you can also specify they be circular instead, regardless of element dimensions.

.one {
  background: radial-gradient(ellipse, black, white);
}

.two {
  background: radial-gradient(circle, black, white);
}

Radial gradient position

The default center position is limiting for making really interesting gradients. Thankfully, it's very easy to specify a different position with the at keyword.

/* The default is farthest corner. The shape will intersect at the element's corners which are furthest from the starting point.  */
.one {
  background: radial-gradient(circle at top left, black, white);
}

/* With farthest-side, the shape will intersect with the most distant straight edge of the element. */
.two {
  background: radial-gradient(circle at 100%, black, white);
}

/* Same as farthest-corner, except now it's the closest one. */
.three {
  background: radial-gradient(circle at bottom, black, white);
}

/* Same as the farthest-side, except now it's the closest one. */
.four {
  background: radial-gradient(circle at 30% 70%, black, white);
}

Radial gradient size

There are four ways to size a radial gradient in relation to its element. When a radial gradient is drawn, it creates either a circle or ellipse that intersects the edges of the element in a certain way. The gradient is interpolated from the starting point to that invisible shape intersecting the element edge. The radial gradient's size determines where that shape intersects the edges of the element.

/* The default is farthest corner. The shape will intersect at the element's corners which are furthest from the starting point.  */
.one {
  background: radial-gradient(ellipse farthest-corner at 30%, black, white);
}

/* With farthest-side, the shape will intersect with the most distant straight edge of the element. */
.two {
  background: radial-gradient(ellipse farthest-side at 30%, black, white);
}

/* Same as farthest-corner, except now it's the closest one. */
.three {
  background: radial-gradient(ellipse closest-corner at 30%, black, white);
}

/* Same as the farthest-side, except now it's the closest one. */
.four {
  background: radial-gradient(ellipse closest-side at 30%, black, white);
}

Combining radial gradients

Like we did with linear gradients, we can get creative with radial gradients to create unique and colorful backgrounds! By now, you should understand enough about how gradients work in CSS that you can read the code below, and understand how it's constructing the gradient background you see here.

background:
  repeating-radial-gradient(circle, hsl(190, 80%, 70%, 0.2) 0px 35px, transparent 35px 70px),
  radial-gradient(at top right, hsl(150, 70%, 50%, 1), hsl(200, 80%, 40%, 0)),
  radial-gradient(circle at 0% 50%, hsl(300, 75%, 40%), hsl(200, 90%, 30%, 0)),
  radial-gradient(circle at bottom center, hsl(30, 70%, 70%, 1), hsl(30, 70%, 70%, 0));

Conic gradients

There's one last instance of gradient in CSS to cover: the conic gradient. If the linear gradient moves like an ocean wave, and the radial gradient moves like ripples in a move, the conic gradient moves like the hands on a clock. This one is a bit weird compared to linear and radial, and you may not find much use for it. But it's worth letting you know that it exists.

  background: conic-gradient(black, white);

Where previously color stops used percentages or units to designate position in the gradient, in the conic gradient, you would use angles. All the units you used in linear gradients to designate direction can instead be used in conic gradients for color position.

  border-radius: 50%;
  background: conic-gradient(red 0 120deg, green 120deg 240deg, blue 240deg 0deg);

Conic gradients don't have a repeating version like the others, but you can use other background properties to repeat them. Using such properties can have interesting results. As unlikely as it sounds, you can create a checkerboard pattern with a repeating conic gradient!

  background: conic-gradient(red 0 120deg, green 120deg 240deg, blue 240deg 0deg);

Accessibility

While conic gradients can allow you to create interesting things like pie charts and checkerboards, keep in mind that screen readers do not interpret anything from these styles. If you populated a report with pie charts made from conic gradients, a screen reader would not be able to explain the charts to its user. You probably shouldn't try to use these gradients for anything other than aesthetics.

Conclusion

Hopefully this introduction to CSS gradients will help you feel more comfortable working with the code for them directly, and not feeling beholden to an online generator tool. By all means, feel free to use tools to create complex gradients more quickly than writing them by hand! But now you can have confidence that, when the time comes to make tweaks to them in the code, you'll be able to do so yourself.

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

An example-based guide to CSS Cascade Layers cover image

An example-based guide to CSS Cascade Layers

CSS is actually good now! If you’ve been a web developer for a while, you’ll know this hasn’t always been the case. Over the past few years, a lot of really amazing features have been added that now support all the major browsers. Cascading and selector specificity have always been a pain point when writing stylesheets. CSS cascade layers is a new feature that provides us with a lot more power and flexibility for tackling this problem. We no longer need to resort to tricky specificity hacks or order-of-appearance magic. Cascade layers are really easy to get started with. I think the best way to understand how and when they are useful is by walking through some practical examples. In this post, we’ll cover: * What CSS cascade layers are and how they work * Real-world examples of using layers to manage style priorities * How Tailwind CSS leverages cascade layers What are CSS Cascade Layers? Imagine CSS cascade layers as drawers in a filing cabinet, each holding a set of styles. The drawer at the top represents the highest priority, so when you open the cabinet, you first access the styles in that drawer. If a style isn't found there, you move down to the next drawer until you find what you need. Traditionally, CSS styles cascade by specificity (i.e., more specific selectors win) and source order (styles declared later in the file override earlier ones). Cascade layers add a new, structured way to manage styles within a single origin—giving you control over which layer takes precedence without worrying about specificity. This is useful when you need to control the order of styles from different sources, like: * Resets (e.g., Normalize) * Third-party libraries (e.g., Tailwind CSS) * Themes and overrides You define cascade layers using the @layer rule, assigning styles to a specific layer. The order in which layers are defined determines their priority in the cascade. Styles in later layers override those in earlier layers, regardless of specificity or order within the file. Here’s a quick example: ` In this example, since the theme layer comes after base, it overrides the paragraph text color to dark blue—even though both declarations have the same specificity. How Do CSS Layers Work? Cascade layers allow you to assign rules to specific named layers, and then control the order of those layers. This means that: * Layers declared later take priority over earlier ones. * You don’t need to increase selector specificity to override styles from another layer—just place it in a higher-priority layer. * Styles outside of any layer will always take precedence over layered styles unless explicitly ordered. Let’s break it down with a more detailed example. ` In this example: * The unlayered audio rule takes precedence because it’s not part of the reset layer, even though the audio[controls] rule has higher specificity. * Without the cascade layers feature, specificity and order-of-appearance would normally decide the winner, but now, we have clear control by defining styles in or outside of a layer. Use Case: Overriding Styles with Layers Cascade layers become especially useful when working with frameworks and third-party libraries. Say you’re using a CSS framework that defines a keyframe animation, but you want to override it in your custom styles. Normally, you might have to rely on specificity or carefully place your custom rules at the end. With layers, this is simplified: ` There’s some new syntax in this example. Multiple layers can be defined at once. This declares up front the order of the layers. With the first line defined, we could even switch the order of the framework and custom layers to achieve the same result. Here, the custom layer comes after framework, so the translate animation takes precedence, no matter where these rules appear in the file. Cascade Layers in Tailwind CSS Tailwind CSS, a utility-first CSS framework, uses cascade layers starting with version 3. Tailwind organizes its layers in a way that gives you flexibility and control over third-party utilities, customizations, and overrides. In Tailwind, the framework styles are divided into distinct layers like base, components, and utilities. These layers can be reordered or combined with your custom layers. Here's an example: ` Tailwind assigns these layers in a way that utilities take precedence over components, and components override base styles. You can use Tailwind’s @layer directive to extend or override any of these layers with your custom rules. For example, if you want to add a custom button style that overrides Tailwind’s built-in btn component, you can do it like this: ` Practical Example: Layering Resets and Overrides Let’s say you’re building a design system with both Tailwind and your own custom styles. You want a reset layer, some basic framework styles, and custom overrides. ` In this setup: * The reset layer applies basic resets (like box-sizing). * The framework layer provides default styles for elements like paragraphs. * Your custom layer overrides the paragraph color to black. By controlling the layer order, you ensure that your custom styles override both the framework and reset layers, without messing with specificity. Conclusion CSS cascade layers are a powerful tool that helps you organize your styles in a way that’s scalable, easy to manage, and doesn’t rely on specificity hacks or the appearance order of rules. When used with frameworks like Tailwind CSS, you can create clean, structured styles that are easy to override and customize, giving you full control of your project’s styling hierarchy. It really shines for managing complex projects and integrating with third-party CSS libraries....

How to Truncate Strings Easily with CSS cover image

How to Truncate Strings Easily with CSS

You'll often need to truncate text when working with user interfaces, especially when displaying content within a limited space. CSS provides a straightforward way to handle this scenario, ensuring that long text strings are cut off gracefully without affecting the overall layout. CSS Truncation Techniques Single-Line Truncation If you want to truncate a single line of text, CSS provides a simple solution. The key properties to use here are overflow, white-space, and text-overflow. ` Explanation of properties: * white-space: nowrap: This ensures the text stays on a single line, preventing wrapping. * overflow: hidden: This hides any content that overflows the container. * text-overflow: ellipsis: This adds the ellipsis (…) at the end of the truncated text. Multi-Line Truncation While truncating a single line of text is common, sometimes you may want to display multiple lines but still cut off the text when it exceeds a certain number of lines. You can use a combination of CSS properties such as -webkit-line-clamp along with the display and overflow properties. ` Live example: Explanation of properties: * display: -webkit-box: This is a legacy flexbox-like display property that works with the -webkit-line-clamp property. * -webkit-line-clamp: Specifies the number of lines to show before truncating. * -webkit-box-orient: vertical: Ensures the box is laid out vertically for proper multi-line truncation. * overflow: hidden: Prevents content overflow. * text-overflow: ellipsis: Adds an ellipsis after the truncated content. Why Use CSS for Text Truncation Over JavaScript Techniques? While it’s possible to truncate text using JavaScript, CSS is often a better choice for this task for several reasons. Let's explore why CSS-based truncation techniques are generally preferred over JavaScript. Performance Efficiency CSS operates directly within the browser's layout engine, meaning it doesn’t require additional processing or event handling as JavaScript does. When using JavaScript to truncate text, the script needs to run on page load (or after DOM manipulation), and sometimes, it needs to listen for events such as window resizing to adjust truncation. This can introduce unnecessary overhead, especially in complex or resource-constrained environments like mobile devices. CSS, on the other hand, is declarative. Once applied, it allows the browser to handle text rendering without any further execution or processing. This leads to faster load times and a smoother user experience. Simplicity and Maintainability CSS solutions are much simpler to implement and maintain than their JavaScript counterparts. All it takes is a few lines of CSS to implement truncation. In contrast, a JavaScript solution would require you to write and maintain a function that manually trims strings, inserts ellipses, and re-adjusts the text whenever the window is resized. Here's the JavaScript Truncation Example to compare the complexity: JavaScript Truncation Example: ` At the example above, we truncated the text to 50 characters which may be 1 line on large screens and 6 lines on mobile and in that case we will need to add more code to truncate it responsively. As you can see, the CSS solution we used earlier is more concise and readable, whereas the JavaScript version is more verbose and requires managing the string length manually. Responsiveness Without Extra Code With CSS, truncation can adapt automatically to different screen sizes and layouts. You can use relative units (like percentages or vw/vh), media queries, or flexbox/grid properties to ensure the text truncates appropriately in various contexts. If you were using JavaScript, you’d need to write additional logic to detect changes in the viewport size and update the truncation manually. This would likely involve adding event listeners for window resize events, which can degrade performance and lead to more complex code. CSS Example for Responsive Truncation: ` To achieve this in JavaScript, you’d need to add more code to handle the width adjustments dynamically, making it more complex to maintain and troubleshoot. Separation of Concerns CSS handles the presentation layer of your website, while JavaScript should focus on dynamic functionality or data manipulation. By keeping truncation logic within your CSS, you're adhering to the principle of separation of concerns, where each layer of your web application has a clear, well-defined role. Using JavaScript for visual tasks like truncation mixes these concerns, making your codebase harder to maintain, debug, and scale. CSS is purpose-built for layout and visual control, and truncation is naturally a part of that domain. Browser Support and Cross-Browser Consistency Modern CSS properties like text-overflow and -webkit-line-clamp are widely supported across all major browsers. This means that CSS solutions for truncation are generally consistent and reliable. JavaScript solutions, on the other hand, may behave differently depending on the browser environment and require additional testing and handling for cross-browser compatibility. While older browsers may not support specific CSS truncation techniques (e.g., multi-line truncation), fallback options (like single-line truncation) can be easily managed through CSS alone. With JavaScript, more complex logic might be required to handle such situations. Reduced Risk of Layout Shifting JavaScript-based text truncation risks causing layout shifting, especially during initial page loads or window resizes. The browser may need to recalculate the layout multiple times, leading to content flashing or jumpy behavior as text truncation is applied. CSS-based truncation is applied as part of the browser’s natural rendering flow, eliminating this risk and ensuring a smoother experience for the user. Conclusion CSS is the optimal solution for truncating text in most cases due to its simplicity, efficiency, and responsiveness. It leverages the power of the browser’s rendering engine, avoids the overhead of JavaScript, and keeps your code clean and maintainable. While JavaScript truncation has its use cases, CSS should always be your go-to solution for truncating strings, especially in static or predictable layouts. If you like this post, check out the other CSS posts on our blog!...

Linting, Formatting, and Type Checking Commits in an Nx Monorepo with Husky and lint-staged cover image

Linting, Formatting, and Type Checking Commits in an Nx Monorepo with Husky and lint-staged

One way to keep your codebase clean is to enforce linting, formatting, and type checking on every commit. This is made very easy with pre-commit hooks. Using Husky, you can run arbitrary commands before a commit is made. This can be combined with lint-staged, which allows you to run commands on only the files that have been staged for commit. This is useful because you don't want to run linting, formatting, and type checking on every file in your project, but only on the ones that have been changed. But if you're using an Nx monorepo for your project, things can get a little more complicated. Rather than have you use eslint or prettier directly, Nx has its own scripts for linting and formatting. And type checking is complicated by the use of specific tsconfig.json files for each app or library. Setting up pre-commit hooks with Nx isn't as straightforward as in a simpler repository. This guide will show you how to set up pre-commit hooks to run linting, formatting, and type checking in an Nx monorepo. Configure Formatting Nx comes with a command, nx format:write for applying formatting to affected files which we can give directly to lint-staged. This command uses Prettier under the hood, so it will abide by whatever rules you have in your root-level .prettierrc file. Just install Prettier, and add your preferred configuration. ` Then add a .prettierrc file to the root of your project with your preferred configuration. For example, if you want to use single quotes and trailing commas, you can add the following: ` Configure Linting Nx has its own plugin that uses ESLint to lint projects in your monorepo. It also has a plugin with sensible ESLint defaults for your linter commands to use, including ones specific to Nx. To install them, run the following command: ` Then, we can create a default .eslintrc.json file in the root of our project: ` The above ESLint configuration will, by default, apply Nx's module boundary rules to any TypeScript or JavaScript files in your project. It also applies its recommended rules for JavaScript and TypeScript respectively, and gives you room to add your own. You can also have ESLint configurations specific to your apps and libraries. For example, if you have a React app, you can add a .eslintrc.json file to the root of your app directory with the following contents: ` Set Up Type Checking Type checking with tsc is normally a very straightforward process. You can just run tsc --noEmit to check your code for type errors. But things are more complicated in Nx with lint-staged. There are a two tricky things about type checking with lint-staged in an Nx monorepo. First, different apps and libraries can have their own tsconfig.json files. When type checking each app or library, we need to make sure we're using that specific configuration. The second wrinkle comes from the fact that lint-staged passes a list of staged files to commands it runs by default. And tsc will only accept either a specific tsconfig file, or a list of files to check. We do want to use the specific tsconfig.json files, and we also only want to run type checking against apps and libraries with changes. To do this, we're going to create some Nx run commands within our apps and libraries and run those instead of calling tsc directly. Within each app or library you want type checked, open the project.json file, and add a new run command like this one: ` Inside commands is our type-checking command, using the local tsconfig.json file for that specific Nx app. The cwd option tells Nx where to run the command from. The forwardAllArgs option tells Nx to ignore any arguments passed to the command. This is important because tsc will fail if you pass both a tsconfig.json and a list of files from lint-staged. Now if we ran nx affected --target=typecheck from the command line, we would be able to type check all affected apps and libraries that have a typecheck target in their project.json. Next we'll have lint-staged handle this for us. Installing Husky and lint-staged Finally, we'll install and configure Husky and lint-staged. These are the two packages that will allow us to run commands on staged files before a commit is made. ` In your package.json file, add the prepare script to run Husky's install command: ` Then, run your prepare script to set up git hooks in your repository. This will create a .husky directory in your project root with the necessary file system permissions. ` The next step is to create our pre-commit hook. We can do this from the command line: ` It's important to use Husky's CLI to create our hooks, because it handles file system permissions for us. Creating files manually could cause problems when we actually want to use the git hooks. After running the command, we will now have a file at .husky/pre-commit that looks like this: ` Now whenever we try to commit, Husky will run the lint-staged command. We've given it some extra options. First, --concurrent false to make sure attempts to write fixes with formatting and linting don't conflict with simultaneous attempts at type checking. Second is --relative, because our Nx commands for formatting and linting expect a list of file paths relative to the repo root, but lint-staged would otherwise pass the full path by default. We've got our pre-commit command ready, but we haven't actually configured lint-staged yet. Let's do that next. Configuring lint-staged In a simpler repository, it would be easy to add some lint-staged configuration to our package.json file. But because we're trying to check a complex monorepo in Nx, we need to add a separate configuration file. We'll call it lint-staged.config.js and put it in the root of our project. Here is what our configuration file will look like: ` Within our module.exports object, we've defined two globs: one that will match any TypeScript files in our apps, libraries, and tools directories, and another that also matches JavaScript and JSON files in those directories. We only need to run type checking for the TypeScript files, which is why that one is broken out and narrowed down to only those files. These globs defining our directories can be passed a single command, or an array of commands. It's common with lint-staged to just pass a string like tsc --noEmit or eslint --fix. But we're going to pass a function instead to combine the list of files provided by lint-staged with the desired Nx commands. The nx affected and nx format:write commands both accept a --files option. And remember that lint-staged always passes in a list of staged files. That array of file paths becomes the argument to our functions, and we concatenate our list of files from lint-staged into a comma-delimitted string and interpolate that into the desired Nx command's --files option. This will override Nx's normal behavior to explicitly tell it to only run the commands on the files that have changed and any other files affected by those changes. Testing It Out Now that we've got everything set up, let's try it out. Make a change to a TypeScript file in one of your apps or libraries. Then try to commit that change. You should see the following in your terminal as lint-staged runs: ` Now, whenever you try to commit changes to files that match the globs defined in lint-staged.config.js, the defined commands will run first, and verify that the files contain no type errors, linting errors, or formatting errors. If any of those commands fail, the commit will be aborted, and you'll have to fix the errors before you can commit. Conclusion We've now set up a monorepo with Nx and configured it to run type checking, linting, and formatting on staged files before a commit is made. This will help us catch errors before they make it into our codebase, and it will also help us keep our codebase consistent and readable. To see an example Nx monorepo with these configurations, check out this repo....

“Music and code have a lot in common,” freeCodeCamp’s Jessica Wilkins on what the tech community is doing right to onboard new software engineers cover image

“Music and code have a lot in common,” freeCodeCamp’s Jessica Wilkins on what the tech community is doing right to onboard new software engineers

Before she was a software developer at freeCodeCamp, Jessica Wilkins was a classically trained clarinetist performing across the country. Her days were filled with rehearsals, concerts, and teaching, and she hadn’t considered a tech career until the world changed in 2020. > “When the pandemic hit, most of my gigs were canceled,” she says. “I suddenly had time on my hands and an idea for a site I wanted to build.” That site, a tribute to Black musicians in classical and jazz music, turned into much more than a personal project. It opened the door to a whole new career where her creative instincts and curiosity could thrive just as much as they had in music. Now at freeCodeCamp, Jessica maintains and develops the very JavaScript curriculum that has helped her and millions of developers around the world. We spoke with Jessica about her advice for JavaScript learners, why musicians make great developers, and how inclusive communities are helping more women thrive in tech. Jessica’s Top 3 JavaScript Skill Picks for 2025 If you ask Jessica what it takes to succeed as a JavaScript developer in 2025, she won’t point you straight to the newest library or trend. Instead, she lists three skills that sound simple, but take real time to build: > “Learning how to ask questions and research when you get stuck. Learning how to read error messages. And having a strong foundation in the fundamentals” She says those skills don’t come from shortcuts or shiny tools. They come from building. > “Start with small projects and keep building,” she says. “Books like You Don’t Know JS help you understand the theory, but experience comes from writing and shipping code. You learn a lot by doing.” And don’t forget the people around you. > “Meetups and conferences are amazing,” she adds. “You’ll pick up things faster, get feedback, and make friends who are learning alongside you.” Why So Many Musicians End Up in Tech A musical past like Jessica’s isn’t unheard of in the JavaScript industry. In fact, she’s noticed a surprising number of musicians making the leap into software. > “I think it’s because music and code have a lot in common,” she says. “They both require creativity, pattern recognition, problem-solving… and you can really get into flow when you’re deep in either one.” That crossover between artistry and logic feels like home to people who’ve lived in both worlds. What the Tech Community Is Getting Right Jessica has seen both the challenges and the wins when it comes to supporting women in tech. > “There’s still a lot of toxicity in some corners,” she says. “But the communities that are doing it right—like Women Who Code, Women in Tech, and Virtual Coffee—create safe, supportive spaces to grow and share experiences.” She believes those spaces aren’t just helpful, but they’re essential. > “Having a network makes a huge difference, especially early in your career.” What’s Next for Jessica Wilkins? With a catalog of published articles, open-source projects under her belt, and a growing audience of devs following her journey, Jessica is just getting started. She’s still writing. Still mentoring. Still building. And still proving that creativity doesn’t stop at the orchestra pit—it just finds a new stage. Follow Jessica Wilkins on X and Linkedin to keep up with her work in tech, her musical roots, and whatever she’s building next. Sticker illustration by Jacob Ashley....

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