How to define css variables in style attribute in React and TypeScript
Asked Answered
I

10

204

I want to define jsx like this:

<table style={{'--length': array.lenght}}>
   <tbody>
      <tr>{array}</tr>
   </tbody>
</table>

and I use --length in CSS, I also have cells that have --count that shows count using CSS pseudo selector (using the counter hack).

but typescript throws an error:

TS2326: Types of property 'style' are incompatible.
  Type '{ '--length': number; }' is not assignable to type 'CSSProperties'.
    Object literal may only specify known properties, and ''--length'' does not exist in type 'CSSProperties'.

is it possible to change the type of style attribute to accept CSS variables (custom properties) or is there a way to force any on the style object?

Incorporator answered 24/8, 2018 at 13:0 Comment(4)
i believe this is discussed here github.com/facebook/react/issues/6411Foul
@Foul I've seen this, in my code css variables works they appear in DOM and css is applied, but they give error in webpack (it look like error but compile pass) when building the app, so it's the problem with typescript typings not with react.Incorporator
@KyawSiesein they problem with js variables is that you can't use them in ::before and ::after.Incorporator
@KyawSiesein this is completely valid and normal in Design Systems. You certainly CAN define local CSS Variables inline.Butylene
T
52

Casting the style to any defeats the whole purpose of using TypeScript, so I recommend extending React.CSSProperties with your custom set of properties:

import React, {CSSProperties} from 'react';

export interface MyCustomCSS extends CSSProperties {
  '--length': number;
}

By extending React.CSSProperties, you will keep TypeScript's property checking alive and you will be allowed to use your custom --length property.

Using MyCustomCSS would look like this:

const MyComponent: React.FC = (): JSX.Element => {
  return (
    <input
      style={
        {
          '--length': 300,
        } as MyCustomCSS
      }
    />
  );
};
Theolatheologian answered 29/1, 2021 at 17:49 Comment(6)
This is interesting because now the style can have only specific custom properties. Not sure how another answers handle this, I also don't have any react/typescript project to test.Incorporator
@Incorporator because MyCustomCSS extends from CSSProperties other properties should be assignable as well.Theolatheologian
I mean that I like the solution because I can only use custom properties I've defined in the type, so If I set --length: number; this will be the only custom property I can use. I know that rest of css works fine this is how extend works any language. by "can have only specific custom properties" I meant that for all custom properties in CSS only those that was defined by the type will be valid.Incorporator
TS 4.4 supports template literal types, so something like this can be written instead of explicitly specifying all custom variables: [key: `--${string}`]: string | number;. One liner: type MyCustomCSS = CSSProperties & Record<`--${string}`, number | string>;Tbilisi
I prefer this one liner solution (@brc-dd) I have tested and I confirm it works perfectly!Duvetyn
I'm marking yours as the accepted answer since this is the right way. Every other answer just ignores the types of custom property or modifies CSSProperties globally. Your solution actually makes the custom property part of the type locally per usage.Incorporator
W
329

Like this:

function Component() {
  const style = { "--my-css-var": 10 } as React.CSSProperties;
  return <div style={style}>...</div>
}

Or without the extra style variable:

function Component() {
  return <div style={{ "--my-css-var": 10 } as React.CSSProperties} />
}
Wreckfish answered 10/1, 2019 at 11:50 Comment(1)
Me too right now, but sure, this dirty cast is the easiest solution for this case.Calorie
M
69

you can simply put this module declaration merge using string templates at the top of the file or in any .d.ts file, then you will be able to use any CSS variable as long it starts '--' and that is string or number

import 'react';

declare module 'react' {
    interface CSSProperties {
        [key: `--${string}`]: string | number
    }
}

for example

<div style={{ "--value": percentage }} />
Missive answered 17/12, 2021 at 19:38 Comment(9)
So you suggest to overwrite builtin type, what if you have 10 modules and each use different variables?Incorporator
as you may know typescript interfaces are merged by default, they do not overwrite the existing, they extend them, source: typescriptlang.org/docs/handbook/declaration-merging.htmlMissive
I ended up using this solution, thanks. One thing to note: it's possible to pass in values with more dashes than two (as a dash is a string). For example, this will be fine: style={{ ---color: 'red' }}. This isn't an issue for my use case, but I just wanted to add this as a note if someone else wonders the same.Violation
yes, you can use any valid javascript string as pattern in the typescript template literal stringMissive
Remember to place import 'react' at the top!Vertumnus
@Vertumnus not necessary in react 17Missive
This is the one true answer. I wonder why this is not built in. Seems like generally accurate typingDoubletongued
TS2590: Expression produces a union type that is too complex to represent.Kyl
I was able to use declare namespace React instead of declare module 'react'. Not sure what the differences are, though.Valadez
T
52

Casting the style to any defeats the whole purpose of using TypeScript, so I recommend extending React.CSSProperties with your custom set of properties:

import React, {CSSProperties} from 'react';

export interface MyCustomCSS extends CSSProperties {
  '--length': number;
}

By extending React.CSSProperties, you will keep TypeScript's property checking alive and you will be allowed to use your custom --length property.

Using MyCustomCSS would look like this:

