Skip to content

Commit 00413d0

Browse files
cleanup
1 parent 1ed03f5 commit 00413d0

File tree

15 files changed

+379
-362
lines changed

15 files changed

+379
-362
lines changed

README.md

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ Customizable Svelte components for theme selection (light mode / dark mode) insp
88

99
## About
1010

11-
The [TailwindCSS site](https://tailwindcss.com/) is a great example of excellent UX and the theme select option is particularly nice. A simple icon gives access to a menu to set the theme to light mode or dark mode, or allow the system to automatically switch based on your device configuration (which can change based on the time of day).
11+
The [TailwindCSS site](https://tailwindcss.com/) is a great example of excellent UX. A simple icon gives access to a menu to set the theme to light mode or dark mode, or allow the system to automatically switch based on your device configuration (which can change based on the time of day).
1212

1313
A particularly nice touch is that it shows if the default system mode has been overridden using a different icon color.
1414

1515
For mobile users, where the navigation bar shrinks to become an expandable menu, they have a larger-hit-target version providing the same features.
1616

17+
They have since switched to a single set of icons allowing quick switching between system, light, or dark modes that is compact and works well on mobile or desktop.
18+
1719
This project re-creates these UI widgets and provides the system to persist and apply the selected theme in your SvelteKit project. Please refer to [TailwindCSS Dark Mode](https://tailwindcss.com/docs/dark-mode) documentation for how to use dark mode styles within your app.
1820

1921
## Usage
@@ -78,14 +80,4 @@ Everything should then be in place to design using the TailWindCSS classes inclu
7880

7981
The UI widgets can be customized to match whatever TailwindCSS colors scheme you are using, the default icons can be replaced if desired, and the "Light", "Dark", and "System" labels replaced.
8082

81-
### Colors
82-
83-
Coming soon
84-
85-
### Icons
86-
87-
Coming soon
88-
89-
### Labels
90-
91-
Coming soon
83+
TODO: document customizations

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "svelte-theme-select",
3-
"version": "0.1.3",
3+
"version": "0.1.4",
44
"type": "module",
55
"keywords": [
66
"svelte",
@@ -49,18 +49,18 @@
4949
},
5050
"devDependencies": {
5151
"@sveltejs/adapter-static": "^3.0.10",
52-
"@sveltejs/kit": "^2.46.4",
52+
"@sveltejs/kit": "^2.47.0",
5353
"@sveltejs/package": "^2.5.4",
5454
"@sveltejs/vite-plugin-svelte": "^6.2.1",
5555
"@tailwindcss/vite": "^4.1.14",
5656
"autoprefixer": "^10.4.21",
5757
"publint": "^0.3.14",
58-
"svelte": "^5.39.11",
58+
"svelte": "^5.40.1",
5959
"svelte-check": "^4.3.3",
6060
"tailwindcss": "^4.1.14",
6161
"tslib": "^2.8.1",
6262
"typescript": "^5.9.3",
63-
"vite": "^7.1.9"
63+
"vite": "^7.1.10"
6464
},
6565
"dependencies": {
6666
"esm-env": "^1.2.2",

pnpm-lock.yaml

Lines changed: 180 additions & 180 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/Icon.svelte

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/lib/Theme.svelte

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,67 @@
1+
<script lang="ts" module>
2+
import { Theme } from './state.svelte'
3+
4+
export { defaultIcons }
5+
</script>
6+
17
<!-- this sets initial dark mode class based on user preference / device settings (in page <head> to avoid FOUC) -->
28
<svelte:head>
39
<script>document.documentElement.classList.toggle('dark', localStorage.theme === 'dark' || !localStorage.theme && window.matchMedia('(prefers-color-scheme: dark)').matches)</script>
410
</svelte:head>
11+
12+
{#snippet defaultIcons(theme: Theme, active: boolean)}
13+
{@const iconActiveStroke = 'stroke-sky-500'}
14+
{@const iconActiveFill = 'fill-sky-500'}
15+
{@const iconActiveShade = 'fill-sky-400/20'}
16+
{@const iconStroke = 'stroke-slate-400'}
17+
{@const iconFill = 'fill-slate-400'}
18+
19+
{#if theme === 'dark'}
20+
<svg viewBox="0 0 24 24" fill="none" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
21+
<path
22+
class="{active ? iconActiveShade : iconStroke}"
23+
fill-rule="evenodd"
24+
clip-rule="evenodd"
25+
d="M17.715 15.15A6.5 6.5 0 0 1 9 6.035C6.106 6.922 4 9.645 4 12.867c0 3.94 3.153 7.136 7.042 7.136 3.101 0 5.734-2.032 6.673-4.853Z"
26+
/>
27+
<path
28+
class="{active ? iconActiveFill : iconFill}"
29+
d="m17.715 15.15.95.316a1 1 0 0 0-1.445-1.185l.495.869ZM9 6.035l.846.534a1 1 0 0 0-1.14-1.49L9 6.035Zm8.221 8.246a5.47 5.47 0 0 1-2.72.718v2a7.47 7.47 0 0 0 3.71-.98l-.99-1.738Zm-2.72.718A5.5 5.5 0 0 1 9 9.5H7a7.5 7.5 0 0 0 7.5 7.5v-2ZM9 9.5c0-1.079.31-2.082.845-2.93L8.153 5.5A7.47 7.47 0 0 0 7 9.5h2Zm-4 3.368C5 10.089 6.815 7.75 9.292 6.99L8.706 5.08C5.397 6.094 3 9.201 3 12.867h2Zm6.042 6.136C7.718 19.003 5 16.268 5 12.867H3c0 4.48 3.588 8.136 8.042 8.136v-2Zm5.725-4.17c-.81 2.433-3.074 4.17-5.725 4.17v2c3.552 0 6.553-2.327 7.622-5.537l-1.897-.632Z"
30+
/>
31+
<path
32+
class="{active ? iconActiveFill : iconFill}"
33+
fill-rule="evenodd"
34+
clip-rule="evenodd"
35+
d="M17 3a1 1 0 0 1 1 1 2 2 0 0 0 2 2 1 1 0 1 1 0 2 2 2 0 0 0-2 2 1 1 0 1 1-2 0 2 2 0 0 0-2-2 1 1 0 1 1 0-2 2 2 0 0 0 2-2 1 1 0 0 1 1-1Z"
36+
/>
37+
</svg>
38+
{/if}
39+
40+
{#if theme === 'light'}
41+
<svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
42+
<path class="{active ? iconActiveStroke + ' ' + iconActiveShade : iconStroke}" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
43+
<path
44+
class={active ? iconActiveStroke : iconStroke}
45+
d="M12 4v1M17.66 6.344l-.828.828M20.005 12.004h-1M17.66 17.664l-.828-.828M12 20.01V19M6.34 17.664l.835-.836M3.995 12.004h1.01M6 6l.835.836"
46+
/>
47+
</svg>
48+
{/if}
49+
50+
{#if theme === 'system'}
51+
<svg viewBox="0 0 24 24" fill="none">
52+
<path
53+
class="{active ? iconActiveStroke + ' ' + iconActiveShade : iconStroke}"
54+
d="M4 6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6Z"
55+
stroke-width="2"
56+
stroke-linejoin="round"
57+
/>
58+
<path
59+
class="{active ? iconActiveStroke : iconStroke}"
60+
d="M14 15c0 3 2 5 2 5H8s2-2 2-5"
61+
stroke-width="2"
62+
stroke-linecap="round"
63+
stroke-linejoin="round"
64+
/>
65+
</svg>
66+
{/if}
67+
{/snippet}

src/lib/ThemeRadio.svelte

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script lang="ts">
2+
import { BROWSER } from 'esm-env'
3+
import { theme } from './state.svelte'
4+
</script>
5+
6+
<div class="relative z-0 inline-grid grid-cols-3 gap-0.5 rounded-full bg-gray-950/5 p-0.75 text-gray-950 dark:bg-white/10 dark:text-white {BROWSER ? 'opacity-100' : 'opacity-0'}" role="radiogroup">
7+
<button class="rounded-full p-1.5 *:size-7 aria-checked:bg-white aria-checked:ring aria-checked:inset-ring aria-checked:ring-gray-950/10 aria-checked:inset-ring-white/10 sm:p-0 dark:aria-checked:bg-gray-700 dark:aria-checked:text-white dark:aria-checked:ring-transparent" aria-label="System theme" role="radio" aria-checked={theme.override === 'system'} tabindex="0" onclick={() => theme.override = 'system'}>
8+
<svg viewBox="0 0 28 28" fill="none"><path d="M7.5 8.5C7.5 7.94772 7.94772 7.5 8.5 7.5H19.5C20.0523 7.5 20.5 7.94772 20.5 8.5V16.5C20.5 17.0523 20.0523 17.5 19.5 17.5H8.5C7.94772 17.5 7.5 17.0523 7.5 16.5V8.5Z" stroke="currentColor"></path><path d="M7.5 8.5C7.5 7.94772 7.94772 7.5 8.5 7.5H19.5C20.0523 7.5 20.5 7.94772 20.5 8.5V14.5C20.5 15.0523 20.0523 15.5 19.5 15.5H8.5C7.94772 15.5 7.5 15.0523 7.5 14.5V8.5Z" stroke="currentColor"></path><path d="M16.5 20.5V17.5H11.5V20.5M16.5 20.5H11.5M16.5 20.5H17.5M11.5 20.5H10.5" stroke="currentColor" stroke-linecap="round"></path></svg>
9+
</button>
10+
11+
<button class="rounded-full p-1.5 *:size-7 aria-checked:bg-white aria-checked:ring aria-checked:inset-ring aria-checked:ring-gray-950/10 aria-checked:inset-ring-white/10 sm:p-0 dark:aria-checked:bg-gray-700 dark:aria-checked:text-white dark:aria-checked:ring-transparent" aria-label="Light theme" role="radio" aria-checked={theme.override === 'light'} tabindex="0" onclick={() => theme.override = 'light'}>
12+
<svg viewBox="0 0 28 28" fill="none"><circle cx="14" cy="14" r="3.5" stroke="currentColor"></circle><path d="M14 8.5V6.5" stroke="currentColor" stroke-linecap="round"></path><path d="M17.889 10.1115L19.3032 8.69727" stroke="currentColor" stroke-linecap="round"></path><path d="M19.5 14L21.5 14" stroke="currentColor" stroke-linecap="round"></path><path d="M17.889 17.8885L19.3032 19.3027" stroke="currentColor" stroke-linecap="round"></path><path d="M14 21.5V19.5" stroke="currentColor" stroke-linecap="round"></path><path d="M8.69663 19.3029L10.1108 17.8887" stroke="currentColor" stroke-linecap="round"></path><path d="M6.5 14L8.5 14" stroke="currentColor" stroke-linecap="round"></path><path d="M8.69663 8.69711L10.1108 10.1113" stroke="currentColor" stroke-linecap="round"></path>
13+
</svg>
14+
</button>
15+
16+
<button class="rounded-full p-1.5 *:size-7 aria-checked:bg-white aria-checked:ring aria-checked:inset-ring aria-checked:ring-gray-950/10 aria-checked:inset-ring-white/10 sm:p-0 dark:aria-checked:bg-gray-700 dark:aria-checked:text-white dark:aria-checked:ring-transparent" aria-label="Dark theme" role="radio" aria-checked={theme.override === 'dark'} tabindex="0" onclick={() => theme.override = 'dark'}>
17+
<svg viewBox="0 0 28 28" fill="none"><path d="M10.5 9.99914C10.5 14.1413 13.8579 17.4991 18 17.4991C19.0332 17.4991 20.0176 17.2902 20.9132 16.9123C19.7761 19.6075 17.109 21.4991 14 21.4991C9.85786 21.4991 6.5 18.1413 6.5 13.9991C6.5 10.8902 8.39167 8.22304 11.0868 7.08594C10.7089 7.98159 10.5 8.96597 10.5 9.99914Z" stroke="currentColor" stroke-linejoin="round"></path><path d="M16.3561 6.50754L16.5 5.5L16.6439 6.50754C16.7068 6.94752 17.0525 7.29321 17.4925 7.35607L18.5 7.5L17.4925 7.64393C17.0525 7.70679 16.7068 8.05248 16.6439 8.49246L16.5 9.5L16.3561 8.49246C16.2932 8.05248 15.9475 7.70679 15.5075 7.64393L14.5 7.5L15.5075 7.35607C15.9475 7.29321 16.2932 6.94752 16.3561 6.50754Z" fill="currentColor" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20.3561 11.5075L20.5 10.5L20.6439 11.5075C20.7068 11.9475 21.0525 12.2932 21.4925 12.3561L22.5 12.5L21.4925 12.6439C21.0525 12.7068 20.7068 13.0525 20.6439 13.4925L20.5 14.5L20.3561 13.4925C20.2932 13.0525 19.9475 12.7068 19.5075 12.6439L18.5 12.5L19.5075 12.3561C19.9475 12.2932 20.2932 11.9475 20.3561 11.5075Z" fill="currentColor" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
18+
</svg>
19+
</button>
20+
</div>

src/lib/ThemeSelect.svelte

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,55 @@
11
<script lang="ts">
2-
import { browser } from '$app/environment'
3-
import Icon from './Icon.svelte'
4-
import { theme, themes, type Theme } from './state.svelte'
2+
import { BROWSER } from 'esm-env'
3+
import type { Snippet } from 'svelte'
4+
import { theme, Theme } from './state.svelte'
5+
import { defaultIcons } from './Theme.svelte'
6+
7+
interface Props {
8+
selectLabel?: string
9+
selectButton?: string
10+
selectIcon?: string
11+
labels?: Record<Theme, string>
12+
icons?: Snippet<[Theme, boolean]>
13+
}
14+
15+
let {
16+
selectLabel = 'text-slate-700 dark:text-slate-500',
17+
selectButton = 'ring-1 ring-slate-900/10 rounded-lg shadow-sm p-2 text-slate-700 bg-white dark:bg-slate-600 dark:ring-0 dark:highlight-white/5 dark:text-slate-200',
18+
selectIcon = 'text-slate-400',
19+
labels = {
20+
light: 'Light',
21+
dark: 'Dark',
22+
system: 'System',
23+
},
24+
icons = defaultIcons,
25+
}: Props = $props()
526
627
function onChange(e: Event) {
728
const el = e.target as HTMLSelectElement
829
const value = el.value as Theme
9-
theme.current = value
30+
theme.override = value
1031
}
1132
</script>
1233

1334
<div class="flex items-center justify-between">
14-
<label for="theme" class="font-normal {theme.colors.selectLabel}">Switch theme</label>
15-
<div class="relative flex items-center gap-2 font-semibold {theme.colors.selectButton} {browser ? 'opacity-100' : 'opacity-0'}">
16-
<Icon />
17-
{theme.labels[theme.override]}
18-
<svg class="size-6 {theme.colors.selectIcon}" fill="none">
35+
<label for="theme" class="font-normal {selectLabel}">Switch theme</label>
36+
<div class="relative flex items-center gap-2 font-semibold {selectButton} {BROWSER ? 'opacity-100' : 'opacity-0'}">
37+
<span class="size-6" hidden={theme.override === 'system'}>
38+
<span class="dark:hidden inline">{@render icons('light', true)}</span>
39+
<span class="hidden dark:inline">{@render icons('dark', true)}</span>
40+
</span>
41+
<span class="size-6" hidden={theme.override !== 'system'}>
42+
<span class="dark:hidden inline">{@render icons('light', false)}</span>
43+
<span class="hidden dark:inline">{@render icons('dark', false)}</span>
44+
</span>
45+
{labels[theme.override]}
46+
<svg class="size-6 {selectIcon}" fill="none">
1947
<path d="m15 11-3 3-3-3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
2048
</svg>
2149
<select value={theme.override} onchange={onChange} class="absolute appearance-none inset-0 w-full h-full opacity-0">
22-
{#each themes as value}
50+
{#each Theme as value}
2351
<option {value}>
24-
{theme.labels[value]}
52+
{labels[value]}
2553
</option>
2654
{/each}
2755
</select>

src/lib/ThemeToggle.svelte

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
11
<script lang="ts">
2+
import type { Snippet } from 'svelte'
23
import Transition from 'svelte-transition'
34
import { createMenu } from 'svelte-headlessui'
4-
import { theme, themes, type Theme } from './state.svelte'
5-
import Icon from './Icon.svelte'
5+
import { theme, Theme } from './state.svelte'
6+
import { defaultIcons } from './Theme.svelte'
7+
8+
interface Props {
9+
dropdownList?: string
10+
dropdownHover?: string
11+
textActive?: string
12+
labels?: Record<Theme, string>
13+
icons?: Snippet<[Theme, boolean]>
14+
}
15+
16+
let {
17+
dropdownList = 'text-slate-700 bg-white ring-1 ring-black/5 dark:bg-slate-700 dark:ring-0 dark:highlight-white/5 dark:text-slate-300',
18+
dropdownHover = 'hover:bg-slate-50 hover:dark:bg-slate-800/50',
19+
textActive = 'text-sky-500',
20+
labels = {
21+
light: 'Light',
22+
dark: 'Dark',
23+
system: 'System',
24+
},
25+
icons = defaultIcons,
26+
}: Props = $props()
627
728
const menu = createMenu({ label: 'Theme' })
829
@@ -22,15 +43,22 @@
2243
function closed() {
2344
// apply any pending setting once closed
2445
if (pending) {
25-
theme.current = pending
46+
theme.override = pending
2647
pending = undefined
2748
}
2849
}
2950
</script>
3051

3152
<div class="relative inline-block">
3253
<button class="w-6 h-6 leading-none" use:menu.button onchange={change}>
33-
<Icon />
54+
<span class="size-6" hidden={theme.override === 'system'}>
55+
<span class="dark:hidden inline">{@render icons('light', true)}</span>
56+
<span class="hidden dark:inline">{@render icons('dark', true)}</span>
57+
</span>
58+
<span class="size-6" hidden={theme.override !== 'system'}>
59+
<span class="dark:hidden inline">{@render icons('light', false)}</span>
60+
<span class="hidden dark:inline">{@render icons('dark', false)}</span>
61+
</span>
3462
</button>
3563

3664
<Transition
@@ -43,18 +71,16 @@
4371
leaveTo="transform opacity-0 scale-95"
4472
on:after-leave={closed}
4573
>
46-
<ul class="origin-top-right absolute right-0 py-1 mt-2 w-28 rounded-md shadow-lg focus:outline-none {theme.colors.dropdownList}" use:menu.items>
47-
{#each themes as value}
74+
<ul class="origin-top-right absolute right-0 py-1 mt-2 w-28 rounded-md shadow-lg focus:outline-none {dropdownList}" use:menu.items>
75+
{#each Theme as value}
4876
{@const active = value === theme.override}
4977
<li
50-
class="flex items-center px-2 py-1 text-sm font-semibold cursor-pointer {theme.colors.dropdownHover} {active
51-
? theme.colors.textActive
52-
: ''}"
78+
class="flex items-center px-2 py-1 text-sm font-semibold cursor-pointer {dropdownHover} {active ? textActive : ''}"
5379
use:menu.item={{ value }}
5480
>
55-
<span class="w-6 h-6 mr-2" hidden={!active}>{@html theme.icons[value](true)}</span>
56-
<span class="w-6 h-6 mr-2" hidden={active}>{@html theme.icons[value](false)}</span>
57-
{theme.labels[value]}
81+
<span class="w-6 h-6 mr-2" hidden={!active}>{@render icons(value, true)}</span>
82+
<span class="w-6 h-6 mr-2" hidden={active}>{@render icons(value, false)}</span>
83+
{labels[value]}
5884
</li>
5985
{/each}
6086
</ul>

src/lib/colors.ts

Lines changed: 0 additions & 29 deletions
This file was deleted.

0 commit comments

Comments
 (0)