CSS

Philosophy #philosophy

At 10up, we value content and the experience users will have reading it. We write CSS with this in mind and don’t sacrifice our clients’ most important assets over the latest, shiniest, half-supported CSS features just for the sake of using them. CSS should help enhance content, not bury it under “cool” distractions.

Our websites are built mobile first, using performant CSS. Well-structured CSS yields maintainability and better collaboration which ultimately yields better client experiences.

Below are our suggested best practices for writing CSS at 10up.

Follow an Established System First #established-system

There are countless ways to organize your CSS styles. From naming conventions to structuring files, there’s no single “right” way to do it. What’s important is understanding and respecting the system that’s already in place.

Only propose changes if the current convention is genuinely problematic, e.g., if it’s inconsistent, hard to scale, or causes errors. When you suggest a change, be ready to explain why the new approach is better and how it solves a specific problem.

This approach helps avoid duplication—why have two things that do the same job? It’s inefficient and creates unnecessary confusion. Beyond that, following established patterns shows respect for your team’s work and fosters a healthy, collaborative relationship. Consistency in the codebase reflects a consistent and unified team.

Establish and Document the System #document-system

When you start a new codebase, pause before writing components and choose a CSS methodology—such as ITCSS, BEM, or a utility-first approach—that fits the project’s size, maintenance horizon, and team expertise. Committing to a structured methodology early establishes clear naming rules, predictable specificity, and sensible file organization. These guardrails ease onboarding, minimize refactors, and save both time and budget over the life of the project.

If you opt to design a custom architecture, capture every key choice: folder layout, naming conventions, tooling, and any deviations from common patterns. A short explanation of why each decision was made helps future contributors understand the system quickly and avoids guesswork.

Note: Keep this record where engineers will look first—typically in a /decisions/ directory inside the repository’s setup or onboarding documentation. Follow your project’s standard documentation format so the notes remain consistent, searchable, and easy to maintain.

Assume You Know Nothing About the Content #content-agnostic

When building components, always start with the mindset that you have no control over the content they’ll handle. Even if the client or designer specifies certain limits, assume those constraints could change—or be ignored entirely.

Content is unpredictable. You might not know how long a title will be, how many items a navigation menu will have, or the length of an excerpt in a card. Design decisions made today can quickly be overturned tomorrow when new requirements come in.

Prepare for edge cases. Your components must be resilient and robust enough to handle all reasonable variations, including unexpected ones. If you don’t account for these possibilities, it could lead to broken layouts, unreadable text, or poor user experiences.

Useful resources:

Stress Test Your Components

When building a component, test it against extreme content scenarios. For example:

The goal is to make these situations work. They may not look ideal, but they should remain usable and accessible.

Designing flexible, content-agnostic components makes your work more resilient over time. It reduces the need for constant adjustments and helps protect layouts from breaking when content does not match the original assumptions. In practice, requirements tend to change as a project moves forward.

Write Only the Styles You Need, Don't Do Too Much #minimal-styles

When writing CSS, stick to the styles you need and avoid overcomplicating. For example, centering a container horizontally often involves a common snippet:

.container {
  max-width: 48rem;
  margin: 0 auto; /* ⚠️ Avoid this */
}

While this centering approach achieves the goal, it also zeroes out the top and bottom margins, which may not be intentional and will have unwanted impact in the cascade. Instead, only set the margin you need:

.container {
  max-width: 48rem;
  margin-inline: auto; /* ✅ Do this instead */
}

This approach, using logical properties like margin-inline, maintains the desired horizontal centering without unnecessary side effects. Avoid shorthands that set multiple properties when only one is needed.

Another example is background styling:

.button--secondary {
  background: var(--color--dark-gray);
}

Using background shorthand like the above implicitly sets many background properties, which can unintentionally override defaults. Instead, specify only the needed properties:

.button--secondary {
  background-color: var(--color--dark-gray);
}

The same principle applies to other shorthand properties, such as border, transform, transition, font, and others.

