Steve Kinney

Creating Custom Shiki Themes with CSS Variables

Let's look at ways to create our own Shiki themes using CSS variables.

TL;DR: I created a little tool for tweaking the CSS variables for Shiki themes and you can check it out here.


The Origin Story

As I was building out this site, I fell down the well-meaning rabbit hole of trying to get a perfect score on Lighthouse. Using SvelteKit obviously helped (a lot), but I had one edge case where the color of the comments in my code blocks didn’t have enough contrast with the background.

I also didn’t particularly care for any of the built-in themes that come with Shiki. So, I wanted to style the code blocks myself. Shiki gives you two and a half ways to do this:

  1. Load a theme from JSON, or
  2. Use CSS variables.

Is the first option probably the better option—especially, since I am rendering this entire page at build time? Sure. Did I even end up theming the code blocks differently in dark mode? No. I didn’t not. Stop asking me so many questions.

The Problem with Shiki and Dark Mode

Since I’m building the site at build-time and serving pre-rendered pages, Shiki and I have no idea if you’re using dark mode or not. It turns out that Shiki give you two ways to deal with this:

  1. Render the code block twice and the dark mode code block of @media (prefers-color-scheme: light) and vice versa.
  2. Use CSS variables.

The astute among you will notice that there is a bit of overlap with the second item on both of those lists. At the time of this writing, that’s the approached I chose to go with.

Using CSS Variables

Using CSS variables with Shiki is fairly straight-forward. Like the lists that have come before this next one, there are two steps:

  1. Change the theme to CSS variables.
  2. Add some CSS variables.

The latter can be done like this.

:root {
	--shiki-color-text: #d6deeb;
	--shiki-color-background: #011628;
	--shiki-token-constant: #7fdbca;
	--shiki-token-string: #edc38d;
	--shiki-token-comment: #94a4ad;
	--shiki-token-keyword: #c792e9;
	--shiki-token-parameter: #d6deeb;
	--shiki-token-function: #edc38d;
	--shiki-token-string-expression: #7fdbca;
	--shiki-token-punctuation: #c792e9;
	--shiki-token-link: #79b8ff;
}

Building a Theme Creator

Because of the same faulty wiring in my brain that compelled me to build a blog from scratch before even bothering to sit down and write a single blog post, I decided that I needed to build my own theme creator so that I could tweak the colors to my heart’s content.

The good news is that I’m just going to share it with you in case you’d like to do something similar. You can check out the full version here or play around with the smaller version below.

:root {
--shiki-color-text: #aac569;
--shiki-color-background: #011628;
--shiki-token-constant: #7fdbca;
--shiki-token-string: #edc38d;
--shiki-token-comment: #94a4ad;
--shiki-token-keyword: #c792e9;
--shiki-token-parameter: #d6deeb;
--shiki-token-function: #edc38d;
--shiki-token-string-expression: #7fdbca;
--shiki-token-punctuation: #c792e9;
--shiki-token-link: #79b8ff;
}

#aac569

#011628

#7fdbca

#edc38d

#94a4ad

#c792e9

#d6deeb

#edc38d

#7fdbca

#c792e9

#79b8ff

Examples

def process_order(order):
  check_fraud(order.order_id, order.payment_info)
  prepare_shipment(order)
  charge_confirm = charge(order.order_id, order.payment_info)
  shipment_confirmation = ship(order)
async function transfer(fromAccount: string, toAccount: string, amount: number) {
	// These are activities, not regular functions.
	// Activities may run elsewhere, and their return value
	// is automatically persisted by Temporal.
	await myActivities.withdraw(fromAccount, amount);
	try {
		await myActivities.deposit(toAccount, amount);
	} catch {
		await myActivities.deposit(fromAccount, amount);
	}
}
@activity.defn
async def compose_greeting(input: ComposeGreetingInput) -> str:
    print(f"Invoking activity, attempt number {activity.info().attempt}")
    # Fail the first 3 attempts, succeed the 4th
    if activity.info().attempt < 4:
        raise RuntimeError("Intentional failure" )
    return f"{input.greeting}, {input.name}!"


@workflow.defn
class GreetingWorkflow:
    @workflow.run
    async def run(self, name: str) -> str:
        # By default activities will retry, backing off an initial interval and
        # then using a coefficient of 2 to increase the backoff each time after
        # for an unlimited amount of time and an unlimited number of attempts.
        # We'll keep those defaults except we'll set the maximum interval to
        # just 2 seconds.
        return await workflow.execute_activity(
            compose_greeting,
            ComposeGreetingInput("Hello", name),
            start_to_close_timeout =timedelta(seconds=10),
            retry_policy=RetryPolicy(maximum_interval=timedelta(seconds=2)),
        )
type GreetingParam struct {
	Name string `json:"name"`
}

func GreetingWorkflow(ctx workflow.Context, param *GreetingParam) (string, error) {
	ctx = workflow.WithScheduleToCloseTimeout(ctx, 30*time.Second)
	var greeting string
	err := workflow.ExecuteActivity(ctx, Greet, param).Get(ctx, &greeting)
	return greeting, err
}
func Accept(ctx workflow.Context, input *AcceptWorkflowInput) (*AcceptWorkflowResult, error) {
	err := emailCandidate(ctx, input)
	if err != nil {
		return nil, err
	}
	submission, err := waitForSubmission(ctx)
	if err != nil {
		return nil, err
	}
	return &AcceptWorkflowResult{Submission: submission},
		nil
}

The defaults—and what I’m using at the time of this writing are loosely based on Sarah Drasner’s Night Owl theme.

Last modified on .