Web Development

What If()? Using Conditional CSS Variables

A cool new feature is coming to CSS: if().

With this, you can set a style or a CSS variable based on some condition, like whether the device is on a touchscreen, or whether some CSS property is supported, or even based on an attribute on an element.

It’s really cool, and sooner or later we’ll all be (ab)using it.

Actually, you can pretty much do this today using CSS Variables, with a little understanding of how variable fallbacks work and how you can use this to make your own conditionals.

What are CSS Variables?

CSS Variables make it easy to use reuse colors and styles across a CSS codebase. At their most basic, they look like this:

:root {
    --my-fg-color: red;
    --my-bg-color: blue;
}

body {
    color: var(--my-fg-color);
    background: var(--my-bg-color);
}

CSS Variables can have fallbacks, which falls back to some other value if the variable isn’t defined. Here, the color will be black if --my-custom-fg-color wasn’t defined in the scope.

body {
    color: var(--my-custom-fg-color, black);
}

This can be nested with other CSS variables:

body {
    color: var(--my-custom-fg-color, var(--my-fg-color, black));
}

Fallbacks depend on whether the variable is truthy or falsy.

Truthy and Falsy Variables

When a variable is falsy, var() will use the fallback value.

When it is truthy, the fallback value won’t be used.

Normally, it’s falsy if the variable isn’t defined, and truthy if it is. But there are a couple tricks to know about:

  1. You can make a variable falsy by setting it to the special keyword initial.
  2. You can make a variable truthy (without expanding to a value) by making the value empty.

Let me show you what I mean:

:root {
    /* Empty -- Truthy and a no-op */
    --no-fallback: ;          

    /* 'initial' -- Falsy, use fallback */
    --use-fallback: initial;
}

body {
    background: var(--no-fallback, grey);
    color: var(--use-fallback, blue);
}

This will result in the following CSS:

body {
    background: ;  /* No value -- ignored. */
    color: blue;
}

Our new --use-fallback will ensure the fallback value will be used, and --no-fallback will ensure it’s never used (but won’t leave behind a value).

These are our core building blocks for conditional CSS Variables.

We can then combine these rules.

body {
    background:
        var(--no-fallback, red)
        var(--use-fallback, pink);

    color:
        var(--no-fallback, pink)
        var(--use-fallback, red);
}

This will give us:

body {
    background: pink;
    color: red;
}

Behold:

A red on pink "Hello, world!"

You can play around with it here:

Armed with this knowledge, we can now start setting CSS Variables based on other state, and trigger the conditions.

Setting Conditional Variables

Let’s set up another contrived example. We’re going to put together some HTML with a button that styles differently based on where it’s placed. In this case, when in <main>, it will appear blue-on-pink and elsewhere it’ll appear pink-on-blue.

It’s my post and I will use hideous color palettes if I want to.

Here’s our HTML:

<html>
 <body>
  <button>Hello</button>
  <main>
   <button>There</button>
  </main>
  <button>Friend</button>
 </body>
</html>

We’re going to set up some conditional state variables based on the main placement. (Yep, contrived example, but this is just for demonstration purposes.)

Here’s our CSS:

:root {
    --if-main: ;
    --if-not-main: initial;
}

main {
    --if-main: initial;
    --if-not-main: ;
}

button {
    background:
        var(--if-main, pink)
        var(--if-not-main, blue);

    color:
        var(--if-main, blue)
        var(--if-not-main, pink);
}

By default, --if-main will be truthy (so no fallback) and --if-not-main will be falsy (so it’ll use the fallback).

When in <main>, we reverse that.

The button will style the background and foreground colors based on that state.

And here’s what we get:

Three buttons. "Hello" with pink on blue, "There" with blue on pink, and "Friend" with pink on blue.

Beautiful.

You can play with that here:

Let’s Build Dark Mode

It’s 2025, so we want to support light and dark modes. There’s no end of articles on how to do this, and now you’re reading another one of them.

A common approach is to write different rules based on CSS classes.

body.-is-light {
    background: orange;
    color: green;
}

body.-is-dark {
    background: darkorange;
    color: darkgreen;
}

That works if we’re manually controlling CSS classes on body, but these days we want to respect system settings, so we use media selectors like so:

@media (prefers-color-scheme: light) {
    body {
        background: orange;
        color: green;
    }
}