Finally, be cautious with broad declarations that affect more elements than intended. For instance, adding an underline animation to links:

a {
  text-decoration: none;
  background-image: linear-gradient(currentColor, currentColor);
  background-position: 0% 100%;
  background-repeat: no-repeat;
  background-size: 100% 2px;
  transition: background-size 0.3s;
}

Adding a global style like this can unintentionally affect other links that don’t require this underline treatment (image links, site logo, etc.). Instead, there are two recommended methods to handle similar styles. First, you can target elements (i.e. links) like this more precisely:

.entry-content :is(h1, h2, h3, h4, h5, h6, p, li) > a {
  /* styles here */
}

This selector precisely targets only links that are direct children of text content elements within .entry-content, avoiding the side effects of the overly broad a selector. The :is() pseudo-class keeps this selector concise by matching any of the listed elements without needing to repeat the entire selector chain for each element type.

Second, you can use a utility class.

.has-animated-underline {
  /* styles here */
}

These solutions ensure only the intended elements are styled and avoid unintended side effects. Keep your CSS clean and purposeful by applying styles selectively and avoiding overly broad or shorthand declarations.

Mobile-First by Default #mobile-first

Designing with a mobile-first approach is essential because most users access websites from their mobile devices. Starting with the mobile layout ensures the core content and functionality are optimized for smaller screens, providing a better user experience. Once the mobile layout is solid, it’s easier to scale up for larger screens, ensuring a consistent and responsive design across all devices, rather than doing it the other way around.

This doesn’t mean we must design for mobile devices first chronologically (although it is a viable approach), but rather that mobile devices get enough attention and are not an afterthought. This approach also helps prioritize essential features and content, making the site faster and more efficient.

What Does It Mean in Practice?

As a front-end engineer, it’s crucial to be present at every design planning meeting. Your job is not just to determine whether the designs can be done in CSS but whether they should be done. Your input ensures that the designs are not only visually appealing but also can be implemented for mobile devices in an efficient way.

Always advocate for creating mobile designs if they are not already included and for simplicity in implementation.

Keep an Eye Out for Common Pitfalls

When reviewing designs, look for common pitfalls like the ones below:

If you see a way to simplify the implementation, always voice it. Suggest adjustments that will make the system leaner and more maintainable.

Style the Project as If Desktop Doesn’t Exist First

Style the project as if Desktop doesn’t exist first and then expand or override rules for the desktop.

With the capabilities of modern CSS it is usually best to reach for media queries last and use features like auto-rows in CSS Grid or functions like clamp() to create layouts that are mobile and desktop-friendly.

Allowing for this flexibility helps handle the “in-between-breakpoint” layouts nicely.

Scrolling Is Okay

Just like books have pages and readers know they need to flip them to move forward, users know they need to scroll to get further on a webpage. Keep this in mind when you see designs like:

There are cases where such layout patterns are necessary. Not all select dropdowns or carousels are bad, but in most cases, this is unnecessary complication. Simple scrolling usually works just fine and is more intuitive than more complicated UI patterns.

Needless to say, implementing such changes between viewports takes more time and can pose accessibility implications.

Useful resources:

Set Solid Defaults #solid-defaults

When building a robust and scalable front-end architecture, setting good default styles is an essential first step. Establishing a CSS foundation ensures consistency across browsers and provides a solid base for further styling. This foundation can be achieved using one of three primary approaches:

  1. CSS Reset: A reset removes virtually all default styles applied by browsers, leveling the playing field by eliminating discrepancies. While this approach offers the cleanest slate, it also removes potentially useful styles, requiring more effort to rebuild a cohesive design system.
  2. CSS Normalize: A normalize CSS file (i.e. normalize.css) focuses on addressing inconsistencies between browsers while retaining most of their default styles. This approach minimizes the need for excessive rework while still providing a reliable base.
  3. Hybrid (or Custom) Approach: Many projects adopt a blend of resets and normalization, coupled with project-specific global styles, to balance control and practicality. For example, resetting some elements like margins and paddings but setting global defaults for others, (following ITCSS architecture) such as lists and forms.

