S-015 Loading moderate

Hourglass Loader 砂時計ローダー

Recognizable CSS-only hourglass with sand drainage, stream, and full rotation. 砂の流下と180度反転をCSSのみで表現した砂時計ローダー。

ライブデモ Live Demo

Loading

読み込み中... Loading...

概要・用途・特徴Overview, Usage & Features

何ができるかWhat it does

A recognizable hourglass animation with dynamic upper sand emptying, stream flowing, lower sand filling, and a periodic flip.

上部の砂の減少、落ちる粒のストリーム、下部の蓄積と定期的な180度反転をCSSのみで再現した砂時計ローダー。

どこで使うかWhere to use

Used inside loading screens, cards, modals, or centered async wrappers.

ローディング画面、カードコンポーネント、モーダル、または非同期データ待機エリアで使用します。

特徴Key features

Pure CSS keyframes, scoped custom properties, accessibility support with role="status", prefers-reduced-motion fallbacks, and pause/resume states.

CSSアニメーションのみで構成、スコープ付きカスタムプロパティ、role="status"によるアクセシビリティ対応、prefers-reduced-motion時の静的表示、一時停止対応。

調整可能パラメータ Adjustable Parameters

Parameter Default Description
duration1.4sAnimation cycle duration
size64pxWidth and height of the loader container
color#818cf8Primary accent color of the loader

実装コード Implementation Code

<div class="s015-loader" role="status" aria-label="Loading">
  <span class="s015-sr-only">Loading</span>
  <div class="s015-glass" aria-hidden="true">
    <div class="s015-top-bulb">
      <div class="s015-top-sand"></div>
    </div>
    <div class="s015-stream"></div>
    <div class="s015-bottom-bulb">
      <div class="s015-bottom-sand"></div>
    </div>
  </div>
</div>
.s015-loader {
  --s015-size: 64px;
  --s015-duration: 1.4s;
  --s015-color: #818cf8;
  --s015-accent: #38bdf8;
  position: relative;
  width: var(--s015-size);
  height: var(--s015-size);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
}

.s015-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.s015-glass {
  position: relative;
  width: calc(var(--s015-size) * 0.52);
  height: calc(var(--s015-size) * 0.78);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  border-top: 3px solid var(--s015-color);
  border-bottom: 3px solid var(--s015-color);
  box-sizing: border-box;
  animation: s015-flip var(--s015-duration) cubic-bezier(0.65, 0, 0.35, 1) infinite;
}

.s015-top-bulb,
.s015-bottom-bulb {
  position: relative;
  width: 100%;
  height: 45%;
  overflow: hidden;
  background: rgba(129, 140, 248, 0.08);
}

.s015-top-bulb {
  clip-path: polygon(0% 0%, 100% 0%, 55% 100%, 45% 100%);
  border: 1.5px solid rgba(129, 140, 248, 0.4);
  border-bottom: none;
}

.s015-bottom-bulb {
  clip-path: polygon(45% 0%, 55% 0%, 100% 100%, 0% 100%);
  border: 1.5px solid rgba(129, 140, 248, 0.4);
  border-top: none;
}

.s015-top-sand {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: var(--s015-accent);
  transform-origin: bottom center;
  animation: s015-drain var(--s015-duration) linear infinite;
}

.s015-bottom-sand {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: var(--s015-accent);
  transform-origin: bottom center;
  animation: s015-fill var(--s015-duration) linear infinite;
}

.s015-stream {
  position: absolute;
  top: 42%;
  left: 50%;
  width: 2px;
  height: 20%;
  background: var(--s015-accent);
  transform: translateX(-50%);
  opacity: 0.85;
}

@keyframes s015-drain {
  0% { transform: scaleY(1); }
  75%, 100% { transform: scaleY(0); }
}

@keyframes s015-fill {
  0% { transform: scaleY(0); }
  75%, 100% { transform: scaleY(1); }
}

@keyframes s015-flip {
  0%, 75% { transform: rotate(0deg); }
  85%, 100% { transform: rotate(180deg); }
}

.s015-loader.paused,
.s015-loader[data-paused="true"],
.s015-loader.paused *,
.s015-loader[data-paused="true"] * {
  animation-play-state: paused !important;
}

@media screen and (prefers-reduced-motion: reduce) {
  .s015-glass {
    animation: none !important;
    transform: none !important;
  }
  .s015-top-sand,
  .s015-bottom-sand {
    animation: none !important;
    transform: scaleY(0.5);
  }
  .s015-stream {
    opacity: 0.4;
  }
}
import React from 'react';
import './S-015.css';