@media (prefers-color-scheme: dark) {
    body {
        background: darkorange;
        color: darkgreen;
    }
}

But now we can’t give users a nice toggle on the page to control their light/dark modes, so we’ll want to bring the CSS classes back with the media selectors:

body.-is-light {
    background: orange;
    color: green;
}

body.-is-dark button {
    background: darkorange;
    color: darkgreen;
}

@media (prefers-color-scheme: light) {
    body {
        background: orange;
        color: green;
    }
}

@media (prefers-color-scheme: dark) {
    body {
        background: darkorange;
        color: darkgreen;
    }
}

Okay, now things are just out of control. And we haven’t moved beyond the <body> tag. We’d have to repeat this for everything else we want to style!

Yuck. I hate CSS.

Let’s Hate CSS Less

It all comes down to this. We’re going to take our building blocks and clean this all up.

Here’s our plan of attack:

  1. We’re going to define some truthy and falsy CSS variables saying if we’re in light or dark mode.
  2. We’re going to consolidate the styles for our components and use our conditional var() statements.

We’ll first set up our state variables. We only need to do this once for the whole codebase.

/* We'll make light mode our default. Everyone loves light mode. */
:root,
:root.-is-light {
    --if-light: initial;
    --if-dark: ;

    color-scheme: light;
}

:root.-is-dark {
    --if-light: ;
    --if-dark: initial;

    color-scheme: dark light;
}

@media (prefers-color-scheme: light) {
    :root {
        --if-light: initial;
        --if-dark: ;

        color-scheme: light;
    }
}

@media (prefers-color-scheme: dark) {
    :root {
        --if-light: ;
        --if-dark: initial;

        color-scheme: dark light;
    }
}

Now let’s use all that to style <body> and <button>:

body {
    background:
        var(--if-light, orange)
        var(--if-dark, darkorange);

    color:
        var(--if-light, darkgreen)
        var(--if-dark, blue);
}

button {
    background:
        var(--if-light, pink)
        var(--if-dark, blue);

    color:
        var(--if-light, blue)
        var(--if-dark, pink);
}

Look at that. So much simpler to maintain. And we can extend that too, if we want to add high-constrast mode or something.

Let’s test it.

Here it is when we switch our system to dark mode:

A blue-on-dark-orange page saying "Welcome to my beautiful page.", with three pink-on-blue buttons saying "Hello", "There", and "Friend".

Far less eye strain than light. I’m sold.

Now light mode:

A green-on-light-orange page saying "Welcome to my beautiful page.", with three blue-on-pink buttons saying "Hello", "There", and "Friend".

And system dark mode with <html class="is-light"> overriding to light mode.

A green-on-light-orange page saying "Welcome to my beautiful page.", with three blue-on-pink buttons saying "Hello", "There", and "Friend".

And system light mode with <html class="is-dark"> overriding to dark mode.

A blue-on-dark-orange page saying "Welcome to my beautiful page.", with three pink-on-blue buttons saying "Hello", "There", and "Friend".

Perfection.

You can play with that one here:

Nesting Conditionals!

That’s right, you can nest them! Let’s update our Light/Dark Mode example to make a prettier color scheme, because I’m starting to realize what we had was kind of ugly.

We’ll define new --if-pretty and --if-ugly states to go along with --if-dark and --if-light:

:root,
:root.-is-light {
    --if-pretty: ;
    --if-ugly: initial;

    ...
}

:root.-is-pretty {
    --if-pretty: initial;
    --if-ugly: ;
}

...

And now we can build some truly beautiful styling, using nested conditionals:

body {
    background:
        var(--if-pretty,
            var(--if-light,
                linear-gradient(
                    in hsl longer hue 45deg,
                    red 0 100%)
                )
            )
            var(--if-dark,
                black
                linear-gradient(
                    in hsl longer hue 45deg,
                    rgba(255, 0, 0, 20%) 0 100%)
                )
            )
        )
        var(--if-ugly,
            var(--if-light, orange)
            var(--if-dark, darkorange)
        );

    color:
        var(--if-pretty,
            var(--if-light, black)
            var(--if-dark, white)
        )
        var(--if-ugly,
            var(--if-light, darkgreen)
            var(--if-dark, blue)
        );

    font-size: 100%;
}

