Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: deprecate CSS components in favour of 3rd party CSS pre-processors #740

Open
a-h opened this issue May 17, 2024 · 7 comments
Open
Labels
NeedsDecision Issue needs some more discussion so a decision can be made proposal

Comments

@a-h
Copy link
Owner

a-h commented May 17, 2024

templ css CSS components are very basic. They don't support a range of use cases #262 including media queries, and the interpolation features are minimal. I have built a PoC of interpolating CSS values within a standard <style> element at https://github.com/a-h/templ-css but before jumping to implementation, I took a step back.

People are already using pre-processors

Many users of templ are using Tailwind, or other CSS pre-processors already. They are essentially templating libraries for CSS, but can also do thing like include browser specific properties.

Pre-processors are popular and mature

The PostCSS NPM package gets around 69.3 million downloads per week, while SASS gets around 13.5 million per week, and tailwind gets 8M. They're well established projects, with large user bases.

templ css features are covered by existing pre-processors

templ css was designed so that it was possible to include classes for specific templ components, and only render the CSS if required. It dynamically generates class names to provide basic scoping. But... its design has severe limits.

Given the range of available pre-processors, their popularity, and the limited resources of the templ project, I think it might be better to spend time on improving the DX around using existing pre-processors in templ, rather than to attempt to create one, alongside everything else.

PostCSS has a plugin for scoping at https://github.com/madyankin/postcss-modules - which allows the scoping behaviour to be achieved easily, and there's a plugin for PostCSS which outputs a list of classes - https://www.npmjs.com/package/postcss-ts-classnames - in that particular case, in TypeScript format.

We can get compile-time errors about missing CSS classes

If, for example, we created a version of postcss-ts-classnames PostCSS plugin which created a Go package called class containing a load of constants, we'd then be able to import the package into our templates, and get a strongly defined list of classes. When you write <div class={ class. in templ, the Go LSP would provide a list of classes in the IDE autocomplete.

This would give us the benefits of a strongly typed list of classes, for minimal development effort.

This proposal fits well with #739

For JavaScript handling, I've proposed an alternative focus on to using JSON script elements, alongside esbuild to bundle up scripts. It solves the problems of transpiling TypeScript so you can use async/await, sorts out minification etc. you can see what that looks like at #739 - esbuild is written in Go, and is very fast.

templ.OncePerRequest

To ensure that links to stylesheets, or <style> content itself is only rendered once, a new templ.OncePerRequest component could ensure that children of the component are only rendered once per request, using the ctx parameter.

Build process

I think the way forward on CSS is similar - with the result that you run templ generate to get your HTML templates, esbuild to covert your modern JS/TS to browser JS, and postcss (or whatever CSS pre-processor you want to use) to get output CSS, and get the Go web server to host the outputs at dist/public.

Automatic (opt-in) migration for users of templ css and templ script components

There could be an automated migration path away from templ css and templ script. A templ migrate command could bring all the CSS together and output scss file(s) instead, while the scripts would be converted into JS functions in a *.js file.

Consideration of project setup

These tools require a more complex setup in that you have to have node installed, and will need an esbuild config mjs file, a package.json and a postcss config, but I think that can be solved by using a tool like cookiecutter (but not cookiecutter) to create some initial app templates, e.g. basic templ hello world, templ, HTMX with typescript and postcss, or templ, HTMX with JS, tailwind etc.

style attribute

Currently, the style attribute can't contain templ expressions. I think this could be relaxed, and any CSS within the style attribute could be pushed through a CSS sanitization process. This would allow the use of style={ fmt.Sprintf("background-image: url(%v)", url) } or the use of a dynamic builder (not implemented, just an example) style={ css.BackgroundImage(url).PaddingPx(10, 10, 10, 10) }.

Summary

I think this would be a smart use of time, and would allow us to spend more time on the LSP and DX areas of templ.

Thoughts?

@jimafisk
Copy link

These tools require a more complex setup in that you have to have node installed, and will need an esbuild config mjs file, a package.json and a postcss config

For me personally, this is something I'd really like to avoid. The main reason I picked Templ over Svelte for my new app was to avoid the JS ecosystem. I'd prefer a built-in solution, even if it had less features, but of course I understand the constraints on developer time and know you have a lot of competing priorities. Templ is awesome overall, thanks for sharing your thoughts with us in real-time!

@bastianwegge
Copy link
Contributor

I agree with the proposal, as I don't think CSS should be a scope for templ at all. I would also like to opt out of the JS ecosystem (@jimafisk ) but I think it's almost impossible right now.

We migrated a medium sized go+react project completely to templ. As we swapped away from React components, we also needed interactivity. We looked into web-components, especially things like stencil or lit but this would ultimately bring back JS dependencies and build processes back into the project. We ended up using HTMX + Alpine.js which is an awesome combination that really drives the idea of locality of behavior and HATEOAS, but it definitely is not a silver-bullet and I'd be open to better solutions.

@zoop-btc
Copy link