const MyComponent: React.FC = (): JSX.Element => {
  return (
    <input
      style={
        {
          '--length': 300,
        } as MyCustomCSS
      }
    />
  );
};
Theolatheologian answered 29/1, 2021 at 17:49 Comment(6)
This is interesting because now the style can have only specific custom properties. Not sure how another answers handle this, I also don't have any react/typescript project to test.Incorporator
@Incorporator because MyCustomCSS extends from CSSProperties other properties should be assignable as well.Theolatheologian
I mean that I like the solution because I can only use custom properties I've defined in the type, so If I set --length: number; this will be the only custom property I can use. I know that rest of css works fine this is how extend works any language. by "can have only specific custom properties" I meant that for all custom properties in CSS only those that was defined by the type will be valid.Incorporator
TS 4.4 supports template literal types, so something like this can be written instead of explicitly specifying all custom variables: [key: `--${string}`]: string | number;. One liner: type MyCustomCSS = CSSProperties & Record<`--${string}`, number | string>;Tbilisi
I prefer this one liner solution (@brc-dd) I have tested and I confirm it works perfectly!Duvetyn
I'm marking yours as the accepted answer since this is the right way. Every other answer just ignores the types of custom property or modifies CSSProperties globally. Your solution actually makes the custom property part of the type locally per usage.Incorporator
D
49

If you go to the definition of CSSProperties, you'll see:

export interface CSSProperties extends CSS.Properties<string | number> {
    /**
     * The index signature was removed to enable closed typing for style
     * using CSSType. You're able to use type assertion or module augmentation
     * to add properties or an index signature of your own.
     *
     * For examples and more information, visit:
     * https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors
     */
}

That page gives examples of how to solve the type error by augmenting the definition of Properties in csstype or casting the property name to any.

Depalma answered 25/8, 2018 at 0:50 Comment(1)
Link is out of date, if it was useful before it should likely be pointed at a commit rather than at master.Serrell
C
45

You can add a type assertion to the variable. i.e. {['--css-variable' as any]: value }

<table style={{['--length' as any]: array.length}}>
   <tbody>
      <tr>{array}</tr>
   </tbody>
</table>
Closegrained answered 31/5, 2019 at 14:50 Comment(1)
suppressing typescript error is not the best solutionInterlaken
F
17

try:

<table style={{['--length' as string]: array.lenght}}>
  ...
</table>
Futility answered 1/12, 2022 at 0:23 Comment(1)
It worked like a charm. I just didn't get why the string literal doesn't work since it's a string. Could you add more details to your answer? Thanks!Henchman
M
5
import "react";

type CustomProp = { [key in `--${string}`]: string };
declare module "react" {
  export interface CSSProperties extends CustomProp {}
}

put this in your global.d.ts file

Maxwell answered 27/9, 2021 at 11:45 Comment(6)
How is it better than the existing answers ?Glassman
I don't think it's a good idea to overwrite react typescript types. So I would say it's a canonical example of a kludge.Incorporator
@Incorporator the React devs disagree with you, as evidenced by the comment quoted in https://mcmap.net/q/126861/-how-to-define-css-variables-in-style-attribute-in-react-and-typescriptPhony
@flyingsheep this is not overwriting the type, it only shows how the code looks like.Incorporator
@flyingsheep maybe I should give your the actual solution to the problem. It was not overwriting the builtin types.Incorporator
Why do you hint at the solution you came up with and call it “the actual” solution? We’re in real life, not an academic test. Therefore there’s only your solution, not the, and if you feel like it’s worth sharing, just do it. I didn’t sign up for a course, don’t treat me like a student.Phony
T
1

I would like to add a different approach by using document.body.style.setProperty, and maybe if your css variable will be affected by certain props you can put it in a useEffect like this:

useEffect(() => {
    document.body.style.setProperty(
      "--image-width-portrait",
      `${windowSize.width - 20}px`
    );
}, [windowSize])

Later inside your css file you can call it like this:

width: var(--image-width-portrait);
Toler answered 24/5, 2022 at 16:39 Comment(1)
This is a component that can have multiple instances on the page.Incorporator
B
1

These are (well almost) all valid approaches to solve this, but there is another.

You could add the ref to your element and set the style where ever. I know that this would be quite possibly an improper use of useEffect but if you have something in useEffect that needs to happen on component mount then:

const tableRef = useRef<HTMLTableElement | null>(null)

useEffect(() => {
  tableRef?.current?.style.setProperty('--length': array.lenght);
}, [])
...

<table ref={tableRef}>
   <tbody>
      <tr>{array}</tr>
   </tbody>
</table>

This can also be used on any interaction and event

Blasius answered 14/2, 2023 at 16:27 Comment(1)
This will work but I don't think this is a good idea. If your CSS depends on the variable it may recalculate styles for no reason. It's better to render everything at once. Unless you use ref outside of React rendering.Incorporator
B
1

Augment CSSProperties:

declare module "react" {
  interface CSSProperties {
    "--length"?: number;
  }
}

Then use CSSProperties as usual.

Some other answers recommends allowing all "--" keys. I would recommend specifying "--length" as it is useful in guarding against mistyped string literals.

This approach is also better than Benny Code's approach of defining interface MyCustomCSSProperties extends CSSProperties, because the "--length" property won't be able to interact nicely with any other interactions with types defined by dependencies that uses CSSProperties.

Another side point: This approach is recommended by the package author: https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors The React CSSProperties derives from this linked package.

Belshazzar answered 31/1 at 8:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.