button {
    background:
        var(--if-pretty,
            radial-gradient(
                ellipse at top,
                orange,
                transparent
            ),
            radial-gradient(
                ellipse at bottom,
                orange,
                transparent
            )
        )
        var(--if-ugly,
            var(--if-light, pink)
            var(--if-dark, blue)
        );

  color:
        var(--if-pretty, black)
        var(--if-ugly,
            var(--if-light, blue)
            var(--if-dark, pink)
        );
}

In standard Ugly Mode, this looks the same as before, but when we apply Pretty Mode with Light Mode, we get:

A black-on-rainbow page saying "Welcome to my beautiful page.", with three orange circular gradient buttons saying "Hello", "There", and "Friend".

And then with Dark Mode:

A white-on-dark-rainbow page saying "Welcome to my beautiful page.", with three orange circular gradient buttons saying "Hello", "There", and "Friend".

I am available for design consulting work on a first-come, first-serve basis only.

Here it is so you can enjoy it yourself:

A Backport from If()

Google’s article on if() has a neat demo showing how you can style some cards based on the value of a data-status= attribute. Based on whether the value is pending, complete, or anything else, the cards will be placed in different columns and have different background and border colors.

Here’s the card:

<div class="card" data-status="complete">
  ...
</div>

Here’s how you’d do that with if() (from their demo):