export default function S015HourglassLoader({
  size = 64,
  duration = 1.4,
  color = '#818cf8',
  label = 'Loading',
  paused = false,
  className = ''
}) {
  const rootStyle = {
    '--s015-size': typeof size === 'number' ? `${size}px` : size,
    '--s015-duration': typeof duration === 'number' ? `${duration}s` : duration,
    '--s015-color': color
  };

  const classNames = [
    's015-loader',
    paused ? 'paused' : '',
    className
  ].filter(Boolean).join(' ');

  return (
    <div
      className={classNames}
      style={rootStyle}
      role="status"
      aria-label={label}
      data-paused={paused ? 'true' : undefined}
    >
  <span className="s015-sr-only">{label}</span>
  <div className="s015-glass" aria-hidden="true">
    <div className="s015-top-bulb">
      <div className="s015-top-sand" />
    </div>
    <div className="s015-stream" />
    <div className="s015-bottom-bulb">
      <div className="s015-bottom-sand" />
    </div>
  </div>
    </div>
  );
}
.s015-loader {
  --s015-size: 64px;
  --s015-duration: 1.4s;
  --s015-color: #818cf8;
  --s015-accent: #38bdf8;
  position: relative;
  width: var(--s015-size);
  height: var(--s015-size);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
}

.s015-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.s015-glass {
  position: relative;
  width: calc(var(--s015-size) * 0.52);
  height: calc(var(--s015-size) * 0.78);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  border-top: 3px solid var(--s015-color);
  border-bottom: 3px solid var(--s015-color);
  box-sizing: border-box;
  animation: s015-flip var(--s015-duration) cubic-bezier(0.65, 0, 0.35, 1) infinite;
}

.s015-top-bulb,
.s015-bottom-bulb {
  position: relative;
  width: 100%;
  height: 45%;
  overflow: hidden;
  background: rgba(129, 140, 248, 0.08);
}

.s015-top-bulb {
  clip-path: polygon(0% 0%, 100% 0%, 55% 100%, 45% 100%);
  border: 1.5px solid rgba(129, 140, 248, 0.4);
  border-bottom: none;
}

.s015-bottom-bulb {
  clip-path: polygon(45% 0%, 55% 0%, 100% 100%, 0% 100%);
  border: 1.5px solid rgba(129, 140, 248, 0.4);
  border-top: none;
}

.s015-top-sand {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: var(--s015-accent);
  transform-origin: bottom center;
  animation: s015-drain var(--s015-duration) linear infinite;
}

.s015-bottom-sand {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: var(--s015-accent);
  transform-origin: bottom center;
  animation: s015-fill var(--s015-duration) linear infinite;
}

.s015-stream {
  position: absolute;
  top: 42%;
  left: 50%;
  width: 2px;
  height: 20%;
  background: var(--s015-accent);
  transform: translateX(-50%);
  opacity: 0.85;
}

@keyframes s015-drain {
  0% { transform: scaleY(1); }
  75%, 100% { transform: scaleY(0); }
}

@keyframes s015-fill {
  0% { transform: scaleY(0); }
  75%, 100% { transform: scaleY(1); }
}

@keyframes s015-flip {
  0%, 75% { transform: rotate(0deg); }
  85%, 100% { transform: rotate(180deg); }
}

.s015-loader.paused,
.s015-loader[data-paused="true"],
.s015-loader.paused *,
.s015-loader[data-paused="true"] * {
  animation-play-state: paused !important;
}

@media screen and (prefers-reduced-motion: reduce) {
  .s015-glass {
    animation: none !important;
    transform: none !important;
  }
  .s015-top-sand,
  .s015-bottom-sand {
    animation: none !important;
    transform: scaleY(0.5);
  }
  .s015-stream {
    opacity: 0.4;
  }
}

仕組みとカスタマイズHow It Works & Customization

仕組みHow it works

Combines CSS custom properties and keyframe transforms to animate visual geometry smoothly while keeping assistive technologies informed.

CSSカスタムプロパティとキーフレームを組み合わせ、支援技術に配慮しながら滑らかなアニメーションを実行します。

カスタマイズ方法Customization

Adjust duration, size, and accent colors via scoped CSS variables or React component props.

CSSカスタムプロパティまたはReactコンポーネントのProps経由でサイズ・周期・色を自在に変更可能です。

注意点Caveats

Always ensure meaningful label text for screen reader users and preserve reduced-motion overrides.

スクリーンリーダー向けのラベルテキストを適切に指定し、prefers-reduced-motion設定を上書きしないようご注意ください。

よくある質問 Frequently Asked Questions

How can I change the animation speed of Hourglass Loader? 砂時計ローダーのアニメーション速度を変更するにはどうすればよいですか?

Override the --s015-duration CSS variable or pass the duration prop to the React component.

CSS変数 --s015-duration を指定するか、Reactコンポーネントの duration プロパティを設定してください。

Does this snippet support prefers-reduced-motion? prefers-reduced-motion(視覚効果の抑制設定)に対応していますか?

Yes, it provides explicit media query overrides that freeze or stabilize animations without disappearing.

はい。メディアクエリによりアニメーションを停止し、要素が非表示にならない安定した静止形状を表示します。

AIへの指示テンプレート AI Prompt Template

以下をAIに貼り付けると実装を依頼できます。 Paste the following into your AI assistant to request implementation.