The Importance of Global Rules

Global rules play a critical role in setting consistent behavior across your entire application. These rules can prevent costly refactoring and unexpected bugs as your project evolves. One key example is setting the box-sizing property globally:

*,
*::before,
*::after {
  box-sizing: border-box;
}

:where(body) {
  margin: 0;
}

By applying box-sizing: border-box, you ensure that padding and border widths are included in the element’s total width and height calculations. This eliminates common layout issues and simplifies sizing calculations, reducing developer frustration and the likelihood of design inconsistencies.

It is very easy to set up at the beginning of a project and very hard and expensive to fix when the project is live and has hundreds (or in some cases thousands) of pages and many components.

Important: Make this decision at the start of your project—don’t proceed without addressing it. Either set box-sizing: border-box manually as shown above, or verify that your chosen CSS reset or normalize library already includes it. This small upfront decision prevents significant technical debt later.

Foundational Styles for HTML Elements

A well-crafted CSS foundation includes thoughtful defaults for HTML elements, such as headings, paragraphs, and lists. Avoid setting specific styles, like text-align: center, on elements that are likely to be used in various contexts. For example:

Headings: Define consistent font sizes, weights, and spacing for <h1> through <h6> elements to create a harmonious typography hierarchy that cascades effectively throughout your project.

:where(h1) {
  /* styles here */
}

:where(h2) {
  /* styles here */
}

/* And so on for other heading levels */

Lists: Ensure that unordered and ordered lists have adequate styling by default.

:where(ul, ol, dl) {
  /* styles here */
}

These are just a few base examples. Foundational styles should be applied to all elements used in the project. Typically, this is the “elements” level in the ITCSS methodology.

Avoiding Overly Opinionated Defaults

While foundational styles are crucial, avoid overly opinionated rules that assume specific use cases for elements. For example, setting text-align: center or fixed widths on common elements like <div> or <p> can lead to unnecessary overrides and limited reusability. Foundational styles should support flexibility and adaptability, empowering developers to craft unique designs without battling restrictive defaults.

By thoughtfully implementing and balancing resets, normalization, and custom global styles, you can establish a CSS foundation that fosters maintainability, consistency, and scalability for your project.

Follow Inverted Triangle Architecture #inverted-triangle

Inverted Triangle is a methodology for organizing CSS, starting from broad, global styles down to more specific, local rules.

Begin with base styles that affect the entire application:

Global

Elements

This layer targets raw HTML elements (tags), without using classes, IDs, or other specific selectors. The purpose of the Elements level is to establish foundational styles for elements to be used within components later on.

Below are some examples of styles at this level:

:where(h1) {
  color: var(--color--heading);
  font-family: var(--font--family--heading);
  font-size: var(--font--size--heading-1);
  font-weight: var(--font--weight--heading);
  line-height: var(--font--line-height--heading-1);
  margin-block: 0;
}

Key details to note:

Avoid adding styles that are too opinionated at the base level. For example, setting text-align: center on the heading in the previous example can cause issues. Even if most headings are center-aligned, there will likely be cases where you need a left- or right-aligned heading. This forces you to override the style in all components where center alignment isn’t needed, bloating the CSS and adding mental overhead.

Be cautious about which styles you place at this level. Only include styles that should apply to all elements in 99.9% of cases. Removing base-level styles once a project is live—especially if thousands of pages depend on them—can introduce regressions.

Components

The Component layer is where styles are applied to reusable UI elements. These are more complex than base-level styles and use classes to ensure component-level specificity.

Key Guidelines

Utilities

Utility classes provide quick, reusable styling solutions for common, single-purpose tasks. They allow you to apply styles without the need for creating new components.

Key Guidelines

Useful resources:

Always Keep Accessibility in Mind #accessibility

Keyboard Navigation

Make sure all interactive elements are accessible via the keyboard. Test that users can navigate through the interface using the Tab key and activate actions with Enter or Space. Avoid creating elements that require a mouse or touch to work.

