Dark / Light
Sistema de dark/light mode embutido no Makuco UI — como funciona, as funções disponíveis e exemplos de integração com React, Angular e HTML puro.
O Makuco UI traz um sistema de dark/light mode embutido, exportado diretamente de @db1/makuco-ui-core.
É framework-agnóstico — funciona em qualquer ambiente JavaScript sem dependências externas.
Conteúdo
- Como funciona
- Funções
- Inicialização
- Lendo o tema atual
- Alterando o tema
- Reagindo a mudanças
- Uso em React
- Uso em Angular
- Sem framework
Como funciona
O sistema de temas opera exclusivamente via CSS. Ao chamar setTheme() ou init(), o atributo
data-theme é escrito no elemento <html>. Os tokens --db1-* do Makuco UI alteram seus valores
de acordo com esse atributo, trocando a paleta de todos os componentes sem nenhum re-render.
html[data-theme="light"] → tokens de modo claro (padrão)
html[data-theme="dark"] → tokens de modo escuroA preferência do usuário é persistida no localStorage sob a chave mk-theme. Quando o valor é
"system", o Makuco UI detecta a preferência do sistema operacional via prefers-color-scheme
e reage a mudanças em tempo real.
Funções
Todas exportadas diretamente de @db1/makuco-ui-core:
import { init, getTheme, setTheme, subscribe } from '@db1/makuco-ui-core';Tipos
type Theme = 'light' | 'dark' | 'system';
interface ThemeState {
/** Preferência explícita do usuário — 'system' quando não foi definida. */
preference: Theme;
/** Valor efetivo após resolver 'system'. Sempre 'light' ou 'dark'. */
resolved: 'light' | 'dark';
}Referência
| Função | Assinatura | Descrição |
|---|---|---|
init | () => () => void | Inicializa o sistema. Aplica data-theme no <html> e inicia o listener de mudanças do SO. Retorna uma função de cleanup. |
getTheme | () => ThemeState | Retorna o estado atual: preferência salva e valor resolvido. |
setTheme | (theme: Theme) => void | 'light' e 'dark' persistem no localStorage; 'system' remove a preferência e volta a seguir o SO. |
subscribe | (cb: (state: ThemeState) => void) => () => void | Registra um callback para mudanças de tema. Retorna uma função de unsubscribe. |
Inicialização
Chame init() uma única vez na inicialização da aplicação. O retorno é uma função de cleanup
que remove listeners e subscribers — chame-a quando a aplicação for destruída.
import { init } from '@db1/makuco-ui-core';
const cleanup = init();
// ao destruir a aplicação:
cleanup();init() realiza três ações:
- Lê a preferência salva no
localStorage(chavemk-theme) - Sem preferência salva, usa
prefers-color-schemecomo fallback - Aplica
data-themeno<html>e registra o listener de mudanças do SO
Lendo o tema atual
import { getTheme } from '@db1/makuco-ui-core';
const { preference, resolved } = getTheme();
// preference → 'light' | 'dark' | 'system'
// resolved → 'light' | 'dark' (nunca 'system')Use resolved para qualquer lógica de renderização — ele sempre contém o valor efetivo.
Alterando o tema
import { setTheme } from '@db1/makuco-ui-core';
setTheme('dark'); // força escuro, persiste no localStorage
setTheme('light'); // força claro, persiste no localStorage
setTheme('system'); // remove preferência; segue o sistema operacionalReagindo a mudanças
Use subscribe() para ser notificado de qualquer mudança — inclusive as originadas pelo SO quando
o tema está em 'system':
import { subscribe } from '@db1/makuco-ui-core';
const unsubscribe = subscribe(({ preference, resolved }) => {
console.log('tema resolvido:', resolved); // 'light' | 'dark'
});
// ao finalizar:
unsubscribe();Uso em React
É JavaScript puro — pode ser integrado num hook sem dependências adicionais.
init() deve ser chamado uma única vez na raiz
O cleanup de init() chama subscribers.clear() internamente, o que removeria todos os
subscribers ao desmontar o componente. Por isso, init() não deve ficar dentro de um hook
usado em múltiplos componentes. Chame-o uma única vez na raiz e use subscribe nos hooks.
Inicializar na raiz
// App.tsx / root layout
import { init } from '@db1/makuco-ui-core';
import { useEffect } from 'react';
export function AppRoot({ children }: { children: React.ReactNode }) {
useEffect(() => init(), []); // init() retorna o cleanup — usado como cleanup do effect
return <>{children}</>;
}Hook useTheme
O hook assina o sistema de temas, sem inicializá-lo:
// use-theme.ts
import { useEffect, useState } from 'react';
import { getTheme, setTheme, subscribe, type ThemeState } from '@db1/makuco-ui-core';
export function useTheme() {
const [state, setState] = useState<ThemeState>(getTheme);
useEffect(() => {
setState(getTheme()); // sincroniza caso o tema tenha mudado antes do efeito montar
return subscribe(setState);
}, []);
return { ...state, setTheme };
}setTheme é uma referência estável (função de módulo) — não precisa de useCallback.
Usando o hook
import { useTheme } from './use-theme';
export function ThemeToggle() {
const { resolved, setTheme } = useTheme();
return (
<button onClick={() => setTheme(resolved === 'dark' ? 'light' : 'dark')}>
{resolved === 'dark' ? '☀ Modo claro' : '☾ Modo escuro'}
</button>
);
}Next.js / next-themes
Se o projeto já usa next-themes, sincronize os dois sistemas num componente wrapper: ao detectar
mudanças do next-themes, chame setTheme() do Makuco UI. Evite chamar init() em paralelo
ao next-themes — use um único ponto de entrada para o atributo data-theme.
Uso em Angular
Em Angular, exponha o sistema de temas como um Injectable reativo.
ThemeService
// theme.service.ts
import { Injectable, OnDestroy, signal } from '@angular/core';
import {
init,
getTheme,
setTheme as coreSetTheme,
subscribe,
type Theme,
type ThemeState,
} from '@db1/makuco-ui-core';
@Injectable({ providedIn: 'root' })
export class ThemeService implements OnDestroy {
readonly state = signal<ThemeState>(getTheme());
private readonly cleanup: () => void;
private readonly unsubscribe: () => void;
constructor() {
this.cleanup = init();
this.unsubscribe = subscribe((state) => this.state.set(state));
}
setTheme(theme: Theme): void {
coreSetTheme(theme);
}
ngOnDestroy(): void {
this.unsubscribe();
this.cleanup();
}
}Usando o serviço
// theme-toggle.component.ts
import { Component, inject } from '@angular/core';
import { ThemeService } from './theme.service';
@Component({
selector: 'app-theme-toggle',
standalone: true,
template: `
<button (click)="toggle()">
{{ theme.state().resolved === 'dark' ? '☀ Modo claro' : '☾ Modo escuro' }}
</button>
`,
})
export class ThemeToggleComponent {
readonly theme = inject(ThemeService);
toggle(): void {
const next = this.theme.state().resolved === 'dark' ? 'light' : 'dark';
this.theme.setTheme(next);
}
}APP_INITIALIZER
Para garantir que data-theme seja aplicado antes do primeiro render, registre init como
inicializador da aplicação:
// main.ts
import { APP_INITIALIZER } from '@angular/core';
import { init } from '@db1/makuco-ui-core';
bootstrapApplication(AppComponent, {
providers: [
{
provide: APP_INITIALIZER,
useFactory: () => init,
multi: true,
},
],
});Sem framework
Funciona também diretamente em qualquer ambiente JavaScript moderno:
import { init, setTheme, subscribe } from '@db1/makuco-ui-core';
document.addEventListener('DOMContentLoaded', () => {
const cleanup = init();
document.querySelector('#btn-dark')
?.addEventListener('click', () => setTheme('dark'));
document.querySelector('#btn-light')
?.addEventListener('click', () => setTheme('light'));
document.querySelector('#btn-system')
?.addEventListener('click', () => setTheme('system'));
subscribe(({ resolved }) => {
document.querySelector('#theme-indicator')!.textContent = resolved;
});
});