PatternFly 4 - CSS Customization

Hi Fliers! The PatternFly team would like to ask your opinion on a couple of foundational approaches to the way we author CSS and allow for customization of PatternFly components in your application. We’re proposing one of the two solutions below, weighing the pros and cons of each approach. There are impacts on the overall amount of code used, component CSS file size, ease of making customizations, consistency of custom property names, and likelihood of introducing breaking changes from upstream changes.

In an effort to be transparent and provide the greatest value to PatternFly consumers, we’re curious which of these methods you would prefer as a developer implementing and maintaining the PatternFly design system in your application.

Option 1. Consolidating shared values in a component to a single variable that is used for multiple values in the component.

.pf-c-button {
  --pf-c-button--PaddingY: var(--pf-global-spacer--sm);
  --pf-c-button--PaddingX: var(--pf-global-spacer--lg);
  --pf-c-button--BorderWidth: var(--pf-global-borderWidth--sm);
  --pf-c-button--BorderColor: var(--pf-global-borderColor--primary);

  padding: var(--pf-c-button--PaddingY) var(--pf-c-button--PaddingX);

  &:hover,
  &:focus,
  &:active {
    border: var(--pf-c-button--BorderWidth) solid var(--pf-c-button--BorderColor);
  }

Pros

  • Less code.
  • Easier to make changes to multiple properties that share a common value.
  • Lower file size**.
  • Encourages more design consistency.

Cons

  • Less consistent variable names.
  • Greater potential for introducing breaking changes if, for example, PatternFly chooses to update the vertical spacing and change --pf-c-button--PaddingY to --pf-c-button--PaddingTop and --pf-c-button--PaddingBottom to support separate values for the top and bottom padding.
  • Less customizable, assuming you want to customize something that isn’t represented as a variable.

Option 2. Create a unique variable to use as the value for each property.

.pf-c-button {
  --pf-c-button--PaddingTop: var(--pf-global-spacer--sm);
  --pf-c-button--PaddingBottom: var(--pf-global-spacer--sm);
  --pf-c-button--PaddingLeft: var(--pf-global-spacer--lg);
  --pf-c-button--PaddingRight: var(--pf-global-spacer--lg);
  --pf-c-button--hover--BorderWidth: var(--pf-global-borderWidth--sm);
  --pf-c-button--hover--BorderColor: var(--pf-global-borderColor--primary);
  --pf-c-button--focus--BorderWidth: var(--pf-global-borderWidth--sm);
  --pf-c-button--focus--BorderColor: var(--pf-global-borderColor--primary);
  --pf-c-button--active--BorderWidth: var(--pf-global-borderWidth--sm);
  --pf-c-button--active--BorderColor: var(--pf-global-borderColor--primary);

  padding: var(--pf-c-button--PaddingTop) var(--pf-c-button--PaddingRight) var(--pf-c-button--PaddingBottom) var(--pf-c-button--PaddingLeft);

  &:hover {
    border: var(--pf-c-button--hover--BorderWidth) solid var(--pf-c-button--hover--BorderColor);
  }

  &:focus {
    border: var(--pf-c-button--focus--BorderWidth) solid var(--pf-c-button--focus--BorderColor);
  }

  &:active {
    border: var(--pf-c-button--active--BorderWidth) solid var(--pf-c-button--active--BorderColor);
  }

Pros

  • More customizable.
  • Less likelihood of introducing breaking changes if PatternFly changes a variable’s value.
  • More consistent variable names.

Cons

  • More code.

  • Larger file size**.

  • More difficult to maintain, assuming PatternFly or a consumer wants to update, for example, :hover , :focus , and :active states at once to have a different (but the same between the states) border-width and/or border-color .

  • Allows for more/easier deviation from PatternFly’s design.

  • File size improvements vary per component, but we expect on average around a 20% improvement per component.

Here are examples of some of the more noteworthy file size improvements. (Note: this is the size of non-minified CSS):

  • Card - from 2.1k to 1.4k (savings of 0.7k, or 67%)
  • Nav - from 21k to 11k (savings of 10k, or 54%)
  • Button - from 16k to 8k (savings of 8k, or 50%)
  • Content - from 12k to 8k (savings of 4k, or 67%)

For context, the package size is currently about 347k, or 291k when minimized. We plan to make the package available without the utility classes, which brings the size to 240k, or 202k when minimized.

2 Likes

Thanks Dana. I would like to propose “Option 2. Create a unique variable to use as the value for each property” as the suggested solution going forward, and prioritize maintainability and backwards compatibility by minimizing the risk of introducing breaking changes with subsequent PatternFly releases. We are also working on other ways to reduce the overall CSS size, and will continue to do so to deliver a performant design system without compromising developer experience.

Option 2 sounds like the more robust option. If we are, as @mcoker says, working on further ways to decrease the size then option 2 would be my choice. The risks of introducing breaking changes more often and being less customizable would be of larger concern to me.

I’m torn on the options - I really like the pros of Option 1 (Less Code, Lower File Size, Design Consistency), but the greater potential for breaking changes push me towards Option 2. Customization is nice, but not at the sacrifice of design consistency which, from PatternFly 3, is often the hardest thing to keep across screens.

The Con of ‘More Code’ for Option 2 (IMO) could possibly be resolved with an expanded breakdown of what needs to be imported - helpers, variables, etc. In the current Alpha builds, only having to pull in what you need is incredibly useful and keeps the build size down - especially when you don’t need every component.

Would there be any way to keep the design consistency aspect of Option 1, while keeping customization from Option 2 (either through class requirements checks or whatnot)? Maintenance is the biggest thing that I’ve taken out of this so, in reality, whatever leads to easier maintenance and implementation is what I would vote for.

Thanks for posting this on the forum!

My two cents: the #1 goal of PatternFly is not simply to roll up code bundles for ease of development, though that is a major perk. :slight_smile: It’s primarily a design system first and foremost, meaning the reason it exists is to provide visual consistency across sites & apps using it. If PF provides a way to override nearly everything (such as top and bottom padding on buttons independently) then it makes it easier to not only to go off-brand, but also to have imbalanced design.

And honestly it puts more work on the consuming developer to override 2 values instead of just 1, should they need taller buttons.

My pick would be Option 1, but to go beyond that and further slim down to just 1 variable. After all, CSS offers shorthand, so really you can pass in 4 values instead of one if you had to override them all independently.

Beyond that, I think it makes sense to have components lean on only 1 or 2 global spacers, and use math to scale up or down. That way you can adjust one spacer unit, and everything on the site becomes more or less airy. As a consuming developer, how can I know what components will change if I change the value of only the global-spacer-sm? Really if I’m going to change one, I will probably need to change them all. Why not put all that work on the shoulders of math?

// global var:
--pf-global-spacer: 8px;

// component:
  --pf-c-button--Padding:  calc(var(--pf-global-spacer) * 0.5) var(--pf-global-spacer);

.pf-c-button {
   padding: var(--pf-c-button--Padding);
}

Because of CSS shorthand, as a consuming developer, I can override button spacing lots of ways:

  --pf-c-button--Padding: 11px;
  --pf-c-button--Padding: 4px 12px;
  --pf-c-button--Padding: 11px 12px 13px 14px;

Sidenote on this:

  • Greater potential for introducing breaking changes if, for example, PatternFly chooses to update the vertical spacing and change --pf-c-button--PaddingY to --pf-c-button--PaddingTop and --pf-c-button--PaddingBottom to support separate values for the top and bottom padding.

If you did stick with option 1 as you outlined it and you decide to change to more specific variables later, it wouldn’t have to be a breaking change. You could just use the old variables as the values of the new variables. Still more code, but it wouldn’t break anything.

--pf-c-button--PaddingTop: var(--pf-c-button--PaddingY);
--pf-c-button--PaddingBottom: var(--pf-c-button--PaddingY);

Thanks!

1 Like

@mindreeper2420 thanks for the feedback!

Would there be any way to keep the design consistency aspect of Option 1, while keeping customization from Option 2 (either through class requirements checks or whatnot)? Maintenance is the biggest thing that I’ve taken out of this so, in reality, whatever leads to easier maintenance and implementation is what I would vote for.

Option 1 was seen as a compromise, in relation to @kendalltotten’s idea of using a single variable to provide consistency. Writing CSS as shortly as possible (using a single variable, for example), at the cost of lack of customization (and benefit of design consistency), was too restrictive, so we opted for grouping some values into a single variable, but not necessarily all, as a compromise. This was also a compromise regarding introducing breaking changes - option 1 would introduce less breaking changes than using a single variable, but more than using separate vars for each value. I think we’ll always work on finding ways to encourage consistency with regard to the implementation of PatternFly, while still providing flexibility for consumers to customize their design when necessary.

@kendalltotten thanks for the reply! These are definitely good points, and conversations we’ve had as a team numerous times. One of the primary goals of PatternFly includes the flexibility of allowing for customization of the design system when necessary.

it puts more work on the consuming developer to override 2 values instead of just 1, should they need taller buttons.

This is a good point, however the extra work is considered a compromise for better maintainability down the road with regard to the introduction of breaking changes with subsequent PatternFly releases.

My pick would be Option 1, but to go beyond that and further slim down to just 1 variable. After all, CSS offers shorthand, so really you can pass in 4 values instead of one if you had to override them all independently.

Initially, I was in this boat, too! I assume if we’re going to be restrictive, why not go all the way and write CSS as lean as possible to save space, make it as easy as possible to update multiple values at once, and enforce the most design consistency with regard to the ease of customization with the PatternFly variable system. However, as I replied to Adam above, a goal of PatternFly is to support customization as well, and using, for example, a single variable to represent 4 equal spacing values in a padding or margin property was considered too restrictive after conversations with the PatternFly developers and designers. We wanted to allow for more customization than that, so we opted for the combination pattern illustrated in option 1 of the OP.

However, after considering this in a broader sense, I think the combination of variables is going to impact developer experience and maintainability in a detrimental way as the system evolves and we end up changing variable names that folks were relying upon to customize PatternFly in their application. Maintenance and ease of updating to new PatternFly releases is one of our main concerns, as well.

If you did stick with option 1 as you outlined it and you decide to change to more specific variables later, it wouldn’t have to be a breaking change. You could just use the old variables as the values of the new variables.

That’s true to some extent, but let me illustrate with an example. Say we started with

.pf-c-button {
  --pf-c-button--PaddingY: var(--pf-global-spacer--sm);
  --pf-c-button--PaddingX: var(--pf-global-spacer--lg);
  
  padding: var(--pf-c-button--PaddingY) var(--pf-c-button--PaddingX);
}

And a PF consumer has overwritten --pf-c-button--PaddingY with their own value to represent padding-top and padding-bottom. And later down the road, the designs call for separating --pf-c-button--PaddingY into a padding-top of --pf-global-spacer--md and padding-bottom of --pf-global-spacer--xl, you wouldn’t still be able to use --pf-c-button--PaddingY for both values of padding-top and padding-bottom. The best you could do is reuse one of those values as --pf-c-button--PaddingY, but since the top and bottom values are different, you’re going to have to use a new variable that is responsible for padding-top or padding-bottom, introducing a breaking change. For example:

.pf-c-button {
  --pf-c-button--PaddingY: var(--pf-global-spacer--md); // padding-top
  --pf-c-button--PaddingBottom: var(--pf-global-spacer--xl); // padding-bottom
  --pf-c-button--PaddingX: var(--pf-global-spacer--lg);
  
  padding: var(--pf-c-button--PaddingY) var(--pf-c-button--PaddingX) var(--pf-c-button--PaddingBottom);
}

Introducing --pf-c-button--PaddingBottom, when the user was relying on --pf-c-button--PaddingY to define a custom padding-top and padding-bottom would be a breaking change. And ideally, if we had a unique padding-top and padding-bottom, we wouldn’t use --pf-c-button--PaddingY as one of the values, as that variable name implies that it controls the y-axis spacers.

One thing re: the backwards compatibility, assuming this was the original code:

.pf-c-button {
  --pf-c-button--PaddingY: var(--pf-global-spacer--md); 
  --pf-c-button--PaddingX: var(--pf-global-spacer--lg);
  
  padding: var(--pf-c-button--PaddingY) var(--pf-c-button--PaddingX);
}

If later PF decides to switch to more specific vars in order to have less padding on the top of buttons, the code could be updated like this:

.pf-c-button {

  --pf-c-button--PaddingTop:    var(--pf-global-spacer--md);   // new variables with desired values
  --pf-c-button--PaddingBottom: var(--pf-global-spacer--lg);

  --pf-c-button--PaddingY: --pf-c-button--PaddingTop --pf-c-button--PaddingBottom;  // old var will still respect overrides on existing sites, but new sites will use the new top & bottom vars

}
1 Like

That’s a great point. I think the main downside is that we’re then introducing additional code just for legacy support. That would also introduce the inconsistency and complexity of X/Y variables being utilized in only some parts of the system depending on whether we needed backwards compatibility for particular variables or not.

I would agree with Kendall’s approach because a design system should by its nature be opinionated. Customizations are completely possible in the approach she laid out but at that point you’ve walked away from the design system and I don’t think it’s worth prioritizing the possible overrides over the elegance and robustness of the system itself.

The value in having a global spacing unit too I think should not be overlooked. Take the example of marketing content versus documentation content. The former should be open and airy and bold and the latter should be more compact and straight forward in its nature. By adjusting a spacing variable, you can accomplish both designs with the same components on your page. #winwin

:smiley:

Thanks @castastrophe! We will definitely continue to work towards encouraging design consistency, while also being able to provide flexibility and customization to the applications PatternFly supports.

Thanks again to everyone for your feedback and thoughtful consideration. After weighing the options, and moving toward our beta release targeted at the end of January 2019, we’re going to move forward with option 2. Striking a balance between design consistency and flexibility for customization within the design system for different applications is always a challenge, and we’ll continue to try and deliver the best system we can.