I started building with templ precisely because I don't want to setup JS tooling. Integrating with them is fine as long as it's optional.

@stuartaccent
Copy link

im not advertising what ive done just a mere food for thought. Im using temple and very quickly found I needed more than the css provided so started our own way of generating css from go. very early days and still working out the best way to use it and what we need from it.

https://github.com/AccentDesign/gcss

@a-h
Copy link
Owner Author

a-h commented May 23, 2024

I love what you're doing there @stuartaccent.

Couple of ideas (not sure if you've already of thought of this)...

If you wanted to, you could update the gcss.Style type to have a Render method.

func (s *Style) Render(ctx context.Context, w io.Writer) error {
  //TODO: Get a CSP nonce from the context.
  //TODO: Check errors etc.
  io.WriteString(w, "<style type=\"text/css\">\n")
  s.CSS(w)
  io.WriteString(w, "\n</style>")
}

func (s *Style) CSS(w io.Writer) error {
  // Existing
}

You could also create a gcss.Stylesheet type which is an alias of []Style, and add the Render method to that too.

type Stylesheet []Style

func (ss Stylesheet) Render(ctx context.Context, w io.Writer) error {
  //TODO: Get a CSP nonce from the context.
  //TODO: Check errors etc.
  io.WriteString(w, "<style type=\"text/css\">\n")
  for _, s := range ss {
    s.CSS(w)
  }
  io.WriteString(w, "\n</style>")
}

Doing this would mean that gcss.Style and gcss.Stylesheet both implement templ.Component and you can drop styles into components. Combined with the new templ.Once function (#750), you'd be able to have CSS that's only loaded once per HTTP request.

So, defining a set of styles as a templ.Component that's only rendered once per HTTP request.

package deps

var buttonStyles = gcss.StyleSheet{
	{
		Selector: ".button",
		Props: gcss.Props{
			AlignItems:     props.AlignItemsCenter,
			BorderRadius:   radius,
			Display:        props.DisplayInlineFlex,
			FontSize:       fontMd,
			FontWeight:     props.FontWeightMedium,
			Height:         spacing10,
			JustifyContent: props.JustifyContentCenter,
			LineHeight:     leadingTight,
			PaddingTop:     spacing2,
			PaddingRight:   spacing4,
			PaddingBottom:  spacing2,
			PaddingLeft:    spacing4,
		},
	},
	// More...
}

var buttonStylesHandle = templ.NewOnceHandle()

templ ButtonCSS(ctx context.Context, w io.Writer) (err error) {
  @buttonStylesHandle.Once() {
    @buttonStyles()
  }
}

Then, using those CSS classes as components in templ:

package components

templ Button(name string) {
  @deps.ButtonCSS
  <button class="button">{name}</button>
}

templ ButtonPrimary(name string) {
  @deps.ButtonCSS
  <button class="button-primary">{name}</button>
}

If you then use Button and ButtonPrimary on a page (bad API design, I know, it's just as an example - https://gopress.io/ uses a struct for attrs which is likely more appropriate here).

But, in this case the CSS would only be loaded into the page as a <style> element if required by the component.

Obviously, I'm not aware of gcss, so maybe you've already discounted these ideas. 😁

Either way, it looks very great.

@stuartaccent
Copy link

thanks, @a-h that looks like a solid idea i will take a look.

cheers stu.

@joerdav joerdav added NeedsDecision Issue needs some more discussion so a decision can be made proposal labels May 30, 2024
@Riu
Copy link

Riu commented Jun 4, 2024

As a Gopress creator, all I can say is - CSS in template engines is a huge topic. It is important to answer the questions: what architecture are we creating for, where and how will we embed styles, what do we want to share/reuse or what should be global, how will we introduce changes (e.g. after changing from the UI designer in Figma). Some challenges:

  • if you have inline css then there will be problem with global changes;
  • if you put styles in files you need to serve only what you use;
  • if you are rendering mail templates maybe you need pure inline css;
  • if you have got microfrontends/htmx architecture, then the number of challenges doubles;
  • if you would like to make library components like we have in FE frameworks (scoped styles), you should not render <style>css code</style> every time you render arrays of components;
  • much, much more...

Gopress has a very unique approach in that we have sets of Tailwind classes that are transferred one-to-one from Golang to, for example, Vue, because we use an atomic design approach. These sets create variants based on attributes. Instead of classes you can use just pure css (it will be worse DX, but the effect can be more or less the same). DX is very important - you can see immediately what styles the class gives (syntax suggestions from Tailwind). The component is isolated and the only dependency is the Tailwind configuration file.

Because Tailwind is also not perfect i'm working on alternative solution (without Tailwind) and one more thing - by design, Gopress was built in such a way that components could be generated and changed from an application designed for this purpose. This is related, on the one hand, to AI and, on the other hand, to the problem of technology adoption.

Gcss is interesting because it has no dependency and in some cases it can be good solution, definitely worth attention.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
NeedsDecision Issue needs some more discussion so a decision can be made proposal
Projects
None yet
Development

No branches or pull requests

7 participants