Color Contrast

Ensure there’s enough contrast between text and its background. Use tools to check for WCAG compliance. Aim for a contrast ratio of at least 4.5:1 for normal text and 3:1 for larger text. Avoid relying on color alone to convey information.

Focus

Properly manage focus states. Use :focus-visible styles to make focused elements visible and distinct. Don’t trap focus in modal dialogs or dropdowns without providing a clear way to exit. Reset focus logically when users interact with dynamic content, such as moving to a new page.

Zoom and Reflow

Your site must support 400% zoom without loss of content or functionality. When a user views your site on a 1280px wide viewport and zooms to 400%, the page should reflow to behave like a 320px viewport. This ensures users who need magnification can still access all features and content.

Key requirements:

Use responsive design techniques with relative units (rem, em, %) rather than fixed pixel values, and test your components at various zoom levels during development. For detailed guidance, see WCAG 2.2 Understanding Reflow.

RTL and Logical Properties

Support right-to-left (RTL) languages by using CSS logical properties instead of physical ones where it makes sense. Logical properties use direction-agnostic terms (inline and block) that automatically adapt to the text direction, making RTL support automatic.

Instead of physical properties like margin-left, padding-right, or text-align: left, use logical equivalents:

.element {
  margin-inline-start: 1rem;  /* instead of margin-left */
  padding-inline: 1rem;       /* instead of padding-left/right */
  text-align: start;           /* instead of text-align: left */
}

Set the dir attribute on the <html> element or specific containers to enable RTL rendering. Test your layouts by toggling dir="rtl" to ensure they work correctly in both directions.

Useful resources:

Advocate for Accessible Design Patterns

Promote the adoption of design patterns that inherently prevent accessibility issues. Examples include:

Keep Specificity Extremely Low #low-specificity

Maintaining low specificity in your CSS is essential for creating scalable, maintainable, and predictable stylesheets. High specificity can make it difficult to override styles, leading to a cascade that is hard to manage and debug. By keeping specificity low, you ensure that your styles remain flexible and easy to adapt as your project evolves.

Aim for a maximum specificity of 0,1,0 to 0,2,1. Avoid combining tag selectors with classes unless necessary—use .button instead of a.button, and .nav-item instead of ul.nav li.nav-item. In limited cases, selectors like .component .utility-class a may reach 0,2,1, but exceeding this threshold requires serious justification.

Tools for checking specificity:

Use :where() for Bare Elements

Utilize the :where() pseudo-class when styling bare elements to avoid increasing specificity. Unlike other selectors, :where() doesn’t contribute to specificity, making it easier to override styles when needed and ensuring a cleaner cascade hierarchy. This allows you to apply styles without affecting how CSS rules are prioritized and inherited, keeping the order of styles predictable and easier to manage. For example:

:where(h1, h2, h3, h4, h5, h6) {
  margin-block: 0;
}

Useful resources:

Avoid Excessive Nesting

Limit the depth of nesting in your CSS selectors. Two levels of nesting are acceptable, and three should only be used when absolutely necessary. Excessive nesting can make CSS harder to read and maintain. It also increases specificity unnecessarily, which complicates overriding styles and debugging. Over-nesting complicates maintenance and increases specificity unnecessarily.

It’s okay to nest pseudo classes that represent state, for example:

/* block */
.button {

  /* ✅ state inside the block is fine */
  &:hover {
    /* styles here */
  }
}

/* ✅ element and modifier stay flat */
.button__icon {
  /* styles here */
}

.button--primary {
  /* styles here */
}

It’s not necessary to nest elements or modifiers:

/* block */
.button {

  /* state inside the block is fine */
  &:hover {
    /* styles here */
  }

  /* ❌ element must not be nested inside the block */
  & .button__icon {
    /* styles here */
  }

	/* ❌ modifier must not be nested inside the block */
  &.button--primary {
    /* styles here */
  }
}

Avoid IDs

