Logo VitNode
Documentation

Single Sign-On (SSO)

Provider Icons and Colors

Put a brand mark and a brand color on your SSO login buttons - an inline SVG or image URL and a hex color from the adapter, or a React component registered in the browser.

Every SSO provider gets a full-width Continue with Provider button on /login and /register. The built-in Google, Discord and Facebook adapters already carry their brand marks and colors, so those buttons look right without any configuration. A custom adapter starts out as a neutral button with text only - a GitHub button with no octocat - and this page shows how to give it a mark and a color.

The VitNode login page with a blurple Continue with Discord button, a white Continue with Google button with the four-color G, and a blue Continue with Facebook button above the email form

From the adapter (SVG or image)

Add an icon to whatever your adapter returns. It travels to the browser with the rest of the provider list, so it has to be a string: inline SVG markup, or a URL the browser can load as an image.

apps/api/src/utils/sso/github.ts
export const GitHubSSOApiPlugin = (): SSOApiPlugin => ({
  id: 'github',
  name: 'GitHub',
  icon: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" /></svg>',
  // ... getUrl, fetchToken, fetchUser
})

Use fill="currentColor" and leave the width and height off: the mark then takes the button's text color in both light and dark mode - white on a brand-colored button - and is sized to match the label. A multicolor mark, such as Google's four-color G, keeps its own fills; give its adapter no brand color so it sits on the neutral button it was designed for.

An image works the same way:

icon: 'https://cdn.example.com/brands/slack.svg' // remote file
icon: '/icons/slack.png' // something in your app's public folder
icon: 'data:image/png;base64,iVBORw0KGgo...' // inlined bytes
What the login page will accept

The markup is inserted into the page, so it is checked before it renders:

  • inline markup must be a single <svg>...</svg> element - an XML prolog, a DOCTYPE and comments around it are fine and get stripped,
  • nothing executable - no <script>, no onload-style attributes, no javascript: URLs, no <foreignObject>, and no <style> block (a <style> inside inline SVG is not scoped, so it would restyle the whole page),
  • image URLs must be https:, http:, a root-relative path (/icons/x.png), or a data:image/... URI,
  • at most 16 KB.

An icon that fails any of those is dropped. The button keeps its name and still signs people in - you lose the mark, never the provider. In development the console says which provider's icon was dropped, so a silent button is never a mystery.

Importing an .svg file

You do not have to paste the markup. Where the adapter is bundled by Vite - apps/web, or any generated VitNode app - import the file itself:

apps/web/src/vitnode.api.config.ts
import githubIcon from './icons/github.svg?raw' // the file's markup
import slackIcon from './icons/slack.svg' // the file's URL

export const vitNodeApiConfig = buildApiConfig({
  authorization: {
    ssoAdapters: [
      GitHubSSOApiPlugin({ icon: githubIcon }),
      SlackSSOApiPlugin({ icon: slackIcon }),
    ],
  },
  // ...
})

Both work: ?raw hands you the file's contents - prolog, DOCTYPE, generator comment and all - and a plain import hands you a URL (/assets/slack-a1b2c3.svg after a build, or an inlined data:image/svg+xml,... for a small file). Prefer ?raw when you want the mark to follow the button's text colour, since a file loaded through <img> cannot see currentColor.

A standalone API app such as apps/api runs on Node with no bundler, so an import of an .svg is not a module there. Read the file instead:

apps/api/src/utils/sso/github.ts
import { readFileSync } from 'node:fs'

const githubIcon = readFileSync(
  new URL('./github.svg', import.meta.url),
  'utf8',
)

Read it once at module scope, not inside getUrl - and remember tsc does not copy .svg files into dist, so either copy it as part of your build or keep the markup in a .ts constant, the way VitNode's own adapters do.

If you would rather have a component than a string, register it in the browser instead - see From the browser.

Brand color

Add a brandColor to fill the button with the provider's color, the way the built-in Discord (#5865F2) and Facebook (#0866FF) adapters do:

apps/api/src/utils/sso/github.ts
export const GitHubSSOApiPlugin = (): SSOApiPlugin => ({
  id: 'github',
  name: 'GitHub',
  icon: githubIcon,
  brandColor: '#24292F',
  // ... getUrl, fetchToken, fetchUser
})

The value has to be a hex color - #RGB or #RRGGBB. The button picks its own text color: white when white text reaches a 4.5:1 contrast ratio on your color, near-black otherwise, so a yellow brand still gets a readable label. A currentColor icon follows that text color.

Leave brandColor out and the button stays neutral, with a border. That is the right choice for providers whose guidelines ask for a neutral button, such as Google and Microsoft. A value that is not a hex color is ignored the same way, and in development the console names the provider.

From the browser (React component or JSX)

If you would rather ship an icon as a component - a lucide-react icon, a component from your own design system, or plain JSX - register it by provider id. A registered icon wins over whatever the adapter sent.

apps/web/src/router.tsx
import { configureSSOIcons } from '@vitnode/core/tanstack/auth'

import { GitHubIcon } from './components/icons/github'

configureSSOIcons({
  github: GitHubIcon, // a component: rendered with `className="size-4"`
  slack: <img alt="" className="size-4" src="/icons/slack.png" />, // or JSX
})

Call it from a module your router entry imports, so it has run before the login screen renders. configureSSOIcons merges, so you can call it more than once - once per plugin, if that suits you.

This is also the escape hatch for a provider whose mark you cannot express as one static SVG: an animated logo, a theme-aware pair of files, or an icon that needs a wrapper element.

Reference

Prop

Type