.card {
    --status: attr(data-status type(<custom-ident>));

    border-color: if(
        style(--status: pending): royalblue;
        style(--status: complete): seagreen;
        else: gray);

    background-color: if(
        style(--status: pending): #eff7fa;
        style(--status: complete): #f6fff6;
        else: #f7f7f7);

    grid-column: if(
        style(--status: pending): 1;
        style(--status: complete): 2;
        else: 3);
}

To do this with CSS Variables, we can define our states using CSS selectors on the attribute, and then style it the way we did earlier:

/* Define our variables and conditions. */
.card {
  --if-pending: ;
  --if-complete: ;
  --if-default: initial;
}

.card[data-status="pending"] {
  --if-pending: initial;
  --if-default: ;
}

.card[data-status="complete"] {
  --if-complete: initial;
  --if-default: ;
}

/* Style our cards. */
.card {
  border-color:
    var(--if-pending, royalblue)
    var(--if-complete, seagreen)
    var(--if-default, grey);

  background-color:
    var(--if-pending, #eff7fa)
    var(--if-complete, #f6fff6)
    var(--if-default, #f7f7f7);

  grid-column:
    var(--if-pending, 1)
    var(--if-complete, 2)
    var(--if-default, 3);
}

Here’s the ported version, live:

It’s Available Now!

You can use this today in all browsers that support CSS Variables. That’s.. counts.. years worth of browsers.

It requires a bit more work to set up the variables, but the nice thing is, once you’ve done that, the rest is highly maintainable. And usage is close enough to that of if() that you can more easily transition once support is widespread.

We’ve based Ink, our (still very young) CSS component library we use for Review Board, around this conditionals pattern. It’s helped us to support light and dark modes along with the beginnings of low- and high-contrast modes and state for some components without tearing our hair out.

Give it a try, play with the demos, and see if it’s a good fit for your codebase. Begin taking advantage of what if() has to offer today, so you can more easily port tomorrow.

What If()? Using Conditional CSS Variables Read More »

CodeMirror and Spell Checking: Solved

A screenshot of the CodeMirror editor with spelling issues and the browser's spell checking menu opened.

For years I’ve wanted spell checking in CodeMirror. We use CodeMirror in our Review Board code review tool for all text input, in order to allow on-the-fly syntax highlighting of code, inline image display, bold/italic, code literals, etc.

(We’re using CodeMirror v5, rather than v6, due to the years’ worth of useful plugins and the custom extensions we’ve built upon it. CodeMirror v6 is a different beast. You should check it out, but we’re going to be using v5 for our examples here. Much of this can likely be leveraged for other editing components as well.)

CodeMirror is a great component for the web, and I have a ton of respect for the project, but its lack of spell checking has always been a complaint for our users.

And the reason for that problem lies mostly on the browsers and “standards.” Starting with…

ContentEditable Mode

Browsers support opting an element into what’s called Content Editable mode. This allows any element to become editable right in the browser, like a fancy <textarea>, with a lot of pretty cool capabilities:

  • Rich text editing
  • Rich copy/paste
  • Selection management
  • Integration with spell checkers, grammar checkers, AI writing assistants
  • Works as you’d expect with your device’s native input methods (virtual keyboard, speech-to-text, etc.)

Simply apply contenteditable="true" to an element, and you can begin typing away. Add spellcheck="true" and you get spell checking for free. Try it!

And maybe you don’t even need spellcheck="true"! The browser may just turn it on automatically. But you may need spellcheck="false" if you don’t want it on. And it might stay on anyway!

Here we reach the first of many inconsistencies. Content Editable mode is complex and not perfectly consistent across browsers (though it’s gotten better). A few things you might run into include:

  • Ranges for selection events and input events can be inconsistent across browsers and are full of edge cases (you’ll be doing a lot of “let me walk the DOM and count characters carefully to find out where this selection really starts” checks).
  • Spell checking behaves quite differently on different browsers (especially in Chrome and Safari, which might recognize a word as misspelled but won’t always show it).
  • Rich copy/paste may mess with your DOM structure in ways you don’t expect.
  • Programmatic manipulating of the text content using execCommand is deprecated with no suitable replacement (and you don’t want to mess with the DOM directly or you break Undo/Redo). It also doesn’t always play nice with input events.

CodeMirror v5 tries to let the browser do its thing and sync state back, but this doesn’t always work. Replacing misspelled words on Safari or Chrome can sometimes cause text to flip back-and-forth. Data can be lost. Cursor positions can change. It can be a mess.

So while CodeMirror will let you enable both Content Editable and Spell Checking modes, it’s at your own peril.

Which is why we never enabled it.

How do we fix this?

When CodeMirror v5 was introduced, there weren’t a lot of options. But browsers have improved since.

The secret sauce is the beforeinput event.

There are a lot of operations that can involve placing new text in a Content Editable:

  • Replacing a misspelled word
  • Using speech-to-text
  • Having AI generate content or rewrite existing content
  • Transforming text to Title Case

These will generate a beforeinput event before making the change, and an input event after making the change.

Both events provide:

  1. The type of operation:
    1. insertText for text-to-speech or newly-generated text
    2. insertReplacementText for spelling replacements, AI rewrites, and other similar operations
  2. The range of text being replaced (or where new text will be inserted)
  3. The new data (either as InputEvent.data in the form of one or more InputEvent.dataTransferItem.items[] entries)

Thankfully, beforeinput can be canceled, which prevents the operation from going through.

This is our way in. We can treat these operations as requests that CodeMirror can fulfill, instead of changes CodeMirror must react to.

A screenshot of a text field in CodeMirror with text highlighted and macOS's AI Writing Tools providing a concise version of the text.

Putting our plan into action

Here’s the general approach:

  1. Listen to beforeinput on CodeMirror’s input element (codemirror.display.input.div).
  2. Filter for the following InputEvent.inputType values: 'insertReplacementText', 'insertText'.
  3. Fetch the ranges and the new plain text data from the InputEvent.
  4. For each range:
    1. Convert each range into a start/end line number within CodeMirror, and a start/end within each line.
    2. Issue a CodeMirror.replaceRange() with the normalized ranges and the new text.

Simple in theory, but there’s a few things to get right:

  1. Different browsers and different operations will report those ranges on different elements. They might be text nodes, they might be a parent element, or they might be the top-level contenteditable element. Or a combination. So we need to be very careful about our assumptions.
  2. We need to be able to calculate those line numbers and offsets. We won’t necessarily have that information up-front, and it depends on what nodes we get in the ranges.
  3. The text data can come from more than one place:
    1. An InputEvent.data attribute value
    2. One or more strings accessed asynchronously from InputEvent.dataTransfer.items[], in plain text, HTML, or potentially other forms.
  4. We may not have all of this! Even as recently as late-2024, Chrome wasn’t giving me target ranges in beforeinput, only in input, which was too late. So we’ll want to bail if anything goes wrong.

Let’s put this into practice. I’ll use TypeScript to help make some of this a bit more clear, but you can do all this in JavaScript.

Feel free to skip to the end, if you don’t want to read a couple pages of TypeScript.

1. Set up our event handler

We’re going to listen to beforeinput. If it’s an event we care about, we’ll grab the target ranges, take over from the browser (by canceling the event), and then prepare to replay the operation using CodeMirror’s API.

This is going to require a bit of help figuring out what lines and columns those ranges correspond to, which we’ll tackle next.

const inputEl = codeMirror.display.input.div;
	
inputEl.addEventListener('beforeinput',
                         (evt: InputEvent) => {
    if (evt.inputType !== 'insertReplacementText' &&
        evt.inputType !== 'insertText') {
        /*
         * This isn't a text replacement or new text event,
         * so we'll want to let the browser handle this.
         *
         * We could just preventDefault()/stopPropagation()
         * if we really wanted to play it safe.
         */
        return;
    }

    /*
     * Grab the ranges from the event. This might be
     * empty, which would have been the case on some
     * versions of Chrome I tested with before. Play it
     * safe, bail if we can't find a range.
     *
     * Each range will have an offset in a start container
     * and an offset in an end container. These containers
     * may be text nodes or some parent node (up to and
     * including inputEl).
     */
    const ranges = evt.getTargetRanges();

    if (!ranges || ranges.length === 0) {
        /* We got empty ranges. There's nothing to do. */
        return;
    }

    const newText =
           evt.data
        ?? evt.dataTransfer?.getData('text')
        ?? null;

	if (newText === null) {
		/* We couldn't locate any text, so bail. */
        return;
	}

    /*
     * We'll take over from here. We don't want the browser
     * messing with any state and impacting CodeMirror.
     * Instead, we'll run the operations through CodeMirror.
     */
    evt.preventDefault();
    evt.stopPropagation();

    /*
     * Place the new text in CodeMirror.
     *
     * For each range, we're getting offsets CodeMirror
     * can understand and then we're placing text there.
     *
     * findOffsetsForRange() is where a lot of magic
     * happens.
     */
    for (const range of state.ranges) {
        const [startOffset, endOffset] =
            findOffsetsForRange(range);

        codeMirror.replaceRange(
            newText,
            startOffset,
            endOffset,
            '+input',
        );
    }
});

This is pretty easy, and applicable to more than CodeMirror. But now we’ll get into some of the nitty-gritty.

2. Map from ranges to CodeMirror positions

Most of the hard work really comes from mapping the event’s ranges to CodeMirror line numbers and columns.

We need to know the following:

  1. Where each container node is in the document, for each end of the range.
  2. What line number each corresponds to.
  3. What the character offset is within that line.

This ultimately means a lot of traversing of the DOM (we can use TreeWalker for that) and counting characters. DOM traversal is an expense we want to incur as little as possible, so if we’re working on the same nodes for both end of the range, we’ll just calculate it once.

function findOffsetsForRange(
    range: StaticRange,
): [CodeMirror.Position, CodeMirror.Position] {
    /*
     * First, pull out the nodes and the nearest elements
     * from the ranges.
     *
     * The nodes may be text nodes, in which case we'll
     * need their parent for document traversal.
     */
    const startNode = range.startContainer;
    const endNode = range.endContainer;

    const startEl = (
        (startNode.nodeType === Node.ELEMENT_NODE)
        ? startNode as HTMLElement
        : startNode.parentElement);
    const endEl = (
        (endNode.nodeType === Node.ELEMENT_NODE)
        ? endNode as HTMLElement
        : endNode.parentElement);

    /*
     * Begin tracking the state we'll want to return or
     * use in future computations.
     *
     * In the optimal case, we'll be calculating some of
     * this only once and then reusing it.
     */
    let startLineNum = null;
    let endLineNum = null;
    let startOffsetBase = null;
    let startOffsetExtra = null;
    let endOffsetBase = null;
    let endOffsetExtra = null;

    let startCMLineEl: HTMLElement = null;
    let endCMLineEl: HTMLElement = null;

    /*
     * For both ends of the range, we'll need to first see
     * if we're at the top input element.
     *
     * If so, range offsets will be line-based rather than
     * character-based.
     *
     * Otherwise, we'll need to find the nearest line and
     * count characters until we reach our node.
     */
    if (startEl === inputEl) {
        startLineNum = range.startOffset;
    } else {
        startCMLineEl = startEl.closest('.CodeMirror-line');
        startOffsetBase = findCharOffsetForNode(startNode);
        startOffsetExtra = range.startOffset;
    }

    if (endEl === inputEl) {
        endLineNum = range.endOffset;
    } else {
        /*
         * If we can reuse the results from calculations
         * above, that'll save us some DOM traversal
         * operations. Otherwise, fall back to doing the
         * same logic we did above.
         */
        endCMLineEl =
            (range.endContainer === range.startContainer &&
             startCMLineEl !== null)
            ? startCMLineEl
            : endEl.closest(".CodeMirror-line");

        endOffsetBase =
            (startEl === endEl && startOffsetBase !== null)
            ? startOffsetBase
            : findCharOffsetForNode(endNode);
        endOffsetExtra = range.endOffset;
    }

    if (startLineNum === null || endLineNum === null) {
        /*
         * We need to find the line numbers that correspond
         * to either missing end of our range. To do this,
         * we have to walk the lines until we find both our
         * missing line numbers.
         */
        for (let i = 0;
             (i < children.length &&
              (startLineNum === null || endLineNum === null));
             i++) {
            const child = children[i];

            if (startLineNum === null &&
                child === startCMLineEl) {
                startLineNum = i;
            }

            if (endLineNum === null &&
                child === endCMLineEl) {
                endLineNum = i;
            }
        }
    }

    /*
     * Return our results.
     *
     * We may not have set some of the offsets above,
     * depending on whether we were working off of the
     * CodeMirror input element, a text node, or another
     * parent element. And we didn't want to set them any
     * earlier, because we were checking to see what we
     * computed and what we could reuse.
     *
     * At this point, anything we didn't calculate should
     * be 0.
     */
    return [
        {
            ch: (startOffsetBase || 0) +
                (startOffsetExtra || 0),
            line: startLineNum,
        },
        {
            ch: (endOffsetBase || 0) +
                (endOffsetExtra || 0),
            line: endLineNum,
        },
    ];
}


/*
 * The above took care of our line numbers and ranges, but
 * it got some help from the next function, which is designed
 * to calculate the character offset to a node from an
 * ancestor element.
 */
function findCharOffsetForNode(
    targetNode: Node,
): number {
    const targetEl = (
        targetNode.nodeType === Node.ELEMENT_NODE)
        ? targetNode as HTMLElement
        : targetNode.parentElement;
    const startEl = targetEl.closest('.CodeMirror-line');
    let offset = 0;

    const treeWalker = document.createTreeWalker(
        startEl,
        NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
    );

    while (treeWalker.nextNode()) {
        const node = treeWalker.currentNode;

        if (node === targetNode) {
            break;
        }

        if (node.nodeType === Node.TEXT_NODE) {
            offset += (node as Text).data.length;
        }
    }

    return offset;

}

Whew! That’s a lot of work.

CodeMirror has some similar logic internally, but it’s not exposed, and not quite what we want. If you were working on making all this work with another editing component, it’s possible this would be more straight-forward.

What does this all give us?

  1. Spell checking and replacements without (nearly as many) glitches in browsers
  2. Speech-to-text without CodeMirror stomping over results
  3. AI writing and rewriting, also without risk of lost data
  4. Transforming of text through other means.

Since we took the control away from the browser and gave it to CodeMirror, we removed most of the risk and instability.

But there are still problems. While this works great on Firefox, Chrome and Safari are a different story. Those browsers are bit more lazy when it comes to spell checking, and even once it’s found some spelling errors, you might not see the red squigglies. Typing, clicking around, or forcing a round of spell checking might bring them back, but might not. But this is their implementation, and not the result of the CodeMirror integration.

Ideally, spell checking would become a first-class citizen on the web. And maybe this will happen someday, but for now, at least there are some workarounds to get it to play nicer with tools like CodeMirror.

We can go further

There’s so much more in InputEvent we could play with. We explored the insertReplacementText and insertText types, but there’s also:

  • insertLink
  • insertFromDrop
  • insertOrderedList
  • formatBold
  • historyUndo

And so many more.

These could be integrated deeper into CodeMirror, which may open some doors to a far more native feel on more platforms. But that’s left as an exercise to the reader (it’s pretty dependent on your CodeMirror modes and the UI you want to provide).

There are also improvements to be made, as this is not perfect yet (but it’s close!). Safari still doesn’t recognize when text is selected, leaving out the AI assisted tools, but Chrome and Firefox work great. We’re working on the rest.

Give it a try

You can try our demo live in your favorite browser. If it doesn’t work for you, let me know what your browser and version are. I’m very curious.

We’ve released this as a new CodeMirror v5 plugin, CodeMirror Speak-and-Spell (NPM). No dependencies. Just drop it into your environment and enable it on your CodeMirror editor, like so:

const codeMirror = new CodeMirror(element, {
  inputStyle: 'contenteditable',
  speakAndSpell: true,
  spellcheck: true,
});

CodeMirror v6 will come in the future, but probably not until we move to v6 (and we’re waiting on a lot of the v5 world to migrate over first).

CodeMirror and Spell Checking: Solved Read More »

Excluding nested node_modules in Rollup.js

We’re often developing multiple Node packages at the same time, symlinking their trees around in order to test them in other projects prior to release.

And sometimes we hit some pretty confusing behavior. Crazy caching issues, confounding crashes, and all manner of chaos. All resulting from one cause: Duplicate modules appearing in our Rollup.js-bundled JavaScript.

For example, we may be developing Ink (our in-progress UI component library) over here with one copy of Spina (our modern Backbone.js successor), and bundling it in Review Board (our open source, code review/document review product) over there with a different copy of Spina. The versions of Spina should be compatible, but technically they’re two separate copies.

And it’s all because of nested node_modules.

The nonsense of nested node_modules

Normally, when Rollup.js bundles code, it looks for any and all node_modules directories in the tree, considering them for dependency resolution.

If a dependency provides its own node_modules, and needs to bundle something from it, Rollup will happily include that copy in the final bundle, even if it’s already including a different copy for another project (such as the top-level project).

This is wasteful at best, and a source of awful nightmare bugs at worst.

In our case, because we’re symlinking source trees around, we’re ending up with Ink’s node_modules sitting inside Review Board’s node_modules (found at node_modules/@beanbag/ink/node_modules.), and we’re getting a copy of Spina from both.

Easily eradicating extra node_modules

Fortunately, it’s easy to resolve in Rollup.js with a simple bit of configuration.

Assuming you’re using @rollup/plugin-node-resolve, tweak the plugin configuration to look like:

{
    plugins: [
        resolve({
            moduleDirectories: [],
            modulePaths: ['node_modules'],
        }),
    ],
}

What we’re doing here is telling Resolve and Rollup two things:

  1. Don’t look for node_modules recursively. moduleDirectories is responsible for looking for the named paths anywhere in the tree, and it defaults to ['node_modules']. This is why it’s even considering the nested copies to begin with.
  2. Explicitly look for a top-level node_modules. modulePaths is responsible for specifying absolute paths or paths relative to the root of the tree where modules should be found. Since we’re no longer looking recursively above, we need to tell it which one we do want.

These two configurations together avoid the dreaded duplicate modules in our situation.

And hopefully it will help you avoid yours, too.

Excluding nested node_modules in Rollup.js Read More »

Re-typing Parent Class Attributes in TypeScript

I was recently working on converting some code away from Backbone.js and toward Spina, our TypeScript Backbone “successor” used in Review Board, and needed to override a type from a parent class.

(I’ll talk about why we still choose to use Backbone-based code another time.)

We basically had this situation:

class BaseClass {
    summary: string | (() => string) = 'BaseClass thing doer';
    description: string | (() => string);
}

class MySubclass extends BaseClass {
    get summary(): string {
        return 'MySubclass thing doer';
    }

    // We'll just make this a standard function, for demo purposes.
    description(): string {
        return 'MySubclass does a thing!';
    }
}

TypeScript doesn’t like that so much:

Class 'BaseClass' defines instance member property 'summary', but extended class 'MySubclass' defines it as an accessor.

Class 'BaseClass' defines instance member property 'description', but extended class 'MySubclass' defines it as instance member function.

Clearly it doesn’t want me to override these members, even though one of the allowed values is a callable returning a string! Which is what we wrote, darnit!!

So what’s going on here?

How ES6 class members work

If you’re coming from another language, you might expect members defined on the class to be class members. For example, you might think you could access BaseClass.summary directly, but you’d be wrong, because these are instance members.

Re-typing Parent Class Attributes in TypeScript Read More »

Scroll to Top