Refrain from using IDs for styling purposes. IDs have high specificity, which makes them difficult to override and maintain. Instead, use classes for consistent and reusable styling. If you must target an element by its ID in CSS, consider using an attribute selector like [id="drawer-trigger"] instead of #drawer-trigger—this provides the same uniqueness without the specificity cost.

Avoid !important

Reserve !important for truly exceptional cases. Overusing !important disrupts the cascade and makes debugging more difficult. Focus on writing clean, organized selectors instead of relying on this property to enforce styles.

Do Not Use Margins on Reusable Components #no-margins-components

Margins might look like one of the easiest CSS properties to manage, but they’re also a common source of layout headaches if not handled carefully.

Example

Let’s say you have a Card component. Avoid doing this:

.card {
  margin-block: 1.5rem; /* ⚠️ Avoid this */
}

Instead, define the margin outside of the component:

/* Context-specific spacing */
.card-grid {
  display: grid;
  grid-template-columns: ...
  row-gap: 1.5rem; /* ✅ Do this instead */
}

This achieves the same result, but the Card can now be reused without disrupting layouts where the margin isn’t needed.

Note: It doesn’t have to be a grid. The key idea is to set margins at the “element” level, ensuring the reusable component—like card-grid in the example—controls only what happens inside it, not outside.

Apply Margins in One Direction #one-direction-margins

When applying margins, stick to using them in a single direction. This approach promotes consistency, simplifies debugging, and makes layouts easier to manage.

Responsiveness and Media Queries #responsiveness

Creating responsive designs is essential, but the approach you take can make a big difference in your code’s maintainability and flexibility. Media queries are a powerful tool, but they shouldn’t always be your first choice.

Naming Things #naming

Clear, consistent naming makes your CSS maintainable, readable, and scalable. Good naming conventions help developers quickly understand what each part of the code does, leading to fewer mistakes and easier updates. Here are practical recommendations for naming CSS elements:

Classes

IDs

Custom Properties (CSS Variables)

A common use case for CSS Custom Properties is design tokens. Naming tokens is crucial and delicate—well-named tokens ensure consistency, ease of understanding, and scalability.

Useful resources:

Animations (@keyframes)

Grid Template Areas and Lines

New CSS Features #new-features

Browsers keep evolving, and CSS is always introducing new features. But adopting them too soon can cause issues for older or niche browsers. Here’s how we evaluate and adopt modern CSS.

When introducing a new feature, think about the experience a user will get if the feature is not available. CSS features, when not supported, are ignored by browsers. Always think about what the effect is for an unsupported browser.

1. Check Browser Compatibility

In 2023, Google introduced Baseline to help developers determine whether certain features or APIs are safe to use in production. This tool aids in understanding the stability and support of web features across the most recent two versions of major browsers: Safari, Firefox, Chrome, and Edge.

2. Provide Fallbacks

Place a widely supported declaration first, then the modern one. Old browsers ignore unknown properties, so they naturally fall back.

.element {
  height: 100vh;    /* widely supported unit */
  height: 100dvh;   /* newer unit */
}

If the browser doesn’t know 100dvh, it sticks to 100vh. No breakage, just a simpler layout.

3. Use @supports

@supports is a native CSS feature-detection mechanism.

.site {
  min-height: 100vh;
}

@supports (min-height: 100dvh) {
  .site {
    min-height: 100dvh;
  }
}

Or reverse it with not:

.site {
  min-height: 100dvh;
}

/* remove when dvh is fully supported */
@supports not (min-height: 100dvh) {
  .site {
    min-height: 100vh;
  }
}

Add comments like /* remove when dvh is fully supported */ above fallback code. This makes it easy to search and remove outdated fallbacks when browser support improves.

4. Consider Polyfills

5. Use Automated Tooling

6. Decide How Soon to Adopt

In short, weigh your users’ browser usage and the criticality of the feature. Rely on Google Baseline, fallback strategies, and safe coding practices. Add polyfills or build tools only if the benefit outweighs the cost. That’s how you use modern CSS without leaving anyone behind.

Useful resources: